From c485ce94a1d6d3f967ffab66a697dfeb9671f36b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Mon, 14 Sep 2026 18:30:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20UI=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E8=BF=94=E5=9B=9E=E5=90=8E=E8=B5=84=E6=BA=90=E7=94=BB?= =?UTF-8?q?=E5=B8=83=E5=B9=B3=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 资源管理卸载重挂时重新绑定画布 wheel 监听 补充 UI 编辑器返回后的资源画布平移回归用例 记录资源画布事件监听重挂排障经验 --- .../src/view/project-development/index.tsx | 7 +- .../appSurface/project-development.suite.ts | 130 ++++++++++++++++++ docs/project-memory/shared-memory/pitfalls.md | 8 ++ 3 files changed, 144 insertions(+), 1 deletion(-) diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 60842a4f6..062ea6e42 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -4633,7 +4633,12 @@ export default function ProjectDevelopmentView({ passive: false, }); return () => manager.removeEventListener('wheel', handleResourceBookWheel); - }, [handleResourceBookWheel, mode]); + }, [ + handleResourceBookWheel, + mode, + // UI 编辑器会卸载整个资源 manager;返回时 ref 指向新节点,必须重新绑定原生 wheel。 + uiEditorRoute, + ]); const handleResourceBookMainPointerDown = useCallback( (event: ReactPointerEvent) => { diff --git a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts index c4c771224..200b70bb7 100644 --- a/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts +++ b/apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts @@ -5916,6 +5916,136 @@ export function registerProjectWorkbenchFoundationTests() { ); }); + it('restores resource canvas panning after returning from the UI editor', async () => { + class TestResizeObserver { + constructor(_callback: ResizeObserverCallback) {} + observe() {} + disconnect() {} + } + vi.stubGlobal('ResizeObserver', TestResizeObserver); + const manifest = createGameCreationAppManifest( + 'workbench-ui-editor-return-pan', + 'UI 编辑器返回平移测试', + ); + manifest.assets = [ + { + id: 'ui-design-resource', + kind: 'UI', + mediaType: 'application/json', + localPath: 'assets/ui-design.json', + source: { kind: 'generated' }, + }, + ]; + const invoke = vi.fn( + async (command: string, args?: Record) => { + if (command === 'read_local_project_resource_graph') { + return resourceGraphForInputs(args); + } + if (command === 'read_local_project_resource_canvas_layout') { + return { + schemaVersion: 'game-creator-resource-layout.v1', + projectId: args?.expectedProjectId, + mode: args?.mode, + revision: 0, + positions: [], + updatedAt: 0, + }; + } + if (command === 'load_ui_design_state') { + return { + revision: 0, + state: { + ui_trees: [], + ui_design_images: {}, + sprite_assets: {}, + font_assets: {}, + }, + }; + } + throw new Error(`unexpected invoke ${command}`); + }, + ); + window.__TAURI__ = { core: { invoke } }; + render( + React.createElement(ProjectDevelopmentView, { + projectName: manifest.name, + projectPath: '/tmp/workbench-ui-editor-return-pan', + manifest, + attachments: [], + recentRunStatus: null, + recentRunStopReason: null, + supervisor: React.createElement('div', null, '项目总控'), + onHomeOpen: vi.fn(), + onProjectsOpen: vi.fn(), + }), + ); + + await openResourceBookCategory('UI 交互'); + const canvas = await screen.findByRole('region', { name: 'UI 交互' }); + const scene = document.querySelector( + '[data-resource-book-view="child"] .game-resource-book-scene', + ); + + const wheel = (target: HTMLElement, deltaY: number) => { + const event = new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaY, + clientX: 120, + clientY: 100, + }); + act(() => target.dispatchEvent(event)); + expect(event.defaultPrevented).toBe(true); + }; + wheel(scene ?? canvas, 120); + const world = canvas.querySelector('[data-resource-viewport]'); + expect(world?.getAttribute('data-resource-viewport')).toMatch( + /^(?!48,48,1)/u, + ); + + fireEvent.click( + screen.getByRole('button', { + name: '选中资源:UI 交互 ui-design.json', + }), + ); + fireEvent.click(await screen.findByRole('button', { name: 'UI 编辑器' })); + await screen.findByRole('button', { name: '返回资源' }); + fireEvent.click(screen.getByRole('button', { name: '返回资源' })); + await waitFor(() => + expect(screen.queryByRole('button', { name: '返回资源' })).toBeNull(), + ); + + const restoredCanvas = await screen.findByRole('region', { + name: 'UI 交互', + }); + const restoredWorld = restoredCanvas.querySelector( + '[data-resource-viewport]', + ); + const beforeReturn = world?.getAttribute('data-resource-viewport'); + const restoredBeforePan = restoredWorld?.getAttribute( + 'data-resource-viewport', + ); + expect(restoredBeforePan).toBe(beforeReturn); + + const restoredScene = document.querySelector( + '[data-resource-book-view="child"] .game-resource-book-scene', + ); + const restoredWheelEvent = new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaY: 120, + clientX: 120, + clientY: 100, + }); + act(() => + (restoredScene ?? restoredCanvas).dispatchEvent(restoredWheelEvent), + ); + expect(restoredWheelEvent.defaultPrevented).toBe(true); + expect(restoredWorld?.getAttribute('data-resource-viewport')).not.toBe( + restoredBeforePan, + ); + }); + it('clears the resource preview cache and cancels the old preview scope on project switch', async () => { // 60d8b8fbb 删掉了从未被写入的 `resourcePreviewVersionByResourceId` 死接线,原先钉 // 那条 prop 与它的清空语句的断言随之取消;用例真正要守的意图不变——项目切换必须把 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index c1632f879..339290853 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -1,5 +1,13 @@ # 踩坑与排障记录 +## 2026-09-14 UI 编辑器返回后资源画布滚轮平移失效 + +- **现象**:资源管理打开 UI 编辑器再返回后,资源画布滚轮平移/缩放不再响应;返回前同一手势正常。 +- **原因**:资源画布的非 passive `wheel` 监听绑定在 `resourceBookManagerRef` 当前 DOM 上,但 effect 只依赖 `handleResourceBookWheel` 与 `mode`。UI 编辑器切换会卸载旧 manager 并挂载新 manager,依赖不变导致新节点没有重新绑定监听。 +- **处理**:将 `uiEditorRoute` 纳入 wheel effect 依赖,使进入/退出 UI 编辑器时先清理旧节点监听,再给返回后的新 manager 绑定同一处理器。 +- **验证**:`npx vitest run apps/ai-game-creator-shell/tests/appSurface.test.ts -t "restores resource canvas panning"`;回归用例覆盖打开栏目、wheel 平移、进入 UI 编辑器、返回并再次 wheel 平移。 +- **关联**:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`。 + ## 2026-09-14 AGC 壳 Rust 套件按「一片一 job」拆分,且分片必须自校验覆盖 - **现象**:`AI game creator shell Rust tests` 一直是客户端 CI 的关键路径。run 2097 实测 15 分 27 秒,其中 `apps/ai-game-creator-shell/src-tauri` 的 bin target 单测(2466 条)一条 `cargo test -- --test-threads=1` 串行占 507 秒。