From 398ee643883685336d184077f21f1f568a367482 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 7 Sep 2026 10:38:36 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=BB=E5=B8=83=E5=86=85?= =?UTF-8?q?=E5=B1=82=E7=A9=BA=E7=99=BD=E5=8C=BA=E5=9F=9F=E6=A1=86=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 框选命中判断支持 world 的真实后代元素 回归测试覆盖共享 CanvasWorld 内层 pointerdown 冒泡 同步记录共享画布框选命中排障经验 --- docs/project-memory/shared-memory/pitfalls.md | 8 +++++ .../src/useImageCanvasStageInteractions.ts | 5 +-- .../useImageCanvasStageInteractions.test.tsx | 35 +++++++++++++++---- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 722ae47c9..575e20a4d 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -4983,3 +4983,11 @@ - 原因:Tauri Windows bundler 执行自己的 `\NSIS\makensis.exe`,默认位于当前用户 `%LOCALAPPDATA%\tauri`,不使用 PATH 中预装的 `makensis.exe`;Jenkins LocalSystem/systemprofile 的 AppData 可能无法启动该缓存程序。 - 处理:Windows 专用 Tauri 配置设置 `bundle.useLocalToolsDir: true`,把工具缓存到 `src-tauri/target/.tauri/NSIS`;Jenkins 预检验证实际用户、项目工具目录可写,并在构建失败时打印实际缓存路径和绝对路径执行结果。 - 验证:不要把 PATH 中 `makensis` 可发现当作 Tauri bundler 工具可执行的充分证据;需要在 Windows Agent 上检查 `target/.tauri/NSIS/makensis.exe`、ACL、EDR/Defender 和直接 `-VERSION` 结果。 + +## 共享画布框选需要识别 world 的真实后代命中 + +- 现象:共享 `CanvasWorld` 在 world 外层增加 `.genarrative-image-canvas__world-content` 后,点击或拖拽空白画布时事件 `target` 是内层 div;框选逻辑若只检查外层 world 本身,就会只清除焦点而不创建选框。 +- 原因:viewport 上的 pointer 事件通过冒泡接收,`event.currentTarget` 是 viewport,空白区域的 `event.target` 可能是 world 的任意后代,不保证命中外层元素。 +- 处理:框选命中判断使用 `Element.closest('.genarrative-image-canvas__world')`,并保留 viewport 自身命中路径;回归测试通过真实 `CanvasWorld` DOM 的 `pointerdown` 冒泡覆盖内层 world content。 +- 验证:`src/components/image-editor/useImageCanvasStageInteractions.test.tsx` 覆盖内层 world content 命中,定向交互测试通过。 +- 关联:`packages/image-canvas-react/src/useImageCanvasStageInteractions.ts`、`packages/image-canvas-react/src/CanvasWorld.tsx`。 diff --git a/packages/image-canvas-react/src/useImageCanvasStageInteractions.ts b/packages/image-canvas-react/src/useImageCanvasStageInteractions.ts index d5b3411cc..ce106893e 100644 --- a/packages/image-canvas-react/src/useImageCanvasStageInteractions.ts +++ b/packages/image-canvas-react/src/useImageCanvasStageInteractions.ts @@ -342,11 +342,12 @@ export function useImageCanvasStageInteractions({ event.preventDefault(); return; } - const target = event.target as HTMLElement; + const target = event.target; if ( effectiveTool === 'select' && (event.target === event.currentTarget || - target.classList.contains('genarrative-image-canvas__world')) + (target instanceof Element && + target.closest('.genarrative-image-canvas__world') !== null)) ) { event.preventDefault(); const rect = canvasViewportRef.current?.getBoundingClientRect(); diff --git a/src/components/image-editor/useImageCanvasStageInteractions.test.tsx b/src/components/image-editor/useImageCanvasStageInteractions.test.tsx index d50e63623..fc510d559 100644 --- a/src/components/image-editor/useImageCanvasStageInteractions.test.tsx +++ b/src/components/image-editor/useImageCanvasStageInteractions.test.tsx @@ -1,5 +1,6 @@ /* @vitest-environment jsdom */ +import { CanvasWorld } from '@genarrative/image-canvas-react'; import { act, fireEvent, render, screen } from '@testing-library/react'; import { type PointerEvent as ReactPointerEvent, @@ -150,7 +151,6 @@ function StageInteractionsHarness({ ) => void; }) { const canvasViewportRef = useRef(null); - const worldRef = useRef(null); const [activeTool, setActiveTool] = useState<'select' | 'hand'>('select'); const [layers, setLayers] = useState(() => [ createLayer({ id: 'first', x: 40, y: 40, zIndex: 1 }), @@ -288,11 +288,9 @@ function StageInteractionsHarness({ onPointerMove={interaction.handlePointerMove} onPointerUp={interaction.finishDrag} > -
+ + {layers.map((layer) => (