From 0914703032f64fdf57f318a5bc051c0e68117349 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Wed, 9 Sep 2026 16:58:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=B5=84=E6=BA=90=E7=94=BB?= =?UTF-8?q?=E6=9C=AC=E9=A3=9E=E8=A1=8C=E6=9C=9F=E9=97=B4=E9=81=AE=E6=8C=A1?= =?UTF-8?q?=E6=B2=BF=E7=94=A8=E9=94=99=E8=AF=AF=E4=B8=80=E4=BE=A7=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 卡片真正重叠的是堆叠那一侧:进入时重叠在总览摞(来源),返回时重叠在摞(目标)。原先统一按目标顺序排,进入时就用错了顺序,堆叠里下面两张飞到上面,结束才回到下方 play 增加 stacking 参数:进入用来源顺序,返回用目标顺序;不在该侧的卡片压到最底层 补充 resourceBookController 用例覆盖来源顺序 --- .../src/view/project-development/index.tsx | 2 + .../resourceBookController.ts | 34 +++++++++++----- .../tests/resourceBookController.test.ts | 40 +++++++++++++++++++ 3 files changed, 66 insertions(+), 10 deletions(-) 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 c470d3fba..493959669 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 @@ -2550,6 +2550,8 @@ export default function ProjectDevelopmentView({ token: resourceBookState.token, }); }, + // 进入时重叠在总览摞上,沿用来源顺序;返回时重叠在摞上,沿用目标顺序。 + resourceBookState.phase === 'returning-main' ? 'target' : 'source', ); }, [resourceBookState.token, resourceBookState.phase, mode]); diff --git a/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts b/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts index 13acf00ee..9af169220 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts +++ b/apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts @@ -271,7 +271,12 @@ export function createResourceBookTransitionController() { invalidate, settle, isCurrent: (expected: number) => token === expected, - play(manager: HTMLElement, expected: number, done: () => void) { + play( + manager: HTMLElement, + expected: number, + done: () => void, + stacking: 'source' | 'target' = 'target', + ) { if (token !== expected) return; root = manager; completion = done; @@ -292,19 +297,28 @@ export function createResourceBookTransitionController() { settle(); return; } - // 两个场景的 z-index 数值本身不同(总览 1–3、子画布 25),只搬数值无法保证 - // 顺序一致。按目标场景的最终绘制顺序(z-index 升序,同值再按 DOM 顺序) - // 给克隆分配单调层级,动画期间的遮挡关系就与结束后的静止状态完全一致; - // 只存在于来源里的淡出项统一压到最底层,不会盖住存活卡片。 - const ordered = [ - ...Array.from(target.values()).sort( - (left, right) => - left.zIndex - right.zIndex || left.order - right.order, - ), + // 两个场景的 z-index 数值域不同(总览 1–3、子画布 25),只搬数值保证不了顺序。 + // 卡片真正重叠的是"堆叠"那一侧:进入时重叠在总览摞(来源),返回时重叠在摞 + // (目标)。按该侧 z-index 升序、同值再按 DOM 顺序给克隆分配单调层级,飞行期间 + // 的遮挡就和用户眼前那一摞一致;不在该侧的卡片压到最底层。 + const reference = stacking === 'source' ? source : target; + const entries = [ + ...Array.from(target.values()), ...Array.from(source.values()).filter( (entry) => !target.has(entry.key), ), ]; + const primary = entries.filter((entry) => reference.has(entry.key)); + const secondary = entries.filter((entry) => !reference.has(entry.key)); + primary.sort((left, right) => { + const leftReference = reference.get(left.key)!; + const rightReference = reference.get(right.key)!; + return ( + leftReference.zIndex - rightReference.zIndex || + leftReference.order - rightReference.order + ); + }); + const ordered = [...primary, ...secondary]; try { for (const [stackOrder, item] of ordered.entries()) { const from = source.get(item.key); diff --git a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts index 05801ec23..933787c9f 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts @@ -224,6 +224,46 @@ describe('resource book FLIP controller', () => { controller.dispose(); }); + it('keeps the source stack order while flying out of the overview', () => { + Object.defineProperty(Element.prototype, 'animate', { + configurable: true, + writable: true, + value: () => ({ + finished: Promise.resolve({} as Animation), + cancel: () => undefined, + }), + }); + const root = document.createElement('div'); + root.innerHTML = `
+
+
+
`; + document.body.append(root); + vi.spyOn(root, 'getBoundingClientRect').mockReturnValue(rect(0, 0, 800, 600)); + for (const element of root.querySelectorAll( + '.game-resource-card', + )) { + vi.spyOn(element, 'getBoundingClientRect').mockReturnValue(rect()); + } + + const controller = createResourceBookTransitionController(); + const token = controller.begin(root); + const cardWrapper = root + .querySelector('[data-resource-card-id="a"]')! + .closest('.game-resource-book-scene-card')!; + cardWrapper.parentElement!.append(cardWrapper); + controller.play(root, token, () => undefined, 'source'); + + const wrappers = Array.from( + root.querySelectorAll('[data-motion-snapshot]'), + ); + expect(wrappers.map((element) => element.dataset.motionSnapshot)).toEqual([ + 'card:a', + 'card:b', + ]); + controller.dispose(); + }); + it('retargets from the current overlay geometry and ignores old completion', async () => { const { root, measure, flights } = setup(); const controller = createResourceBookTransitionController();