diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/previewRightPanGesture.ts b/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/previewRightPanGesture.ts index e8875f38f..839a0ce78 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/previewRightPanGesture.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/previewRightPanGesture.ts @@ -79,6 +79,7 @@ export function resolvePreviewRightPanRelease({ * 平台差异:Windows / Linux 的 Chromium 与 Firefox 在**按下**时触发 contextmenu, * macOS 在指针抬起之后触发;而原生菜单一旦弹出,页面之后收不到 pointermove / pointerup, * 所以必须按下时就拦截,并把拦截窗口保留到下一次指针按下之前。 + * 键盘菜单键不在此列:Chromium 实测上报 `button: -1`,只认 2 正好把它留给原有菜单路径。 */ export function shouldInterceptRightContextMenu({ button, diff --git a/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx b/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx index eed1c1425..7b5cb0c6d 100644 --- a/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx +++ b/apps/ai-game-creator-shell/tests/previewRightPanDrag.test.tsx @@ -350,6 +350,21 @@ describe('PreviewWorkspace right-button pan', () => { expect(screen.getByRole('menu')).toBeTruthy(); }); + it('leaves the keyboard menu key to the existing node menu path', () => { + const { canvas, child } = renderPreview(); + + dispatchPointer(child, 'pointerdown', { + button: 2, + clientX: 100, + clientY: 100, + pointerId: 13, + }); + // 键盘菜单键在 Chromium 上报 button: -1,不能被右键手势的拦截窗口吃掉。 + expect(fireEvent.contextMenu(child, { button: -1 })).toBe(false); + expect(canvas.selectNode).toHaveBeenCalledWith('child'); + expect(screen.getByRole('menu')).toBeTruthy(); + }); + it('keeps middle-button panning and its shared grabbing cursor', () => { const { container, preview } = renderPreview(); const before = worldTranslate(container); diff --git a/apps/ai-game-creator-shell/tests/previewRightPanGesture.test.ts b/apps/ai-game-creator-shell/tests/previewRightPanGesture.test.ts index 525e429be..866d706f1 100644 --- a/apps/ai-game-creator-shell/tests/previewRightPanGesture.test.ts +++ b/apps/ai-game-creator-shell/tests/previewRightPanGesture.test.ts @@ -96,5 +96,11 @@ describe('preview right-button pan gesture', () => { withinRightPressSequence: true, }), ).toBe(false); + expect( + shouldInterceptRightContextMenu({ + button: -1, + withinRightPressSequence: true, + }), + ).toBe(false); }); }); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index cd04aecea..7d495a9ac 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2,11 +2,11 @@ ## 2026-09-22 UI 编辑器预览画布补上右键拖拽平移,节点菜单改为右键抬起弹出 -- 背景:预览画布此前只有中键与空格+左键平移,右键整段留给节点操作菜单(`UiTreeRenderer.onContextMenu` 直接弹 `UiNodeContextMenu`)。这次要补右键拖拽平移,并要求"拖拽过就不许再触发右键菜单"。实测(Linux Chromium 151 / Firefox 151,真实 X11 输入)确认 `contextmenu` 在**按下**瞬间触发,且原生菜单一旦弹出,页面之后收不到任何 `pointermove` / `pointerup` / `mouseup` / `auxclick`,所以"先让菜单弹、拖拽时再关"在浏览器层面不可行;headless 没有原生菜单,Playwright 复现不出该行为。macOS 的 `contextmenu` 在 mouseup 触发(本容器无法实测),但同一条实现路径对两种时序都成立。 +- 背景:预览画布此前只有中键与空格+左键平移,右键整段留给节点操作菜单(`UiTreeRenderer.onContextMenu` 直接弹 `UiNodeContextMenu`)。这次要补右键拖拽平移,并要求"拖拽过就不许再触发右键菜单"。实测(Linux Chromium 151 / Firefox 151,真实 X11 输入)确认 `contextmenu` 在**按下**瞬间触发,且原生菜单一旦弹出,页面之后收不到任何 `pointermove` / `pointerup` / `mouseup` / `auxclick`,所以"先让菜单弹、拖拽时再关"在浏览器层面不可行;headless 没有原生菜单,Playwright 复现不出该行为。macOS 的 `contextmenu` 在 mouseup 触发(本容器无法实测),但同一条实现路径对两种时序都成立。同一次实测:键盘菜单键触发的是 `button: -1`,所以"只认按钮 2"的拦截天然把键盘菜单留给原有节点菜单路径。 - 决策:预览视口在捕获阶段拦截按钮 2 的 `contextmenu`(`preventDefault` + `stopPropagation`),右键手势改由预览自己裁决:按下时记录起点、`setPointerCapture` 并交焦点;移动越过与左键拖拽共用的 `DRAG_THRESHOLD_SCREEN_PX`(2px)后本次手势定死为平移,按"按下点全量 delta"更新视口(光标复用共享 `CanvasViewport` 的 `isPanning` → `cursor: grabbing`,三个平移绑定一起生效);未越阈值且在预览内抬起时,用 `[data-node-id]` 加树容器 `data-tree-id` 命中节点并打开 `UiNodeContextMenu`,保留"右键即选中该节点"的既有语义;空白处干净右键不做事(原生菜单已被抑制)。中键、空格+左键平移不变,macOS ctrl+左键与键盘菜单键继续走原有即时菜单路径;平移是纯视图操作,不写 State、不进历史、不受 `isLocked` 与空格按住态限制。 - 原因:右键同时承载菜单与视图平移,只能等到手势结束再裁决;把判定放进预览,是因为树、节点命中、预览边界与拖动阈值都在预览手里,而 `useNodeTransformInteraction` 应保持左键变换的单一职责;`[data-node-id]` 命中也已是 `resolveHitNodeId` 的既有模式。 - 代价与取舍:右键从"按下即弹菜单"变成"抬起才弹"(与 Windows 自身右键菜单一致);预览内空白处的浏览器原生菜单被永久抑制;右键平移与节点菜单互斥(越过阈值后抬起不再弹菜单);`grabbing` 光标在悬停到节点上时仍会被节点自身的 `cursor-move` 覆盖,与共享画布现状一致。本次只改 UI 编辑器预览:AGC 美术画布仍只有空格/中键平移,资源画布保留自己的右键平移实现,都不动,也不抽 `packages/shared`。 -- 验证方式:新增 `previewRightPanGesture` 纯状态机单测(阈值跨越、起点全量 delta、拖拽吞菜单 / 干净抬起开菜单、取消与失焦清理),并在 `previewWorkspaceZoom.test.tsx`、`uiEditorPreview.test.tsx` 补右键回归;运行 `npx vitest run`(定向文件)、`apps/ai-game-creator-shell` `npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding`、`git diff --check`。 +- 验证方式:新增 `previewRightPanGesture` 纯状态机单测(阈值跨越、起点全量 delta、拖拽吞菜单 / 干净抬起开菜单、取消与失焦清理),并在 `previewRightPanDrag.test.tsx`、`previewRightPanGesture.test.ts` 补右键回归(含键盘菜单键 `button: -1` 不被拦截的用例);运行 `npx vitest run`(定向文件)、`apps/ai-game-creator-shell` `npm run typecheck`、`npm run lint:eslint`、`npm run check:encoding`、`git diff --check`。 ## 2026-09-21 合并 origin/master:Supervisor 永久退役,策划 V1V2 退役落到当前两条产品路径