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({