From d02d0ed7a45de31f9601ff6478ee04a9acea4545 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 14 Sep 2026 17:07:55 +0800 Subject: [PATCH] =?UTF-8?q?AGC=20=E8=B5=84=E6=BA=90=E7=94=BB=E5=B8=83?= =?UTF-8?q?=EF=BC=9A=E6=96=B0=E7=B4=A0=E6=9D=90=E4=B8=8D=E5=86=8D=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E9=87=8D=E6=8E=92=EF=BC=8C=E6=94=B9=E4=B8=BA=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E3=80=8C=E6=95=B4=E7=90=86=E7=94=BB=E5=B8=83=E3=80=8D?= =?UTF-8?q?=E5=B9=B6=E5=9C=A8=E7=94=9F=E6=88=90=E5=90=8E=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=81=9A=E7=84=A6=E6=96=B0=E7=B4=A0=E6=9D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useProjectResourceCanvasLayout:资源写意图新增 rederive 标记,显式整理那一笔按 rederive 策略重算自动坐标(手动坐标原样保留),自动同步仍是 preserve 只补新卡 - useProjectResourceCanvasLayout:新增 rederiveNow(),复用既有写队列 / sidecar 写回链路, 只在坐标确有变化时落盘,成功复用既有「布局已保存」提示 - index.tsx:两个排序模式的 rederiveAutomaticPositions 都固定 false,画布不再因为资源 协调签名变化(新增素材、改标签、改分类、拓扑重建)整体重排 - index.tsx:依赖侧「关系图首次就绪」改为按项目作用域的一次性 flag,等该侧 sidecar ready 后调用一次 rederiveNow(),之后一律只补新卡(切排序 tab 不重新武装) - index.tsx:依赖/类型 tab 组新增「整理画布」按钮,作为整张画布重排的唯一显式入口 - index.tsx:新增 manifest.assets 新增 id 检测(seenManifestAssetIdsRef),把新卡交给既有 pendingResourceFocusRef + advanceFocusGeneration() 裁决链聚焦;首次打开 / 切项目只登记 基线不聚焦,重命名不改 id 天然不触发,已有同目标意图时不重复挂 - 新增 tests/resourceCanvasManualLayout.test.tsx(8 条):新素材入库后既有自动坐标逐值不变、 依赖侧首次就绪后不再重排、新素材自动聚焦、被搜索挡住走既有「清除搜索并定位」、 「整理画布」重算并保留手动坐标、已经整齐时再按一次不写盘、首次打开与切项目不聚焦 --- .../src/view/project-development/index.tsx | 116 ++- .../useProjectResourceCanvasLayout.ts | 67 +- .../tests/resourceCanvasManualLayout.test.tsx | 838 ++++++++++++++++++ 3 files changed, 1015 insertions(+), 6 deletions(-) create mode 100644 apps/ai-game-creator-shell/tests/resourceCanvasManualLayout.test.tsx 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..14fa858a9 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 @@ -2369,7 +2369,8 @@ export default function ProjectDevelopmentView({ topology: resourceLayoutTopology, initializationReady: resourceGraphInitializationReady, renderFallbackWhileBlocked: resourceGraphFailed, - rederiveAutomaticPositions: resourceGraphReady, + // 画布不再自动重派生:新增素材只补位,整张重排只能由「整理画布」显式发起。 + rederiveAutomaticPositions: false, }); const typeLayout = useProjectResourceCanvasLayout({ projectPath, @@ -2377,8 +2378,38 @@ export default function ProjectDevelopmentView({ mode: 'type', resources: resourcesWithCanvasCardSize, initializationReady: true, - rederiveAutomaticPositions: true, + rederiveAutomaticPositions: false, }); + /** + * 依赖视图的自动重派生只做一次:关系图首次就绪、且这一侧的 sidecar 已经读完的那一刻。 + * + * 旧口径把 `rederiveAutomaticPositions` 长期等于 `resourceGraphReady`——只要关系图是 + * 现成的,任何一次资源协调签名变化(新增一张素材、改一个标签、拓扑重建)都会丢掉全部 + * 自动坐标重排整张画布。现在改成一次性:首次就绪时用显式重派生把坐标对齐到最终拓扑, + * 之后一律只补新卡。用 ref 记"这一次已经做过",而不是让布尔长期为真;判据也放在 + * `dependencyLayout.ready` 之后就绪,避免"关系图先就绪、sidecar 后读完"时把这一次重算 + * 白白吃掉。按项目作用域记账:切排序 tab 不会重新武装它。 + */ + const dependencyRederiveScopeRef = useRef(null); + const dependencyLayoutReady = dependencyLayout.ready; + const rederiveDependencyLayout = dependencyLayout.rederiveNow; + useEffect(() => { + if (!resourceGraphReady || !dependencyLayoutReady) { + return; + } + const scopeKey = JSON.stringify([projectPath, manifest.projectId]); + if (dependencyRederiveScopeRef.current === scopeKey) { + return; + } + dependencyRederiveScopeRef.current = scopeKey; + rederiveDependencyLayout(); + }, [ + dependencyLayoutReady, + manifest.projectId, + projectPath, + rederiveDependencyLayout, + resourceGraphReady, + ]); const activeResourceLayout = sortMode === 'dependency' ? dependencyLayout : typeLayout; const resourceLayout = activeResourceLayout.layout; @@ -5103,6 +5134,72 @@ export default function ProjectDevelopmentView({ [advanceFocusGeneration, manifest.assets, openUiDesignEditor], ); + /** + * 新素材入库后自动聚焦:只认 `manifest.assets` 里**此前没见过**的 id。 + * + * 存量素材不是"刚生成":首次打开项目 / 切项目只登记基线,不然一进工作台就会跳到 + * 最后一张卡上。重命名不改 id,天然不触发。 + * + * 这里不自己造滚动或选中逻辑,只把意图交给既有的 `pendingResourceFocusRef` + + * `advanceFocusGeneration()` 裁决链:投影、两种布局落位、可见性、DOM 就绪都由那条链 + * 负责,被搜索条件挡住时也沿用现成的「清除搜索并定位」提示与动作。 + * + * 已有一条指向同一资源的聚焦意图时不再插手:显式生成链路(面板/工具栏生成)已经在 + * 提交时就挂好了意图,重复设置只会把它更精细的提示文案顶掉。 + */ + const seenManifestAssetIdsRef = useRef<{ + scopeKey: string; + ids: Set; + } | null>(null); + useEffect(() => { + const scopeKey = JSON.stringify([projectPath, manifest.projectId]); + const assetIds = manifest.assets.map((asset) => asset.id); + const previous = seenManifestAssetIdsRef.current; + if (!previous || previous.scopeKey !== scopeKey) { + seenManifestAssetIdsRef.current = { scopeKey, ids: new Set(assetIds) }; + return; + } + const addedIds = assetIds.filter((id) => !previous.ids.has(id)); + for (const id of assetIds) { + previous.ids.add(id); + } + if (addedIds.length === 0) { + return; + } + // 一次可能进多条(例如一次派生产出多个产物):聚焦清单里最后落地的那一条。 + const addedAssetId = addedIds[addedIds.length - 1]!; + const resourceId = `asset:${addedAssetId}`; + // 资源投影是同步的:这里查不到对应资源说明这条素材不会出现在画布上, + // 不为它挂聚焦意图,免得裁决链一直停在"等待投影"。 + if (!resources.some((resource) => resource.id === resourceId)) { + return; + } + if (pendingResourceFocusRef.current?.resourceId === resourceId) { + return; + } + const flowId = crypto.randomUUID(); + const focusGeneration = advanceFocusGeneration(); + activeFocusFlowIdRef.current = flowId; + pendingResourceFocusRef.current = { + flowId, + saveAttemptId: flowId, + sessionId: flowId, + draftId: flowId, + commitId: `asset-added:${addedAssetId}`, + projectPath, + projectId: manifest.projectId, + focusGeneration, + resourceId, + completed: false, + }; + }, [ + advanceFocusGeneration, + manifest.assets, + manifest.projectId, + projectPath, + resources, + ]); + useEffect(() => { if (uiEditorRoute) return; const completed = manifest.assets.find( @@ -7101,6 +7198,21 @@ export default function ProjectDevelopmentView({