From 5844afa14d493e881b77647372b000bbbe62840d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 9 Sep 2026 20:17:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=BD=E5=8F=96=E8=87=AA=E5=8A=A8=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E7=B4=A0=E6=9D=90=E7=8A=B6=E6=80=81=E5=8F=98=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用 SpriteAsset 校验并支持锁内构造下一份 State。 保留公开 addSpriteAssets 的锁保护与历史提交行为。 --- .../features/ui-editor/useUiEditorState.ts | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) 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],