(),
+ positions: mixedPositions,
+ cardSizes: mixedCardSizes,
+ visibleResourceIds: new Set(mixed.map((item) => item.id)),
+ state: {
+ view: 'child',
+ category: RESOURCE_BOOK_ALL_TARGET,
+ phase: 'idle',
+ token: 1,
+ },
+ allLayout,
+ });
+
+ const cards = plan.find((group) => group.category === 'unclassified')!.cards;
+ expect(cards.map((card) => card.resource.id)).toEqual(
+ mixed.map((item) => item.id),
+ );
+ expect(cards.map((card) => card.stackColumn)).toEqual([0, 0, 1]);
+ });
+
it('fades the leaving all-resources cards from their band positions', () => {
const allLayout = buildResourceBookAllLayout({
categoryOrder: ['unclassified'],
diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md
index 60bec3e9d..5e6302247 100644
--- a/docs/project-memory/shared-memory/pitfalls.md
+++ b/docs/project-memory/shared-memory/pitfalls.md
@@ -5413,3 +5413,13 @@
- **验证**:appSurface 钉住新不变量——钉住的标题栏内联 `transform` 必须为空、`closest('.game-resource-book-scene-world')` 必须为 null、父节点是场景根,同时卡片必须仍在 world 内;`resourceBookController.test.ts` 新增「屏幕坐标系的宿主不被 world 缩放除」(worldScale=2 时 `translate(40px, 30px)` 而不是 `(20px, 15px)`)。变异验证三处:把标题栏搬回 world → `expected to be null`;恢复抵消 transform → `expected 'translate(-160px, 158px) scale(1)' to be ''`;把 per-node 缩放退回 `world.scale` → `expected 'translate(20px, 15px) …' to be 'translate(40px, 30px) …'`,三处都如期变红。定向:`resourceBook*` 76 passed、appSurface 413 passed、`npm run typecheck` exit 0、`npm run check:encoding` 4409 files passed、`git diff --check` 干净。**像素级效果本机没有复现出差异**,因此这次修复是"把清晰度从浏览器光栅决策里拿回来"的结构修复,真机判据见下一条。
- **真机判据**:那一行的三段文字(栏目名 / 计数 / `资源总览`)与小标题等锐度;Ctrl+滚轮把子画布缩放到 110% / 90% 时,钉住的那一行不再随缩放变化锐度,也不随缩小而变糊。
- **关联**:`apps/ai-game-creator-shell/src/view/project-development/index.tsx`(`ResourceBookScene.renderTitlebar`)、`apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts`(`nodeWorldTransform`)、`apps/ai-game-creator-shell/tests/appSurface/project-development.suite.ts`、`apps/ai-game-creator-shell/tests/resourceBookController.test.ts`、`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md`。
+
+## 2026-09-12 进栏目只有几张卡片有位移:总览每摞只铺 3 张,其余卡片没有 First 帧
+
+- **现象**:资源总览点进任一栏目(或展开「所有资源」)时,只有总览里已经画出来的那几张卡有"从那一摞飞出来"的位移动画,其余卡片是原地出现;用户原话「当前只有卡片里的那几张图片会有位移动画,而其他的图片是直接出现的,希望是全都从那堆卡片里出现」。
+- **原因(已用代码核实,不是推断)**:总览每摞只铺 `RESOURCE_BOOK_OVERVIEW_STACK_LIMIT`(3)张卡(`resourceBookLayout.ts:45` 的 `overviewStackRotations.length`;`selectResourceBookOverviewCards`,`resourceBookLayout.ts:217`),其余数量只写在栏目卡标题栏的"N 项"里;子画布/展开态则铺满该栏目全部卡片(`resourceBookLayout.ts:452`、`:507` 的 `childLayout` 分支)。转场层按 DOM 收集节点、只给"提交前可见"的节点记 First(`resourceBookController.ts:633` 的 `begin`),所以超出 3 张的卡片在总览侧**根本没有节点**、拿不到 First;`play` 把它们当"新挂载节点"补一次原地淡入(`:553` 的 `mountFade`,`startOpacity = 0`,`transform` 首末都是 `none`)⇒ 观感就是"直接出现"。**先排查了基线顶端那笔"钉住的栏目标题栏移出画布缩放层"(4c180ac29)**:`git diff 46458a825 4c180ac29 -- index.tsx` 只动了 `renderTitlebar`(标题栏改挂场景根)与 `nodeWorldTransform`,卡片宿主仍留在 `.game-resource-book-scene-world` 内、节点身份与 `begin` 的捕获口径逐字未变 ⇒ 它不是本次现象的原因(它只影响标题栏自己的坐标系换算)。
+- **处理**:把"那一摞"变成一类 First 来源,不新增第二套动画系统、也不为动画多渲染节点——渲染层把摞身份写到卡片宿主上(`data-resource-book-stack-column` / `data-resource-book-stack-index`,`index.tsx:1302` 一带);`begin` 顺带按 `(栏目, 列号)` 记下**同一摞最深那张卡**的 First 矩形当"堆锚点"(`resourceBookController.ts:649`);`play` 时没有 First 帧的卡片认领自己那一摞的锚点当合成 First,`startOpacity` 取目标不透明度(不做淡入),终点仍是 identity ⇒ 仍是同一套 FLIP。锚点只在**本次转场**有效(`maybeFinish` / `settle` / `invalidate` 清掉),转场中重基(已在跑动画)不认领锚点(起点必须是"此刻像素")。`buildResourceBookScenePlan` 的 `allOpen` 分支补上真实类型摞列号(展开态此前一律 0,第 2 摞之后的卡片会全从第 1 摞飞出来)。
+- **验证**:`resourceBookController.test.ts` 新增 3 条——「进栏目时每一张卡都从**自己那一摞**位移出来」(7 张卡全部有位移、第 0/1 摞分别认领各自锚点、总览已渲染的仍用自己 First、合成起点的 opacity 首末为 1)、「转场中重基从此刻像素而不是被拉回摞上」、「转场收尾后挂载的卡片不从过期锚点飞出」;`resourceBookLayout.test.ts` 新增「展开态每张卡带自己那一摞的列号」(同栏目两摞 ⇒ `[0,0,1]`);appSurface 在既有总览用例里钉住宿主属性(总览 3 张 = 下标 `0,1,2`,进栏目后 5 张 = 下标 `0..4`、同类型列号 `0`)。变异验证 3 处,均如期变红:去掉合成 First(`readStackAnchor` 返回 null)⇒「art-4: expected 'none' not to be 'none'」+ 重基用例同时红;不在 `maybeFinish` 清锚点 ⇒「expected [ Array(1) ] to have a length of +0 but got 1」;`allOpen` 的列号写回 0 ⇒「expected [0,0,0] to deeply equal [0,0,1]」。定向:`resourceBookController.test.ts` 27 passed、`resourceBookLayout.test.ts` 21 passed、`appSurface.test.ts` 415 passed、`npm run typecheck` exit 0、`npm --prefix apps/ai-game-creator-shell run typecheck` exit 0、`npm run check:encoding` 通过、`git diff --check` 干净。
+- **真机判据**:点进任一栏目(或展开「所有资源」)时,**每一张**卡片都从它那一摞的位置/尺寸位移并缩放到自己的位置,而不只是总览里那 3 张;同一栏目里不同类型的两摞各自飞各自的卡。`prefers-reduced-motion: reduce` 下仍不播放动画(口径未改)。
+- **已知未覆盖**:① 子画布内直接切到另一个栏目(分页画布换栏目)时目标栏目的卡片在该次 `begin` 时未渲染、拿不到任何 First,整段转场仍按旧口径跳过(`play` 的 `!captured.entries.size` 早退),本次未改;② 真机动画观感由浏览器渲染,jsdom 只覆盖几何与调用契约,需要按上面那条判据人工确认一次。
+- **关联**:`apps/ai-game-creator-shell/src/view/project-development/resourceBookController.ts`(`begin` 的堆锚点 / `syncNodes` 的合成 First)、`resourceBookLayout.ts`(`allOpen` 列号)、`index.tsx`(宿主 `data-resource-book-stack-*`)、`apps/ai-game-creator-shell/tests/{resourceBookController,resourceBookLayout}.test.ts`、`tests/appSurface/project-development.suite.ts`、`docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md`。
diff --git a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md
index be229580e..2ca26305a 100644
--- a/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md
+++ b/docs/technical/【技术方案】GameAgent资源自由画板与快速编辑-2026-08-20.md
@@ -7,7 +7,8 @@
- 钉在视口上的那条栏目标题栏是**例外**:它是 `.game-resource-book-scene-world` 的**兄弟节点**(挂在场景根下),只在子画布态渲染;总览态的栏目标题栏仍在 world 里。原因是文字清晰度——world 带 `scale()`,挂进去就必须给标题栏自己加 `scale(1 / s)` 抵消才能回到 1:1,而反向缩放只保证几何,整条标题栏的文字(栏目名、计数徽标、`资源总览` 入口)仍要在被缩放的祖先里栅格化/合成,一旦那一层被提升或复用旧纹理就会整行发虚。钉住的那一层本来就活在屏幕坐标系里。转场由控制器按 key 记录 First 屏幕矩形、在新宿主上从该矩形回到静止位驱动,仍然是真实元素的 FLIP,只是标题栏宿主不再跨视图保持同一个节点(卡片仍然保持)。
- 反向 transform 写在宿主节点自身坐标系:`transform-origin: 0 0`,缩放比取 `First.size / Last.size`,位移按宿主原点偏移换算后除以**宿主自己的坐标系缩放**(world 里的卡片取 `.game-resource-book-scene-world` 的缩放,world 外的钉住标题栏取 1,见 `resourceBookController.nodeWorldTransform`)。
- `begin` 之后的所有布局提交都调 `sync`:比较每个节点的布局签名,变化时取消旧动画、重新测量,以**当前视觉矩形**(含运行中的 transform)为起点重基到新的干净终点。转场进行中用剩余时长(`startTime + 420 - now`,下限 80ms),空闲布局变化用完整 420ms。卡片预览尺寸是异步到达的,因此"动画期间甚至动画结束之后"的二次布局变化同样是动画而不是瞬移。窗口/容器尺寸变化与拖拽中的卡片不做布局动画。
-- 目标不可见的节点(内联 `opacity` 为 0)钉在 First 位置淡出,没有 First 的挂载节点淡入;搜索/排序导致的卸载保留一帧后用同一套淡出撤下,进入栏目期间其他栏目保留挂载淡出,返回总览时源栏目未回到摞上的卡片按子画布布局淡出。转场时长统一 420ms;动画创建失败、零尺寸、容器尺寸变化或 `prefers-reduced-motion` 时不创建动画,也不等待定时器。
+- 目标不可见的节点(内联 `opacity` 为 0)钉在 First 位置淡出,没有 First 的挂载节点淡入(卡片例外:**有那一摞的堆锚点就从摞里飞出来**,见下一条);搜索/排序导致的卸载保留一帧后用同一套淡出撤下,进入栏目期间其他栏目保留挂载淡出,返回总览时源栏目未回到摞上的卡片按子画布布局淡出。转场时长统一 420ms;动画创建失败、零尺寸、容器尺寸变化或 `prefers-reduced-motion` 时不创建动画,也不等待定时器。
+- 总览每摞只铺 `RESOURCE_BOOK_OVERVIEW_STACK_LIMIT`(3)张卡,其余数量写在栏目卡标题栏的"N 项"里,所以进栏目/进展开态时**大部分卡片在总览侧根本没有节点**、拿不到 First 帧。这类卡片不能原地淡入(用户看到的就是"只有那几张有位移,其余直接出现"):`begin` 顺带按 (栏目, 类型摞列号) 记下**同一摞最深那张卡**(宿主 `data-resource-book-stack-index` 最大者)的 First 矩形当堆锚点,`play` 时没有 First 帧的卡片认领它自己的锚点当合成 First,走的仍是同一套 FLIP、也没有为动画多渲染任何节点。锚点只在**本次转场**有效(`maybeFinish` / `settle` / `invalidate` 都清掉),否则转场之后才挂载的卡片会从一段过期位置飞出来;转场中重基(已在跑动画的节点)不认领锚点,起点必须是"此刻像素"。展开态的每张卡仍带自己那一摞的真实列号(`buildResourceBookScenePlan` 的 `allOpen` 分支),不是一律 0。
- `begin` 只记录**当时可见**的节点。不可见节点的屏幕矩形是"看不见的位置":非活动栏目标题栏在子画布态保留总览布局坐标,却被子画布 world 变换推到屏幕外,若把它当 First,返回总览时标题栏会从屏幕外飞进来。不可见节点没有 First,只走淡入,位置保持不动;进入栏目方向它们仍然可见,因此照旧钉在原地淡出。同理,卡片只有在提交前可见时才参与几何 FLIP。
- 再次导航先读取当前像素位置,再取消旧动画并重新建立目标;token 隔离旧 promise,过期完成和取消均不能提交新状态。
- 缩放、适应内容和空白拖动先结束视觉转场再执行用户输入;转场中的资源卡 pointer-down 不启动持久化拖拽。普通滚轮只平移当前视图,Ctrl/Meta 滚轮以指针为锚点缩放,均不切换栏目。