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 edfedcc9c..41a04cc08 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 @@ -285,7 +285,12 @@ export function createResourceBookTransitionController() { return; } const target = capture(manager); - const keys = new Set([...source.keys(), ...target.keys()]); + // 先按目标场景的 DOM 顺序排,再补上只存在于来源里的 key。层级相同的卡片 + // 由 DOM 顺序决定遮挡关系,顺序不一致会让动画结束的瞬间发生跳变。 + const keys = [ + ...target.keys(), + ...Array.from(source.keys()).filter((key) => !target.has(key)), + ]; if (!target.size) { settle(); return; diff --git a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts index 4d238116f..e041fbf50 100644 --- a/apps/ai-game-creator-shell/tests/resourceBookController.test.ts +++ b/apps/ai-game-creator-shell/tests/resourceBookController.test.ts @@ -178,6 +178,46 @@ describe('resource book FLIP controller', () => { controller.dispose(); }); + it('appends clones in the target DOM order so equal z-index cards keep their occlusion', () => { + 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); + + const wrappers = Array.from( + root.querySelectorAll('[data-motion-snapshot]'), + ); + expect(wrappers.map((element) => element.dataset.motionSnapshot)).toEqual([ + 'card:b', + 'card:a', + ]); + controller.dispose(); + }); + it('retargets from the current overlay geometry and ignores old completion', async () => { const { root, measure, flights } = setup(); const controller = createResourceBookTransitionController();