合并 master 最新变更

同步远端 master 至游戏场景功能分支
保留场景自定义画风控件并接入 master 的自适应文本输入组件
解决图片画布基础生成表单的唯一合并冲突
This commit is contained in:
2026-08-05 11:02:58 +08:00
139 changed files with 37973 additions and 3016 deletions
@@ -40,6 +40,7 @@ import {
generationInputsOrNull,
isInlineEditorMediaSource,
resolveContextMenuPosition,
resolveLayerResourceAssetKind,
} from './ImageCanvasEditorModel';
import { ImageCanvasEditorShellView } from './ImageCanvasEditorShellView';
import type {
@@ -66,6 +67,7 @@ import {
} from './ImageCanvasGenerationModel';
import { formatCanvasHistoryAction } from './ImageCanvasHistoryModel';
import { fitViewportToBounds } from './ImageCanvasInteractionModel';
import type { CanvasLayerCopyBlockReason } from './ImageCanvasLayerCommandModel';
import {
isCanvasGenerationComposerVisible,
resolveQuickEditFocusViewport,
@@ -325,15 +327,7 @@ export function ImageCanvasEditorView({
const discardCanvasHistoryEntriesContainingLayerRef = useRef<
(matchesLayer: (layer: CanvasLayer) => boolean) => void
>(() => {});
const persistRestoredAssetKindRef = useRef<
(layer: CanvasLayer, historyEntryId?: number) => void
>(() => {});
const layerResourcePersistenceVersionRef = useRef(new Map<string, number>());
const assetKindRollbackLayerByIdRef = useRef(new Map<string, CanvasLayer>());
const assetKindHistoryEntryIdsByLayerIdRef = useRef(
new Map<string, Set<number>>(),
);
const persistingAssetKindLayerIdsRef = useRef(new Set<string>());
const resetCanvasInteractionStateRef = useRef<() => void>(() => {});
const closeGenerationTransientStateRef = useRef<() => void>(() => {});
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
@@ -798,35 +792,24 @@ export function ImageCanvasEditorView({
}),
[],
);
const canvasHistoryEffects = useMemo(
() => ({
persistRestoredAssetKind: (layer: CanvasLayer, historyEntryId?: number) =>
persistRestoredAssetKindRef.current(layer, historyEntryId),
}),
[],
);
const {
canUndo,
canRedo,
getCanvasHistorySnapshot,
captureCanvasHistory,
discardCanvasHistoryEntriesContainingLayer,
discardCanvasHistoryEntriesById,
undoCanvasChange,
redoCanvasChange,
} = useCanvasHistory({
refs: canvasHistoryRefs,
setters: canvasHistorySetters,
resetters: canvasHistoryResetters,
effects: canvasHistoryEffects,
});
captureCanvasHistoryRef.current = captureCanvasHistory;
discardCanvasHistoryEntriesContainingLayerRef.current =
discardCanvasHistoryEntriesContainingLayer;
const [historyFeedback, setHistoryFeedback] =
useState<CanvasHistoryFeedback | null>(null);
const [persistingAssetKindLayerIds, setPersistingAssetKindLayerIds] =
useState<ReadonlySet<string>>(() => new Set());
useEffect(() => {
if (!historyFeedback) {
return;
@@ -834,6 +817,21 @@ export function ImageCanvasEditorView({
const timer = window.setTimeout(() => setHistoryFeedback(null), 3000);
return () => window.clearTimeout(timer);
}, [historyFeedback]);
const showCanvasLayerCopyWarning = useCallback(
(reason: CanvasLayerCopyBlockReason) => {
setHistoryFeedback((currentFeedback) => ({
id: (currentFeedback?.id ?? 0) + 1,
tone: 'warning',
text:
reason === 'self-contained-local-unsupported'
? '历史序列帧素材暂不支持复制'
: reason === 'unresolved-resource'
? '素材资源尚未登记,暂不能复制'
: '素材仍在保存,请稍后重试',
}));
},
[],
);
const handleUndoCanvasChange = useCallback(() => {
const result = undoCanvasChange();
if (result.status === 'empty') {
@@ -1240,105 +1238,19 @@ export function ImageCanvasEditorView({
.then(applyGeneratedProjectSnapshot)
.catch(() => undefined);
}, [applyGeneratedProjectSnapshot, projectId]);
const setLayerAssetKindPersistencePending = useCallback(
(layerId: string, isPending: boolean) => {
const nextLayerIds = new Set(persistingAssetKindLayerIdsRef.current);
if (isPending) {
nextLayerIds.add(layerId);
} else {
nextLayerIds.delete(layerId);
}
persistingAssetKindLayerIdsRef.current = nextLayerIds;
setPersistingAssetKindLayerIds(nextLayerIds);
},
[],
);
const persistUpdatedLayerResource = useCallback(
(
layer: CanvasLayer,
rollbackLayer?: CanvasLayer,
historyEntryId?: number,
) => {
(layer: CanvasLayer) => {
const requestVersion =
(layerResourcePersistenceVersionRef.current.get(layer.id) ?? 0) + 1;
layerResourcePersistenceVersionRef.current.set(layer.id, requestVersion);
const confirmedRollbackLayer = rollbackLayer
? (assetKindRollbackLayerByIdRef.current.get(layer.id) ?? rollbackLayer)
: undefined;
if (confirmedRollbackLayer) {
assetKindRollbackLayerByIdRef.current.set(
layer.id,
confirmedRollbackLayer,
);
if (historyEntryId !== undefined) {
const historyEntryIds = new Set(
assetKindHistoryEntryIdsByLayerIdRef.current.get(layer.id),
);
historyEntryIds.add(historyEntryId);
assetKindHistoryEntryIdsByLayerIdRef.current.set(
layer.id,
historyEntryIds,
);
}
setLayerAssetKindPersistencePending(layer.id, true);
}
const rollbackLatestRequest = (error: unknown) => {
if (
layerResourcePersistenceVersionRef.current.get(layer.id) !==
requestVersion ||
!confirmedRollbackLayer
) {
return;
}
setLayers((currentLayers) => {
const nextLayers = currentLayers.map((currentLayer) =>
currentLayer.id === layer.id
? {
...currentLayer,
assetKind: confirmedRollbackLayer.assetKind,
resourceId: confirmedRollbackLayer.resourceId,
}
: currentLayer,
);
layersRef.current = nextLayers;
return nextLayers;
});
setMetadataLayer((currentLayer) =>
currentLayer?.id === layer.id
? {
...currentLayer,
assetKind: confirmedRollbackLayer.assetKind,
resourceId: confirmedRollbackLayer.resourceId,
}
: currentLayer,
);
const historyEntryIds =
assetKindHistoryEntryIdsByLayerIdRef.current.get(layer.id);
if (historyEntryIds) {
discardCanvasHistoryEntriesById(historyEntryIds);
}
assetKindHistoryEntryIdsByLayerIdRef.current.delete(layer.id);
assetKindRollbackLayerByIdRef.current.delete(layer.id);
setLayerAssetKindPersistencePending(layer.id, false);
const reason =
error instanceof Error && error.message.trim()
? error.message.trim()
: '请稍后重试';
setHistoryFeedback((currentFeedback) => ({
id: (currentFeedback?.id ?? 0) + 1,
tone: 'warning',
text: `素材类型保存失败,已恢复原标签:${reason}`,
}));
};
if (!projectId) {
rollbackLatestRequest(new Error('项目尚未就绪'));
return;
}
const imageSrc = resolveLayerProjectResourceImageSrc(layer);
if (!imageSrc) {
rollbackLatestRequest(new Error('素材图片尚未就绪'));
return;
}
const resourceAssetKind = resolveLayerResourceAssetKind(layer);
void createEditorProjectResource(projectId, {
imageSrc,
objectKey: layer.objectKey,
@@ -1353,7 +1265,7 @@ export function ImageCanvasEditorView({
taskId: layer.taskId,
durationSeconds: layer.durationSeconds,
sourceResourceId: layer.sourceResourceId,
assetKind: layer.assetKind,
assetKind: resourceAssetKind,
generationInputs: layer.generationInputs,
})
.then((resource) => {
@@ -1363,21 +1275,19 @@ export function ImageCanvasEditorView({
) {
return;
}
if (
confirmedRollbackLayer &&
(resource.assetKind !== layer.assetKind ||
!resource.resourceId.trim())
) {
rollbackLatestRequest(new Error('服务器未确认新的素材类型'));
return;
}
const confirmedResourceAssetKind =
canvasAssetKindOrNull(resource.assetKind) ?? resourceAssetKind;
setLayers((currentLayers) => {
const nextLayers = currentLayers.map((currentLayer) =>
currentLayer.id === layer.id
? {
...currentLayer,
assetKind: layer.assetKind,
resourceId: resource.resourceId,
resourcePersistenceState: 'registered' as const,
resourceAssetKind: confirmedResourceAssetKind,
assetKind:
currentLayer.assetKindOverride ??
confirmedResourceAssetKind,
}
: currentLayer,
);
@@ -1388,31 +1298,20 @@ export function ImageCanvasEditorView({
currentLayer?.id === layer.id
? {
...currentLayer,
assetKind: layer.assetKind,
resourceId: resource.resourceId,
resourcePersistenceState: 'registered' as const,
resourceAssetKind: confirmedResourceAssetKind,
assetKind:
currentLayer.assetKindOverride ??
confirmedResourceAssetKind,
}
: currentLayer,
);
if (confirmedRollbackLayer) {
assetKindHistoryEntryIdsByLayerIdRef.current.delete(layer.id);
assetKindRollbackLayerByIdRef.current.delete(layer.id);
setLayerAssetKindPersistencePending(layer.id, false);
}
})
.catch(rollbackLatestRequest);
.catch(() => undefined);
},
[
discardCanvasHistoryEntriesById,
projectId,
setLayerAssetKindPersistencePending,
],
[projectId],
);
persistRestoredAssetKindRef.current = (layer, historyEntryId) => {
const rollbackLayer = layersRef.current.find(
(currentLayer) => currentLayer.id === layer.id,
);
persistUpdatedLayerResource(layer, rollbackLayer ?? layer, historyEntryId);
};
const {
assetExportStatus,
isExportingAssets,
@@ -1520,11 +1419,7 @@ export function ImageCanvasEditorView({
}
refreshEditorWalletState();
},
[
projectId,
refreshEditorWalletState,
showGenerationWarning,
],
[projectId, refreshEditorWalletState, showGenerationWarning],
);
const effectiveIsAgentConversationOpen =
isAgentConversationEnabled && isAgentConversationOpen;
@@ -1781,6 +1676,9 @@ export function ImageCanvasEditorView({
: null;
const {
canvasClipboard,
canCopyContextLayers,
contextLayerCopyBlockReason,
isCanvasClipboardCopyBlocked,
pasteCanvasClipboard,
copyContextLayers,
copySelectedLayers,
@@ -1805,6 +1703,7 @@ export function ImageCanvasEditorView({
selectedLayerIds,
canvasGenerationDialogs,
setLayers,
appendCanvasLayersWithResources,
setSelectedLayerId,
setSelectedLayerIds,
setHoveredLayerId,
@@ -1817,6 +1716,7 @@ export function ImageCanvasEditorView({
onDeleteLayerSideEffects: clearDeletedLayerGenerationState,
onDeleteGenerationDialogSideEffects: removeCanvasGenerationDialogById,
exportLayerImage,
onCanvasLayerCopyBlocked: showCanvasLayerCopyWarning,
});
const {
canvasMarquee,
@@ -1961,18 +1861,23 @@ export function ImageCanvasEditorView({
if (!targetLayer || targetLayer.assetKind === assetKind) {
return;
}
const historyEntryId = captureCanvasHistory({
captureCanvasHistory({
type: 'change-asset-kind',
count: 1,
layerIds: [layerId],
});
const resourceAssetKind = resolveLayerResourceAssetKind(targetLayer);
const assetKindOverride =
assetKind === resourceAssetKind ? null : assetKind;
const nextLayer = {
...targetLayer,
assetKind,
resourceAssetKind,
assetKindOverride,
assetKind: assetKindOverride ?? resourceAssetKind,
};
setLayers((currentLayers) => {
const nextLayers = currentLayers.map((layer) =>
layer.id === layerId ? { ...layer, assetKind } : layer,
layer.id === layerId ? nextLayer : layer,
);
layersRef.current = nextLayers;
return nextLayers;
@@ -1980,11 +1885,10 @@ export function ImageCanvasEditorView({
setMetadataLayer((currentLayer) =>
currentLayer?.id === layerId ? nextLayer : currentLayer,
);
persistUpdatedLayerResource(nextLayer, targetLayer, historyEntryId);
setImageContextMenu(null);
setContextMenu(null);
},
[captureCanvasHistory, persistUpdatedLayerResource],
[captureCanvasHistory],
);
const selectAllCanvasObjects = useCallback(() => {
const layerSelectionIds = layersRef.current
@@ -2387,7 +2291,6 @@ export function ImageCanvasEditorView({
generationComposerStyle,
selectedToolbarStyle,
splittingIconSpritesheetLayerIds,
persistingAssetKindLayerIds,
uploadDropTarget,
contextMenu,
canvasClipboard,
@@ -2395,6 +2298,9 @@ export function ImageCanvasEditorView({
imageContextMenuLayer,
contextShouldShowLayer,
contextShouldUnlockLayer,
canCopyContextLayers,
contextLayerCopyBlockReason,
isCanvasClipboardCopyBlocked,
canUndo,
canRedo,
historyFeedback,
@@ -2438,10 +2344,9 @@ export function ImageCanvasEditorView({
onOpenCropExpandPanel: openCropExpandPanel,
onRemoveBackground: removeSelectedLayerBackground,
onSplitIconSpritesheet: (layer: CanvasLayer) => {
if (persistingAssetKindLayerIdsRef.current.has(layer.id)) {
return;
}
void splitSelectedIconSpritesheet(layer);
void flushProjectPersistence().then(() =>
splitSelectedIconSpritesheet(layer),
);
},
onExtractUiDesignAssets: extractUiDesignAssets,
onUiAssetExtractionToolChange: changeUiAssetExtractionTool,