修复画布快速编辑还原生成器

直接点击生成图时还原原生成器对话框

非生成图直接点击只选中不打开生成器

快速编辑按钮和右键入口按素材元数据打开对应生成器

补齐资源元数据回填和入口边界测试

(cherry picked from commit 45e4b6e64a)
This commit is contained in:
2026-06-29 16:06:51 +08:00
parent 08f188e26e
commit b3501f6fed
6 changed files with 336 additions and 613 deletions
File diff suppressed because it is too large Load Diff
@@ -645,11 +645,12 @@ export function hydrateLayer(
? imageSequenceFrames
: undefined,
previewVideoPath: stringOrNull(snapshot.previewVideoPath),
prompt: stringOrNull(snapshot.prompt),
actualPrompt: stringOrNull(snapshot.actualPrompt),
model: stringOrNull(snapshot.model),
provider: stringOrNull(snapshot.provider),
taskId: stringOrNull(snapshot.taskId),
prompt: stringOrNull(snapshot.prompt) ?? stringOrNull(resource?.prompt),
actualPrompt:
stringOrNull(snapshot.actualPrompt) ?? stringOrNull(resource?.actualPrompt),
model: stringOrNull(snapshot.model) ?? stringOrNull(resource?.model),
provider: stringOrNull(snapshot.provider) ?? stringOrNull(resource?.provider),
taskId: stringOrNull(snapshot.taskId) ?? stringOrNull(resource?.taskId),
objectKey: stringOrNull(snapshot.objectKey) ?? stringOrNull(resource?.objectKey),
assetObjectId:
stringOrNull(snapshot.assetObjectId) ?? stringOrNull(resource?.assetObjectId),
@@ -48,6 +48,7 @@ import type {
QuickEditPanelState,
} from './ImageCanvasEditorTypes';
import { getEditorUploadAccept, isImageFile } from './ImageCanvasFileModel';
import { createLayerGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
import { applyEditorGenerationPricingConfig } from './ImageCanvasGenerationModel';
import { isCanvasGenerationComposerVisible } from './ImageCanvasOverlayModel';
import {
@@ -88,6 +89,25 @@ function resolveLayerProjectResourceImageSrc(layer: CanvasLayer) {
return layer.src;
}
function findSourceAnimationLayer(
layers: CanvasLayer[],
sourceLayer: CanvasLayer,
) {
if (sourceLayer.assetKind !== 'character-animation') {
return null;
}
const sourceResourceId = sourceLayer.sourceResourceId?.trim();
if (!sourceResourceId) {
return null;
}
return (
layers.find(
(layer) =>
layer.resourceId === sourceResourceId || layer.id === sourceResourceId,
) ?? null
);
}
function isEditablePasteTarget(target: EventTarget | null) {
const element = target instanceof HTMLElement ? target : null;
if (!element) {
@@ -892,7 +912,6 @@ export function ImageCanvasEditorView() {
isPickingUiDesignSpecFromCanvas,
setIsPickingUiDesignSpecFromCanvas,
openCharacterAnimationPanel,
openQuickEditPanel,
openRedrawPanel,
openCropExpandPanel,
removeSelectedLayerBackground,
@@ -960,6 +979,51 @@ export function ImageCanvasEditorView() {
clearGenerationPanelsAfterBlur,
getCanvasPointFromClient,
});
const openLayerGenerationDialog = useCallback(
(layer: CanvasLayer) => {
const existingDialog = [...canvasGenerationDialogsRef.current]
.reverse()
.find((dialog) => dialog.generatedLayerId === layer.id);
if (existingDialog) {
activateCanvasGenerationDialog(existingDialog);
setSelectedLayerId(layer.id);
setSelectedLayerIds([layer.id]);
return true;
}
const restoredDraft = createLayerGenerationDialogDraft({
sourceLayer: layer,
canvasSize,
viewport: viewportRef.current,
sourceAnimationLayer: findSourceAnimationLayer(
layersRef.current,
layer,
),
});
if (!restoredDraft) {
return false;
}
openCanvasGenerationDialog(restoredDraft);
setSelectedLayerId(layer.id);
setSelectedLayerIds([layer.id]);
return true;
},
[
activateCanvasGenerationDialog,
canvasSize,
openCanvasGenerationDialog,
setSelectedLayerId,
setSelectedLayerIds,
],
);
const openGeneratedLayerGenerationDialog = useCallback(
(layer: CanvasLayer) => {
if (layer.sourceType !== 'generated') {
return false;
}
return openLayerGenerationDialog(layer);
},
[openLayerGenerationDialog],
);
const contextMenuLayer =
contextMenu?.kind === 'layer'
? (layers.find((layer) => layer.id === contextMenu.layerId) ?? null)
@@ -1046,6 +1110,7 @@ export function ImageCanvasEditorView() {
pickCharacterReferenceFromLayer,
pickIconSpecFromLayer,
pickUiDesignSpecFromLayer,
openLayerGenerationDialog: openGeneratedLayerGenerationDialog,
activateCanvasGenerationDialog,
updateCanvasGenerationDialogById,
moveViewportFromMinimapPointer,
@@ -1549,7 +1614,7 @@ export function ImageCanvasEditorView() {
onActivateGenerationDialog: activateCanvasGenerationDialog,
onToggleTaskSidebar: generationSurface.toggleTaskSidebar,
onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize,
onOpenQuickEditPanel: openQuickEditPanel,
onOpenQuickEditPanel: openLayerGenerationDialog,
onOpenRedrawPanel: openRedrawPanel,
onOpenCropExpandPanel: openCropExpandPanel,
onRemoveBackground: removeSelectedLayerBackground,
@@ -20,6 +20,7 @@ import {
createEditDialogDraft,
createGenerateDialogDraft,
createIconGenerationDialogDraft,
createLayerGenerationDialogDraft,
createPublicationGenerationDialogDraft,
createQuickEditPanelDraft,
createRedrawPanelDraft,
@@ -593,6 +594,61 @@ describe('ImageCanvasGenerationDialogModel', () => {
});
});
it('restores a plain generated layer from its own metadata', () => {
const canvasSize = { width: 960, height: 720 };
const viewport = { x: 0, y: 0, scale: 1 };
expect(
createLayerGenerationDialogDraft({
sourceLayer: createLayer({
sourceType: 'generated',
prompt: '图层提示词',
model: 'gpt-image-2',
generationInputs: {
fields: [{ title: '生成提示词', value: '元数据提示词' }],
references: [],
},
}),
canvasSize,
viewport,
}),
).toMatchObject({
mode: 'generate',
prompt: '元数据提示词',
generatedLayerId: 'layer-source',
imageModel: 'gpt-image-2',
placeholder: {
x: 120,
y: 140,
width: 320,
height: 240,
originalWidth: 1024,
originalHeight: 768,
},
});
});
it('restores a tagged uploaded layer from its current asset kind', () => {
expect(
createLayerGenerationDialogDraft({
sourceLayer: createLayer({
sourceType: 'uploaded',
assetKind: 'character',
generationInputs: {
fields: [{ title: '角色设定', value: '黑衣剑士' }],
references: [],
},
}),
canvasSize: { width: 960, height: 720 },
viewport: { x: 0, y: 0, scale: 1 },
}),
).toMatchObject({
mode: 'character',
prompt: '黑衣剑士',
generatedLayerId: 'layer-source',
});
});
it('normalizes legacy nanobanana model aliases in quick edit drafts', () => {
expect(
createQuickEditPanelDraft(
@@ -649,9 +649,55 @@ function resolveGeneratedSourceDialogMode({
if (sourceLayer.assetKind === 'character-animation') {
return 'character-animation';
}
if (sourceLayer.sourceType === 'generated') {
return 'generate';
}
return null;
}
function buildLayerGenerationPlaceholder(sourceLayer: CanvasLayer) {
return {
x: sourceLayer.x,
y: sourceLayer.y,
width: sourceLayer.width,
height: sourceLayer.height,
originalWidth: sourceLayer.originalWidth,
originalHeight: sourceLayer.originalHeight,
};
}
export function createLayerGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
sourceAnimationLayer,
}: Omit<SourceGenerationDialogDraftContext, 'mode'>): Omit<
CanvasGenerationDialogState,
'id'
> | null {
const draft = createSameSourceGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
sourceAnimationLayer,
mode: 'redraw',
});
if (!draft) {
return null;
}
return {
...draft,
prompt: draft.prompt || sourceLayer.prompt?.trim() || '',
imageModel: sourceDialog?.imageModel ?? sourceLayer.model ?? draft.imageModel,
status: 'idle',
composerOpen: true,
generatedLayerId: sourceLayer.id,
placeholder: buildLayerGenerationPlaceholder(sourceLayer),
};
}
function getSourceDraftPrompt({
sourceLayer,
mode,
@@ -70,6 +70,7 @@ type UseImageCanvasStageInteractionsOptions = {
pickIconSpecFromLayer: (layer: CanvasLayer) => void;
pickUiDesignSpecFromLayer: (layer: CanvasLayer) => void;
pickPublicationReferenceFromLayer: (layer: CanvasLayer) => void;
openLayerGenerationDialog?: (layer: CanvasLayer) => boolean;
activateCanvasGenerationDialog: (
dialog: CanvasGenerationDialogState,
) => void;
@@ -120,6 +121,7 @@ export function useImageCanvasStageInteractions({
pickIconSpecFromLayer,
pickUiDesignSpecFromLayer,
pickPublicationReferenceFromLayer,
openLayerGenerationDialog,
activateCanvasGenerationDialog,
updateCanvasGenerationDialogById,
moveViewportFromMinimapPointer,
@@ -389,9 +391,11 @@ export function useImageCanvasStageInteractions({
}
setSelectedLayerId(layer.id);
setSelectedLayerIds([layer.id]);
setGenerateDialog((currentDialog) =>
updateGenerateDialogForLayerClick(currentDialog),
);
if (!openLayerGenerationDialog?.(layer)) {
setGenerateDialog((currentDialog) =>
updateGenerateDialogForLayerClick(currentDialog),
);
}
onCloseImageContextMenu();
},
[
@@ -402,6 +406,7 @@ export function useImageCanvasStageInteractions({
isPickingIconSpecFromCanvas,
isPickingPublicationReferenceFromCanvas,
onCloseImageContextMenu,
openLayerGenerationDialog,
setGenerateDialog,
setSelectedLayerId,
setSelectedLayerIds,