From 65ffd4cc69ebda53ffc7f3d33444ebe136a1c60d Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sat, 5 Sep 2026 09:25:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8F=96=E6=B6=88=E8=B5=84=E6=BA=90=E7=94=BB?= =?UTF-8?q?=E6=9C=AC=E8=BF=94=E5=9B=9E=E4=B8=BB=E7=94=BB=E5=B8=83=E7=9A=84?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E5=B9=B3=E7=A7=BB=E5=8A=A8=E7=94=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 returning-main 阶段,区分返回主画布与进入子画布的 transform 过渡。 返回期间取消残留分页事件和进行中的子画布切换,避免返回流程被重新改写。 补充状态机与 appSurface 回归测试,并同步技术方案文档。 --- apps/ai-game-creator-shell/src/styles.css | 12 ++-- .../src/view/project-development/index.tsx | 8 +++ .../project-development/resourceBookModel.ts | 17 +++--- .../appSurface/project-development.suite.ts | 25 +++++++++ .../tests/resourceBookModel.test.ts | 56 +++++++++++++++++++ ...】GameAgent资源自由画板与快速编辑-2026-08-20.md | 2 + 6 files changed, 106 insertions(+), 14 deletions(-) diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 73324f471..e13a7e671 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -5192,11 +5192,14 @@ iframe.preview-frame { transition: none; } -.game-resource-book-scene--entering .game-resource-book-scene-world, -.game-resource-book-scene--returning .game-resource-book-scene-world { +.game-resource-book-scene--entering .game-resource-book-scene-world { transition: transform 420ms cubic-bezier(0.2, 0.78, 0.2, 1); } +.game-resource-book-scene--returning-main .game-resource-book-scene-world { + transition: none; +} + .game-resource-book-scene--child { pointer-events: auto; } @@ -5471,11 +5474,6 @@ iframe.preview-frame { linear-gradient(145deg, #fffdfa 0%, #fff8f2 100%); } -.game-resource-book-manager--entering .game-resource-book-main, -.game-resource-book-manager--returning .game-resource-book-main { - transition: transform 420ms cubic-bezier(0.2, 0.78, 0.2, 1); -} - .game-resource-book-main-heading { display: flex; align-items: flex-end; 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 4f0e6e1ce..448ffeb79 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 @@ -2640,6 +2640,13 @@ export default function ProjectDevelopmentView({ return; } if (resourceBookState.phase === 'returning') { + if (!resourceBookState.pendingCategory) { + // A return-to-main transition has no destination category. Ignore + // any wheel step that was already queued before the collapse click; + // otherwise it can resurrect a child-enter transition and append a + // second viewport movement after the return leg. + return; + } // A wheel gesture can arrive while a click-driven book transition is // still in flight. Retarget the transition through the same tokenized // path so the old timer cannot restore a stale category. @@ -2661,6 +2668,7 @@ export default function ProjectDevelopmentView({ cancelResourceCanvasPan, cancelResourceCardDrag, openResourceBookChild, + resourceBookState.pendingCategory, resourceBookState.phase, resourcePageCategories, ], diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceBookModel.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceBookModel.ts index 12ceb3956..fc73658d7 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceBookModel.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceBookModel.ts @@ -1,7 +1,11 @@ import type { ProjectResourceCategory } from './resourceProjectionModel'; export type ResourceBookCategory = ProjectResourceCategory; -export type ResourceBookPhase = 'idle' | 'entering' | 'returning'; +export type ResourceBookPhase = + | 'idle' + | 'entering' + | 'returning' + | 'returning-main'; export type ResourceBookState = { view: 'main' | 'child'; @@ -74,7 +78,7 @@ export function resourceBookReducer( case 'finish-transition': return state.token === action.token ? { ...state, phase: 'idle' } : state; case 'return-to-main': - if (state.view !== 'child' || state.phase === 'returning') { + if (state.view !== 'child') { return state; } return { @@ -97,11 +101,10 @@ export function resourceBookReducer( view: 'main', category: null, pendingCategory: null, - // Keep the return leg in the animated phase while the scene - // viewport transitions from the child viewport back to the - // main viewport. The controller dispatches finish-transition - // after the same animation window used by child entry. - phase: 'entering', + // Returning to the main canvas is already completed by the + // shared-card shrink leg. Keep a distinct phase so the main + // viewport does not receive a second transform transition. + phase: 'returning-main', } : state; case 'reset': 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 72ca57e96..b6891f24b 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 @@ -786,6 +786,31 @@ export function registerProjectWorkbenchFoundationTests() { { name: '收起资源' }, ), ); + fireEvent.click( + within( + document.querySelector( + '.game-resource-book-scene-titlebar.is-active', + ) as HTMLElement, + ).getByRole('button', { name: '收起资源' }), + ); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 340)); + }); + expect( + screen + .getByRole('region', { name: '资源主画布' }) + .closest('[data-resource-book-view]') + ?.getAttribute('data-resource-book-transition'), + ).toBe('returning-main'); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 540)); + }); + expect( + screen + .getByRole('region', { name: '资源主画布' }) + .closest('[data-resource-book-view]') + ?.getAttribute('data-resource-book-transition'), + ).toBe('idle'); await openResourceBookCategory('设计文档'); dispatchPageWheel(dependencyCanvas); diff --git a/apps/ai-game-creator-shell/tests/resourceBookModel.test.ts b/apps/ai-game-creator-shell/tests/resourceBookModel.test.ts index 8aa5bafe6..e25ea6ee2 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookModel.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookModel.test.ts @@ -73,6 +73,62 @@ describe('resource book state machine', () => { expect(resourceBookSceneCategory(overview)).toBe('art'); }); + it('marks a return to the main canvas separately from a child enter transition', () => { + const entering = resourceBookReducer(initialResourceBookState, { + type: 'open-category', + category: 'document', + token: 1, + }); + const child = resourceBookReducer(entering, { + type: 'commit-enter', + token: 1, + }); + const returning = resourceBookReducer(child, { + type: 'return-to-main', + token: 2, + }); + const main = resourceBookReducer(returning, { + type: 'commit-return', + token: 2, + }); + + expect(main).toMatchObject({ + view: 'main', + category: null, + pendingCategory: null, + phase: 'returning-main', + }); + }); + + it('lets an explicit collapse cancel an in-flight child switch', () => { + const entering = resourceBookReducer(initialResourceBookState, { + type: 'open-category', + category: 'document', + token: 1, + }); + const child = resourceBookReducer(entering, { + type: 'commit-enter', + token: 1, + }); + const switching = resourceBookReducer(child, { + type: 'open-category', + category: 'art', + token: 2, + }); + const main = resourceBookReducer(switching, { + type: 'return-to-main', + token: 3, + }); + + expect(main).toMatchObject({ + view: 'child', + category: 'document', + pendingCategory: null, + phase: 'returning', + token: 3, + }); + }); + it('ignores stale transition callbacks', () => { const entering = resourceBookReducer(initialResourceBookState, { type: 'open-category', diff --git a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md index 856ed0775..4aea7c435 100644 --- a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md +++ b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md @@ -28,6 +28,8 @@ 子画布改为固定视口加可无限平移的 world 层:视口本身保持 `overflow: hidden`,平移和缩放只作用于 `.game-resource-book-scene-world`,不再让内容容器的边界限制拖动范围。首次 fit(包括隐藏分页画布提前完成的 fit)统一保证比例不低于 `1:1`,避免子画布展开后卡片缩成不可读的小块;用户后续主动缩放或平移的视口仍按每个分类独立保留。新增 viewport 回归测试覆盖初始可读比例和超出内容边界双向平移。 +返回主画布时使用独立的 `returning-main` 阶段:卡片先完成子画布到主画布预览位的缩回,随后主画布直接进入稳定布局,不再重复播放 world/main viewport 的平移动画;子画布之间切换仍使用原有的返回后进入动画。 + ## 2026-09-04 资源画本 UI 阶段补充 资源工作台当前产品方向调整为“主画布 + 子画布”的统一画本模型。主画布展示各子画布的缩略卡片,不直接展开真实资源内容;缩略卡片内部按资源类型显示有限层叠卡片,最多展示三层,更多资源以虚化卡片提示。进入子画布时使用共享元素转场:标题栏和资源卡片沿用同一组 DOM 元素,从缩略卡片的原始位置移动、展开到子画布中的目标位置,不允许先播放独立放大动画再硬切另一套内容;主画布预览和子画布展开态必须复用同一套卡片视觉与标题栏结构。进入子画布时在标题栏提供缩回主画布的缩放按钮;从一个子画布切换到另一个子画布时必须先完成缩回主画布,再进入目标子画布。