Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
468bb5579d | ||
|
|
9d2aa879e9 |
@@ -1,9 +1,12 @@
|
|||||||
.editor-page-shell {
|
.editor-page-shell {
|
||||||
min-height: 100vh;
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
min-height: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 56px minmax(0, 1fr) 32px;
|
grid-template-rows: 56px minmax(0, 1fr) 32px;
|
||||||
background: #e8e8e5;
|
background: #e8e8e5;
|
||||||
color: #111111;
|
color: #111111;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-page-shell :focus-visible {
|
.editor-page-shell :focus-visible {
|
||||||
@@ -124,6 +127,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 280px minmax(0, 1fr) 320px;
|
grid-template-columns: 280px minmax(0, 1fr) 320px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-assets-panel,
|
.editor-assets-panel,
|
||||||
@@ -542,13 +546,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
@media (max-width: 760px) {
|
||||||
.editor-page-shell { grid-template-rows: auto minmax(0, 1fr) auto; }
|
.editor-page-shell { height: auto; min-height: 100dvh; grid-template-rows: auto minmax(0, 1fr) auto; overflow: visible; }
|
||||||
.editor-toolbar { display: flex; min-height: 56px; flex-wrap: wrap; gap: 8px; padding: 8px 10px; }
|
.editor-toolbar { display: flex; min-height: 56px; flex-wrap: wrap; gap: 8px; padding: 8px 10px; }
|
||||||
.editor-title { min-width: 0; flex: 1 1 calc(100% - 56px); }
|
.editor-title { min-width: 0; flex: 1 1 calc(100% - 56px); }
|
||||||
.editor-history-actions { order: 3; }
|
.editor-history-actions { order: 3; }
|
||||||
.editor-save-status { order: 4; flex: 1 1 128px; }
|
.editor-save-status { order: 4; flex: 1 1 128px; }
|
||||||
.editor-toolbar-controls > button { display: block; order: 5; }
|
.editor-toolbar-controls > button { display: block; order: 5; }
|
||||||
.editor-layout { grid-template-columns: 1fr; }
|
.editor-layout { grid-template-columns: 1fr; overflow: visible; }
|
||||||
.editor-assets-panel, .editor-inspector { border: 0; }
|
.editor-assets-panel, .editor-inspector { border: 0; }
|
||||||
.editor-assets-panel { order: 2; }
|
.editor-assets-panel { order: 2; }
|
||||||
.editor-inspector { order: 3; }
|
.editor-inspector { order: 3; }
|
||||||
|
|||||||
@@ -160,6 +160,20 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
const conflictExportGuardRef = useRef(new ConflictExportGuard());
|
const conflictExportGuardRef = useRef(new ConflictExportGuard());
|
||||||
const candidateMenuRef = useRef<HTMLDivElement | null>(null);
|
const candidateMenuRef = useRef<HTMLDivElement | null>(null);
|
||||||
const candidateTriggerRef = useRef<HTMLButtonElement | null>(null);
|
const candidateTriggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const noticeTimerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||||
|
|
||||||
|
function showNotice(message: string) {
|
||||||
|
if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current);
|
||||||
|
setNotice(message);
|
||||||
|
noticeTimerRef.current = setTimeout(() => {
|
||||||
|
setNotice("");
|
||||||
|
noticeTimerRef.current = undefined;
|
||||||
|
}, 3_000);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
if (noticeTimerRef.current) clearTimeout(noticeTimerRef.current);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let active = true;
|
let active = true;
|
||||||
@@ -175,7 +189,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
setDraftAdjustments(initial.background.adjustments);
|
setDraftAdjustments(initial.background.adjustments);
|
||||||
historyRef.current = new CanvasEditHistory(initial);
|
historyRef.current = new CanvasEditHistory(initial);
|
||||||
elementControllerRef.current = new CanvasElementController(initial);
|
elementControllerRef.current = new CanvasElementController(initial);
|
||||||
}).catch(() => { if (active) setNotice("编辑器暂时无法读取项目"); });
|
}).catch(() => { if (active) showNotice("编辑器暂时无法读取项目"); });
|
||||||
return () => { active = false; };
|
return () => { active = false; };
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
|
|
||||||
@@ -291,7 +305,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
function applyPreview() {
|
function applyPreview() {
|
||||||
if (!canvasState) return;
|
if (!canvasState) return;
|
||||||
commitCanvas(updateBackgroundAdjustments(canvasState, draftAdjustments));
|
commitCanvas(updateBackgroundAdjustments(canvasState, draftAdjustments));
|
||||||
setNotice("底图调整已提交");
|
showNotice("底图调整已提交");
|
||||||
}
|
}
|
||||||
|
|
||||||
function undo() {
|
function undo() {
|
||||||
@@ -334,9 +348,9 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
const palette = await paletteForAsset(pendingBackground);
|
const palette = await paletteForAsset(pendingBackground);
|
||||||
commitCanvas(switchBackground(canvasState, pendingBackground, palette));
|
commitCanvas(switchBackground(canvasState, pendingBackground, palette));
|
||||||
setPendingBackground(undefined);
|
setPendingBackground(undefined);
|
||||||
setNotice("已更换底图,覆盖元素保留,底图处理已重置");
|
showNotice("已更换底图,覆盖元素保留,底图处理已重置");
|
||||||
} catch {
|
} catch {
|
||||||
setNotice("新底图无法读取,未更换底图或刷新色卡");
|
showNotice("新底图无法读取,未更换底图或刷新色卡");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +367,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
commitCanvas(next);
|
commitCanvas(next);
|
||||||
setGuides([]);
|
setGuides([]);
|
||||||
setCandidateMenu(undefined);
|
setCandidateMenu(undefined);
|
||||||
setNotice(message);
|
showNotice(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addSticker(sticker: StaticStickerCatalogItem) {
|
function addSticker(sticker: StaticStickerCatalogItem) {
|
||||||
@@ -369,8 +383,8 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
}));
|
}));
|
||||||
commitElementOperation(controller, "贴纸已加入画布");
|
commitElementOperation(controller, "贴纸已加入画布");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。");
|
if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。");
|
||||||
else setNotice("贴纸未能加入画布");
|
else showNotice("贴纸未能加入画布");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,8 +397,8 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
controller.add(createColorCardElement(definition, palette, newElementIdentity(), canvasState.elements.length));
|
controller.add(createColorCardElement(definition, palette, newElementIdentity(), canvasState.elements.length));
|
||||||
commitElementOperation(controller, "色卡已按原始底图加入画布");
|
commitElementOperation(controller, "色卡已按原始底图加入画布");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。");
|
if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。");
|
||||||
else setNotice("无法从原始底图稳定提取五色,色卡未加入画布");
|
else showNotice("无法从原始底图稳定提取五色,色卡未加入画布");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -394,7 +408,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
if (templateId === "DYN012") {
|
if (templateId === "DYN012") {
|
||||||
const font = fontOption("FONT081");
|
const font = fontOption("FONT081");
|
||||||
if (!font || await ensureFont(font.fontId, font.url) !== "ready") {
|
if (!font || await ensureFont(font.fontId, font.url) !== "ready") {
|
||||||
setNotice("DYN012 的 FONT081 替代字体不可用,未使用系统字体替代。");
|
showNotice("DYN012 的 FONT081 替代字体不可用,未使用系统字体替代。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -407,8 +421,8 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
commitElementOperation(controller, "动态值已确认并加入画布");
|
commitElementOperation(controller, "动态值已确认并加入画布");
|
||||||
setLocationDialog(undefined);
|
setLocationDialog(undefined);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。");
|
if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。");
|
||||||
else setNotice("动态贴纸未能加入画布");
|
else showNotice("动态贴纸未能加入画布");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -456,7 +470,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
controller.replaceElement(overrideDynamicStickerValue(element, value));
|
controller.replaceElement(overrideDynamicStickerValue(element, value));
|
||||||
commitElementOperation(controller, "动态贴纸显示文字已更新");
|
commitElementOperation(controller, "动态贴纸显示文字已更新");
|
||||||
} catch {
|
} catch {
|
||||||
setNotice("动态贴纸显示文字不能为空");
|
showNotice("动态贴纸显示文字不能为空");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -480,7 +494,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
if (!template.fontUrl || !canvasState) return;
|
if (!template.fontUrl || !canvasState) return;
|
||||||
const status = await ensureFont(template.defaultFontId, template.fontUrl);
|
const status = await ensureFont(template.defaultFontId, template.fontUrl);
|
||||||
if (status !== "ready") {
|
if (status !== "ready") {
|
||||||
setNotice("素材暂不可用,未使用系统字体替代。");
|
showNotice("素材暂不可用,未使用系统字体替代。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const controller = controllerForCurrent();
|
const controller = controllerForCurrent();
|
||||||
@@ -490,8 +504,8 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
commitElementOperation(controller, "文字模板已加入画布");
|
commitElementOperation(controller, "文字模板已加入画布");
|
||||||
void recordRecentTextTemplate(template.templateId, template.resourceVersion);
|
void recordRecentTextTemplate(template.templateId, template.resourceVersion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。");
|
if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。");
|
||||||
else setNotice("文字模板未能加入画布");
|
else showNotice("文字模板未能加入画布");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -503,7 +517,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
action(edit);
|
action(edit);
|
||||||
return { ...current, draft: edit.value };
|
return { ...current, draft: edit.value };
|
||||||
} catch {
|
} catch {
|
||||||
setNotice("文字参数不在允许范围内");
|
showNotice("文字参数不在允许范围内");
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -514,7 +528,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
if (!template?.fontUrl) return;
|
if (!template?.fontUrl) return;
|
||||||
const status = await ensureFont(template.defaultFontId, template.fontUrl);
|
const status = await ensureFont(template.defaultFontId, template.fontUrl);
|
||||||
if (status !== "ready") {
|
if (status !== "ready") {
|
||||||
setNotice("素材暂不可用,未使用系统字体替代。");
|
showNotice("素材暂不可用,未使用系统字体替代。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateTextDraft((edit) => edit.switchTemplate(templateId));
|
updateTextDraft((edit) => edit.switchTemplate(templateId));
|
||||||
@@ -527,7 +541,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
}
|
}
|
||||||
const option = fontOption(fontId);
|
const option = fontOption(fontId);
|
||||||
if (!option || await ensureFont(option.fontId, option.url) !== "ready") {
|
if (!option || await ensureFont(option.fontId, option.url) !== "ready") {
|
||||||
setNotice("字体素材暂不可用,未使用系统字体替代。");
|
showNotice("字体素材暂不可用,未使用系统字体替代。");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateTextDraft((edit) => edit.setStyle({ fontOverride: fontId }));
|
updateTextDraft((edit) => edit.setStyle({ fontOverride: fontId }));
|
||||||
@@ -546,8 +560,8 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
void recordRecentTextTemplate(complete.template_or_asset_id, complete.resource_version);
|
void recordRecentTextTemplate(complete.template_or_asset_id, complete.resource_version);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "text_content_required") setNotice("请输入文字内容或删除该元素。");
|
if (error instanceof Error && error.message === "text_content_required") showNotice("请输入文字内容或删除该元素。");
|
||||||
else setNotice("文字编辑未能完成");
|
else showNotice("文字编辑未能完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -556,7 +570,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
const current = canvasState.elements.find((element) => element.element_id === textEdit.elementId);
|
const current = canvasState.elements.find((element) => element.element_id === textEdit.elementId);
|
||||||
if (current) setTextEdit({ draft: structuredClone(current), elementId: current.element_id, originalTemplateId: current.template_or_asset_id });
|
if (current) setTextEdit({ draft: structuredClone(current), elementId: current.element_id, originalTemplateId: current.template_or_asset_id });
|
||||||
}
|
}
|
||||||
setNotice("已取消未提交的文字修改");
|
showNotice("已取消未提交的文字修改");
|
||||||
}
|
}
|
||||||
|
|
||||||
function pendingTextDraft() {
|
function pendingTextDraft() {
|
||||||
@@ -624,7 +638,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const ran = await conflictExportGuardRef.current.run(() => executeExport(options));
|
const ran = await conflictExportGuardRef.current.run(() => executeExport(options));
|
||||||
if (!ran) setNotice("版本冲突时仅允许导出本页版本一次");
|
if (!ran) showNotice("版本冲突时仅允许导出本页版本一次");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function retryExportDownload() {
|
async function retryExportDownload() {
|
||||||
@@ -639,8 +653,8 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
action(controller);
|
action(controller);
|
||||||
commitElementOperation(controller, message);
|
commitElementOperation(controller, message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。");
|
if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。");
|
||||||
else setNotice("对象操作未完成");
|
else showNotice("对象操作未完成");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +682,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
opacityGestureRef.current = undefined;
|
opacityGestureRef.current = undefined;
|
||||||
if (gesture.last === gesture.base) return;
|
if (gesture.last === gesture.base) return;
|
||||||
commitCanvas(gesture.last);
|
commitCanvas(gesture.last);
|
||||||
setNotice("贴纸透明度已提交");
|
showNotice("贴纸透明度已提交");
|
||||||
}
|
}
|
||||||
|
|
||||||
function duplicateSelection() {
|
function duplicateSelection() {
|
||||||
@@ -688,7 +702,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
const controller = controllerForCurrent();
|
const controller = controllerForCurrent();
|
||||||
if (!controller || selectedIds.length === 0) return;
|
if (!controller || selectedIds.length === 0) return;
|
||||||
clipboardRef.current = controller.copySelected();
|
clipboardRef.current = controller.copySelected();
|
||||||
setNotice("已复制到画布剪贴板");
|
showNotice("已复制到画布剪贴板");
|
||||||
}
|
}
|
||||||
|
|
||||||
function pasteSelection() {
|
function pasteSelection() {
|
||||||
@@ -698,7 +712,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
controller.pasteElements(clipboardRef.current, () => newElementIdentity());
|
controller.pasteElements(clipboardRef.current, () => newElementIdentity());
|
||||||
commitElementOperation(controller, "已粘贴画布对象");
|
commitElementOperation(controller, "已粘贴画布对象");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error && error.message === "canvas_element_limit_reached") setNotice("画布最多 50 个元素,请先删除现有元素。");
|
if (error instanceof Error && error.message === "canvas_element_limit_reached") showNotice("画布最多 50 个元素,请先删除现有元素。");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,7 +742,7 @@ export function EditorPage({ projectId }: { projectId: string }) {
|
|||||||
const drag = dragRef.current;
|
const drag = dragRef.current;
|
||||||
if (!drag) return;
|
if (!drag) return;
|
||||||
commitCanvas(drag.last);
|
commitCanvas(drag.last);
|
||||||
setNotice("对象位置已提交");
|
showNotice("对象位置已提交");
|
||||||
setGuides([]);
|
setGuides([]);
|
||||||
dragRef.current = undefined;
|
dragRef.current = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,17 @@ import { drawColorCard } from "./palette-provider.js";
|
|||||||
|
|
||||||
interface Gesture {
|
interface Gesture {
|
||||||
append: boolean;
|
append: boolean;
|
||||||
|
bounds: DOMRect;
|
||||||
hit: boolean;
|
hit: boolean;
|
||||||
longPressOpened: boolean;
|
longPressOpened: boolean;
|
||||||
|
moved: boolean;
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
start: CanvasPoint;
|
start: CanvasPoint;
|
||||||
|
startClient: CanvasPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DRAG_THRESHOLD_PX = 4;
|
||||||
|
|
||||||
interface EditorStageProps {
|
interface EditorStageProps {
|
||||||
assetId: string | null;
|
assetId: string | null;
|
||||||
canvasState: CanvasState;
|
canvasState: CanvasState;
|
||||||
@@ -38,11 +43,10 @@ interface EditorStageProps {
|
|||||||
selectedIds: readonly string[];
|
selectedIds: readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointFromEvent(event: PointerEvent<HTMLCanvasElement>): CanvasPoint {
|
function pointFromClient(clientX: number, clientY: number, bounds: DOMRect): CanvasPoint {
|
||||||
const bounds = event.currentTarget.getBoundingClientRect();
|
|
||||||
return {
|
return {
|
||||||
x: Math.max(0, Math.min(1, (event.clientX - bounds.left) / bounds.width)),
|
x: Math.max(0, Math.min(1, (clientX - bounds.left) / bounds.width)),
|
||||||
y: Math.max(0, Math.min(1, (event.clientY - bounds.top) / bounds.height)),
|
y: Math.max(0, Math.min(1, (clientY - bounds.top) / bounds.height)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,10 +360,14 @@ export function EditorStage(props: EditorStageProps) {
|
|||||||
|
|
||||||
function handlePointerDown(event: PointerEvent<HTMLCanvasElement>) {
|
function handlePointerDown(event: PointerEvent<HTMLCanvasElement>) {
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
const start = pointFromEvent(event);
|
const bounds = event.currentTarget.getBoundingClientRect();
|
||||||
|
const start = pointFromClient(event.clientX, event.clientY, bounds);
|
||||||
const append = event.shiftKey;
|
const append = event.shiftKey;
|
||||||
const hit = props.onSelect(start, append);
|
const hit = props.onSelect(start, append);
|
||||||
gestureRef.current = { append, hit, longPressOpened: false, pointerId: event.pointerId, start };
|
gestureRef.current = {
|
||||||
|
append, bounds, hit, longPressOpened: false, moved: false, pointerId: event.pointerId, start,
|
||||||
|
startClient: { x: event.clientX, y: event.clientY },
|
||||||
|
};
|
||||||
event.currentTarget.setPointerCapture(event.pointerId);
|
event.currentTarget.setPointerCapture(event.pointerId);
|
||||||
longPressRef.current = setTimeout(() => {
|
longPressRef.current = setTimeout(() => {
|
||||||
const gesture = gestureRef.current;
|
const gesture = gestureRef.current;
|
||||||
@@ -375,9 +383,11 @@ export function EditorStage(props: EditorStageProps) {
|
|||||||
props.onPointerMoved();
|
props.onPointerMoved();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const point = pointFromEvent(event);
|
const clientDistance = Math.hypot(event.clientX - gesture.startClient.x, event.clientY - gesture.startClient.y);
|
||||||
|
if (!gesture.moved && clientDistance < DRAG_THRESHOLD_PX) return;
|
||||||
|
gesture.moved = true;
|
||||||
|
const point = pointFromClient(event.clientX, event.clientY, gesture.bounds);
|
||||||
const delta = { x: point.x - gesture.start.x, y: point.y - gesture.start.y };
|
const delta = { x: point.x - gesture.start.x, y: point.y - gesture.start.y };
|
||||||
if (Math.abs(delta.x) + Math.abs(delta.y) < 0.003) return;
|
|
||||||
if (longPressRef.current) clearTimeout(longPressRef.current);
|
if (longPressRef.current) clearTimeout(longPressRef.current);
|
||||||
props.onPointerMoved();
|
props.onPointerMoved();
|
||||||
if (gesture.hit && !gesture.longPressOpened) props.onMovePreview(delta);
|
if (gesture.hit && !gesture.longPressOpened) props.onMovePreview(delta);
|
||||||
@@ -388,11 +398,18 @@ export function EditorStage(props: EditorStageProps) {
|
|||||||
const gesture = gestureRef.current;
|
const gesture = gestureRef.current;
|
||||||
if (!gesture || gesture.pointerId !== event.pointerId) return;
|
if (!gesture || gesture.pointerId !== event.pointerId) return;
|
||||||
if (longPressRef.current) clearTimeout(longPressRef.current);
|
if (longPressRef.current) clearTimeout(longPressRef.current);
|
||||||
const point = pointFromEvent(event);
|
const clientDistance = Math.hypot(event.clientX - gesture.startClient.x, event.clientY - gesture.startClient.y);
|
||||||
const delta = { x: point.x - gesture.start.x, y: point.y - gesture.start.y };
|
const moved = gesture.moved || clientDistance >= DRAG_THRESHOLD_PX;
|
||||||
const moved = Math.abs(delta.x) + Math.abs(delta.y) >= 0.003;
|
if (moved && !gesture.moved && !gesture.longPressOpened) {
|
||||||
|
const point = pointFromClient(event.clientX, event.clientY, gesture.bounds);
|
||||||
|
const delta = { x: point.x - gesture.start.x, y: point.y - gesture.start.y };
|
||||||
|
if (gesture.hit) props.onMovePreview(delta);
|
||||||
|
}
|
||||||
if (gesture.hit && moved && !gesture.longPressOpened) props.onMoveCommit();
|
if (gesture.hit && moved && !gesture.longPressOpened) props.onMoveCommit();
|
||||||
else if (!gesture.hit && moved) props.onMarquee({ height: delta.y, width: delta.x, x: gesture.start.x, y: gesture.start.y }, gesture.append);
|
else if (!gesture.hit && moved) {
|
||||||
|
const point = pointFromClient(event.clientX, event.clientY, gesture.bounds);
|
||||||
|
props.onMarquee({ height: point.y - gesture.start.y, width: point.x - gesture.start.x, x: gesture.start.x, y: gesture.start.y }, gesture.append);
|
||||||
|
}
|
||||||
setMarquee(undefined);
|
setMarquee(undefined);
|
||||||
gestureRef.current = undefined;
|
gestureRef.current = undefined;
|
||||||
event.currentTarget.releasePointerCapture(event.pointerId);
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
||||||
|
|||||||
@@ -212,3 +212,81 @@ test("TDD-WP4-TXT-002 waits for the archived font and commits exact style ranges
|
|||||||
writeEvidence("TDD-WP4-TXT-002-font-metrics-ranges", "pixel-diff.json", { background_alpha_separate: true, clipped_visible_text: false, effective_font_size: 96 });
|
writeEvidence("TDD-WP4-TXT-002-font-metrics-ranges", "pixel-diff.json", { background_alpha_separate: true, clipped_visible_text: false, effective_font_size: 96 });
|
||||||
if (process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR, "TDD-WP4-TXT-002-font-metrics-ranges", "font-styles.png") });
|
if (process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR) await page.screenshot({ fullPage: true, path: resolve(process.env.DADA_EVIDENCE_DIR_TEXT_EDITOR, "TDD-WP4-TXT-002-font-metrics-ranges", "font-styles.png") });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("POSTV1-07 keeps the canvas anchored when the text template panel opens", async ({ page }) => {
|
||||||
|
const projectId = uuid(760);
|
||||||
|
const backend: Backend = { canvas: emptyCanvas(), recent: [], saves: 0, version: 5 };
|
||||||
|
await routeEditor(page, projectId, backend);
|
||||||
|
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
|
||||||
|
|
||||||
|
const stage = page.getByLabel("编辑画布");
|
||||||
|
const before = await stage.boundingBox();
|
||||||
|
if (!before) throw new Error("Canvas geometry is unavailable before opening text templates.");
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "文字模板", exact: true }).click();
|
||||||
|
await expect(page.locator(".editor-template-grid button")).toHaveCount(32);
|
||||||
|
const after = await stage.boundingBox();
|
||||||
|
if (!after) throw new Error("Canvas geometry is unavailable after opening text templates.");
|
||||||
|
const assetsPanelScroll = await page.getByLabel("素材与底图来源").evaluate((panel) => ({
|
||||||
|
clientHeight: panel.clientHeight,
|
||||||
|
overflowY: getComputedStyle(panel).overflowY,
|
||||||
|
scrollHeight: panel.scrollHeight,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(Math.abs(after.x - before.x)).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(after.y - before.y)).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(after.width - before.width)).toBeLessThanOrEqual(1);
|
||||||
|
expect(Math.abs(after.height - before.height)).toBeLessThanOrEqual(1);
|
||||||
|
expect(assetsPanelScroll.overflowY).toBe("auto");
|
||||||
|
expect(assetsPanelScroll.scrollHeight).toBeGreaterThan(assetsPanelScroll.clientHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("POSTV1-07 keeps text selection stable and dismisses move feedback", async ({ page }) => {
|
||||||
|
const projectId = uuid(770);
|
||||||
|
const backend: Backend = { canvas: emptyCanvas(), recent: [], saves: 0, version: 6 };
|
||||||
|
await routeEditor(page, projectId, backend);
|
||||||
|
await page.goto(`${webUrl}/app/projects/${projectId}/editor`);
|
||||||
|
await page.getByRole("button", { name: "文字模板", exact: true }).click();
|
||||||
|
await page.getByRole("button", { name: /H003 生活分享家/ }).click();
|
||||||
|
await expect.poll(() => backend.canvas.elements.length).toBe(1);
|
||||||
|
await expect.poll(() => backend.saves, { timeout: 5_000 }).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
const stage = page.getByLabel("编辑画布");
|
||||||
|
const bounds = await stage.boundingBox();
|
||||||
|
const element = backend.canvas.elements[0];
|
||||||
|
if (!bounds || !element) throw new Error("Text selection geometry is unavailable.");
|
||||||
|
const positionBefore = structuredClone(element.position);
|
||||||
|
const savesBefore = backend.saves;
|
||||||
|
const clientX = bounds.x + bounds.width * element.position.x;
|
||||||
|
const clientY = bounds.y + bounds.height * element.position.y;
|
||||||
|
|
||||||
|
await page.mouse.move(clientX, clientY);
|
||||||
|
await page.mouse.down();
|
||||||
|
await expect(page.getByLabel("字体覆盖")).toBeVisible();
|
||||||
|
const selectedBounds = await stage.boundingBox();
|
||||||
|
const inspectorScroll = await page.getByLabel("对象参数").evaluate((panel) => ({
|
||||||
|
clientHeight: panel.clientHeight,
|
||||||
|
overflowY: getComputedStyle(panel).overflowY,
|
||||||
|
scrollHeight: panel.scrollHeight,
|
||||||
|
}));
|
||||||
|
if (!selectedBounds) throw new Error("Canvas geometry is unavailable after selecting text.");
|
||||||
|
expect(Math.abs(selectedBounds.y - bounds.y)).toBeLessThanOrEqual(1);
|
||||||
|
expect(inspectorScroll.overflowY).toBe("auto");
|
||||||
|
expect(inspectorScroll.scrollHeight).toBeGreaterThan(inspectorScroll.clientHeight);
|
||||||
|
await page.mouse.move(clientX + 1, clientY);
|
||||||
|
await page.mouse.up();
|
||||||
|
|
||||||
|
await expect(page.getByLabel("字体覆盖")).toBeVisible();
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
expect(backend.canvas.elements[0]?.position).toEqual(positionBefore);
|
||||||
|
expect(backend.saves).toBe(savesBefore);
|
||||||
|
|
||||||
|
await page.mouse.move(clientX, clientY);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(clientX + 12, clientY);
|
||||||
|
await page.mouse.up();
|
||||||
|
await expect(page.getByText("对象位置已提交", { exact: true })).toBeVisible();
|
||||||
|
await expect.poll(() => backend.saves, { timeout: 5_000 }).toBeGreaterThan(savesBefore);
|
||||||
|
await expect(page.getByText("对象位置已提交", { exact: true })).toBeHidden({ timeout: 5_000 });
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user