diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts b/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts index a65ed50dc..d04c52f93 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/useUiEditorState.ts @@ -524,6 +524,40 @@ function spriteResourceValidationError(sprite: SpriteAsset) { return border.ok ? null : border.message; } +/** + * Apply the SpriteAsset resource checks without acquiring the editor mutation + * guard. Workflow adapters use this while a State lock is already held so + * resource insertion and component backfill can be committed atomically. + */ +export function addSpriteAssetsToState( + current: State, + assets: readonly SpriteAsset[], +): UiEditorOperationResult { + const unique = new Map(); + for (const asset of assets) { + const candidate = + unique.get(asset.asset_id) ?? current.sprite_assets[asset.asset_id]; + if (candidate && !sameResource(candidate, asset)) { + return { ok: false, reason: 'duplicate' }; + } + unique.set(asset.asset_id, asset); + } + const invalidSprite = [...unique.values()] + .map((asset) => ({ asset, error: spriteResourceValidationError(asset) })) + .find((item) => item.error); + if (invalidSprite?.error) { + return { + ok: false, + reason: `invalid:${invalidSprite.asset.asset_id}:${invalidSprite.error}`, + }; + } + const next = cloneState(current); + for (const asset of unique.values()) { + next.sprite_assets[asset.asset_id] = structuredClone(asset); + } + return { ok: true, value: next }; +} + function fontResourceValidationError(font: FontAsset) { if (font.asset_id.trim().length === 0) return '缺少字体 ID'; if (font.path.trim().length === 0) return '缺少字体路径'; @@ -782,32 +816,9 @@ export function useUiEditorState(initialState: State = EMPTY_UI_EDITOR_STATE) { const blocked = guard(); if (blocked) return blocked; const current = stateRef.current; - const unique = new Map(); - for (const asset of assets) { - const candidate = - unique.get(asset.asset_id) ?? current.sprite_assets[asset.asset_id]; - if (candidate && !sameResource(candidate, asset)) { - return { ok: false, reason: 'duplicate' }; - } - unique.set(asset.asset_id, asset); - } - const invalidSprite = [...unique.values()] - .map((asset) => ({ - asset, - error: spriteResourceValidationError(asset), - })) - .find((item) => item.error); - if (invalidSprite?.error) { - return { - ok: false, - reason: `invalid:${invalidSprite.asset.asset_id}:${invalidSprite.error}`, - }; - } - const next = cloneState(current); - for (const asset of unique.values()) { - next.sprite_assets[asset.asset_id] = structuredClone(asset); - } - commit(next); + const result = addSpriteAssetsToState(current, assets); + if (!result.ok) return result; + commit(result.value); return { ok: true, value: undefined }; }, [commit, guard],