快速编辑改造接入统一画布生成器,视频改造走视频流程

快速编辑不再使用独立 quickEditPanel,改为 openPlacedCanvasGenerationDialog 创建 quick-edit 模式的画布生成器占位及作曲面板
改造操作区分媒体类型:视频层改造走 createVideoRedrawGenerationDialogDraft 自动打开视频生成面板
appendGenerationReference 扩展支持 quick-edit 模式,pickQuickEditReferenceFromLayer / updateQuickEditSelection 走统一 generateDialog 状态
去掉 appendQuickEditReference / archiveActiveCanvasGenerationDialog 调用,新增 createVideoRedrawGenerationDialogDraft
集成测试更新对话框 aria-label 为统一生成器标签
This commit is contained in:
2026-06-25 16:55:41 +08:00
parent 346146113d
commit e4ac57c97d
4 changed files with 135 additions and 37 deletions
@@ -959,9 +959,9 @@ describe('ImageCanvasEditorView generation integration', () => {
clientY: 120,
});
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
expect(screen.getByRole('dialog', { name: '快速编辑图片' })).toBeTruthy();
expect(screen.getByRole('dialog', { name: '改造图片' })).toBeTruthy();
expect(
screen.queryByRole('button', { name: '关闭快速编辑图片' }),
screen.queryByRole('button', { name: '关闭改造图片' }),
).toBeNull();
fireEvent.pointerDown(viewport, {
@@ -971,7 +971,7 @@ describe('ImageCanvasEditorView generation integration', () => {
clientY: 180,
});
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull();
expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull();
fireEvent.pointerDown(sourceLayer, {
button: 0,
@@ -2785,7 +2785,7 @@ describe('ImageCanvasEditorView generation integration', () => {
expect(screen.getByText('角色')).toBeTruthy();
expect(screen.getByRole('button', { name: '生成动画' })).toBeTruthy();
expect(screen.queryByRole('dialog', { name: '重绘图片' })).toBeNull();
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull();
expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull();
expect(
screen.queryByRole('dialog', { name: '角色动画生成面板' }),
).toBeNull();
@@ -2796,7 +2796,7 @@ describe('ImageCanvasEditorView generation integration', () => {
expect(screen.getByRole('menuitem', { name: '生成动画' })).toBeTruthy();
expect(screen.queryByRole('dialog', { name: '重绘图片' })).toBeNull();
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull();
expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '生成动画' }));
expect(
@@ -3242,7 +3242,7 @@ describe('ImageCanvasEditorView generation integration', () => {
});
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
const quickPanel = screen.getByRole('dialog', { name: '快速编辑图片' });
const quickPanel = screen.getByRole('dialog', { name: '改造图片' });
expect(quickPanel.className).toContain(
'image-canvas-editor__quick-edit-panel',
);
@@ -3284,7 +3284,7 @@ describe('ImageCanvasEditorView generation integration', () => {
);
const updatedQuickPanel = screen.getByRole('dialog', {
name: '快速编辑图片',
name: '改造图片',
});
expect(within(updatedQuickPanel).getByText('星空参考')).toBeTruthy();
expect(within(updatedQuickPanel).queryByLabelText('原图 图2')).toBeNull();
@@ -3411,7 +3411,7 @@ describe('ImageCanvasEditorView generation integration', () => {
).toBeTruthy();
fireEvent.click(within(menu).getByRole('menuitem', { name: '快速编辑' }));
const panel = screen.getByRole('dialog', { name: '快速编辑图片' });
const panel = screen.getByRole('dialog', { name: '改造图片' });
expect(within(panel).queryByText('右键图片')).toBeNull();
fireEvent.change(within(panel).getByLabelText('快速编辑提示词'), {
target: { value: '换成夜晚' },
@@ -3502,7 +3502,7 @@ describe('ImageCanvasEditorView generation integration', () => {
});
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
const quickPanel = screen.getByRole('dialog', { name: '快速编辑图片' });
const quickPanel = screen.getByRole('dialog', { name: '改造图片' });
fireEvent.change(within(quickPanel).getByLabelText('快速编辑提示词'), {
target: { value: '改成陶泥风格' },
});
@@ -3753,13 +3753,13 @@ describe('ImageCanvasEditorView generation integration', () => {
clientY: 180,
});
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
const quickPanel = screen.getByRole('dialog', { name: '快速编辑图片' });
const quickPanel = screen.getByRole('dialog', { name: '改造图片' });
fireEvent.change(within(quickPanel).getByLabelText('快速编辑提示词'), {
target: { value: '加一层暖光' },
});
fireEvent.click(within(quickPanel).getByRole('button', { name: '生成' }));
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull();
expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull();
expect(screen.getByAltText('画布图片:快速编辑源图')).toBeTruthy();
expect(sourceLayer.className).not.toContain(
'image-canvas-editor__layer--generating',
@@ -117,6 +117,36 @@ function renderComposer(
}
describe('ImageCanvasGenerationComposerView', () => {
it('让快速编辑复用普通图片生成面板结构', () => {
renderComposer({
mode: 'quick-edit',
prompt: '',
status: 'idle',
composerOpen: true,
sourceLayerId: 'layer-source',
generationReferences: [],
imageModel: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '1K',
});
const panel = screen.getByRole('dialog', { name: '改造图片' });
expect(panel.className).toContain(
'image-canvas-editor__generation-composer',
);
expect(panel.className).toContain(
'image-canvas-editor__generation-composer--image',
);
expect(panel.className).not.toContain(
'image-canvas-editor__quick-edit-panel',
);
expect(within(panel).getByRole('textbox', { name: '改造提示词' }))
.toBeTruthy();
expect(
panel.querySelector('.image-canvas-editor__generation-composer-footer'),
).toBeTruthy();
});
it('让生成UI设计图面板复用普通图片生成面板的纵向结构', () => {
const setGenerateDialog = vi.fn();
renderComposer({
@@ -559,6 +559,41 @@ export function createAudioRedrawGenerationDialogDraft(
};
}
export function createVideoRedrawGenerationDialogDraft(
sourceLayer: CanvasLayer,
): Omit<CanvasGenerationDialogState, 'id'> | null {
if (sourceLayer.mediaType !== 'video') {
return null;
}
const placeholderSize = resolveEditorVideoGenerationPixelSize({
aspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
resolution: '480p',
});
return {
mode: 'video',
sourceLayerId: sourceLayer.id,
prompt: resolveUserGenerationPromptSnapshot(sourceLayer),
status: 'idle',
composerOpen: true,
generationReferences: [createCanvasLayerReference(sourceLayer)],
videoModel: DEFAULT_VIDEO_MODEL,
videoAspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
videoResolution: '480p',
videoDurationSeconds: DEFAULT_VIDEO_DURATION_SECONDS,
videoMode: 'std',
videoSound: DEFAULT_VIDEO_SOUND,
videoWebSearchEnabled: DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
placeholder: {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
export function createUiDesignGenerationDialogDraft({
canvasSize,
viewport,
@@ -784,7 +819,7 @@ export function appendGenerationReference(
if (dialog?.mode === 'video' && !isSeedanceVideoModel(dialog.videoModel)) {
return dialog;
}
if (dialog?.mode === 'generate') {
if (dialog?.mode === 'generate' || dialog?.mode === 'quick-edit') {
if (getReferenceMediaType(layer) !== 'image') {
return dialog;
}
@@ -36,7 +36,6 @@ import {
appendCharacterReference,
appendGenerationReference,
appendPublicationReference,
appendQuickEditReference,
assignCharacterSpecReference,
assignIconSpecReference,
assignUiDesignSpecReference,
@@ -55,6 +54,7 @@ import {
createSoundEffectGenerationDialogDraft,
createSpecDialogDraft,
createUiDesignGenerationDialogDraft,
createVideoRedrawGenerationDialogDraft,
createVideoGenerationDialogDraft,
hideGeneratedLayerComposerAfterBlur,
isCharacterSpecReferenceLayer,
@@ -900,11 +900,30 @@ export function useImageCanvasGenerationWorkflow({
setImageContextMenu(null);
setMetadataLayer(null);
setUiAssetExtractionState(null);
archiveActiveCanvasGenerationDialog();
setGenerateDialog(null);
setCropExpandPanel(null);
setCharacterAnimationPanel(null);
setQuickEditPanel(createQuickEditPanelDraft(sourceLayer));
const quickEditDraft = createQuickEditPanelDraft(sourceLayer);
const quickEditSize = resolveEditorImageGenerationPixelSize({
model: quickEditDraft.model,
aspectRatio: quickEditDraft.aspectRatio,
imageSize: quickEditDraft.imageSize,
});
openPlacedCanvasGenerationDialog({
...createQuickEditGenerationDialogDraft({
sourceLayer,
prompt: quickEditDraft.prompt,
references: quickEditDraft.quickEditReferences ?? [],
model: quickEditDraft.model,
aspectRatio: quickEditDraft.aspectRatio,
imageSize: quickEditDraft.imageSize,
frame: {
width: quickEditSize.width,
height: quickEditSize.height,
},
}),
composerOpen: true,
});
setQuickEditPanel(null);
const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, {
initialTool: null,
});
@@ -917,10 +936,9 @@ export function useImageCanvasGenerationWorkflow({
},
[
canvasSize,
openPlacedCanvasGenerationDialog,
selectSingleLayer,
archiveActiveCanvasGenerationDialog,
setActiveTool,
setGenerateDialog,
setImageContextMenu,
setMetadataLayer,
setViewport,
@@ -973,6 +991,17 @@ export function useImageCanvasGenerationWorkflow({
setActiveTool('music');
return;
}
if (sourceLayer.mediaType === 'video') {
const videoDraft = createVideoRedrawGenerationDialogDraft(sourceLayer);
if (!videoDraft) {
return;
}
setQuickEditPanel(null);
selectSingleLayer(sourceLayer.id);
openCanvasGenerationDialog(videoDraft);
setActiveTool('video');
return;
}
if (
sourceLayer.mediaType === undefined ||
sourceLayer.mediaType === 'image'
@@ -1002,17 +1031,12 @@ export function useImageCanvasGenerationWorkflow({
setActiveTool('generate');
return;
}
setGenerateDialog(null);
setQuickEditPanel(createRedrawPanelDraft(sourceLayer));
selectSingleLayer(sourceLayer.id);
setActiveTool('generate');
},
[
openCanvasGenerationDialog,
openPlacedCanvasGenerationDialog,
selectSingleLayer,
setActiveTool,
setGenerateDialog,
setImageContextMenu,
setMetadataLayer,
],
@@ -1275,13 +1299,14 @@ export function useImageCanvasGenerationWorkflow({
const pickQuickEditReferenceFromLayer = useCallback(
(layer: CanvasLayer) => {
setQuickEditPanel((currentPanel) =>
appendQuickEditReference(currentPanel, layer),
setGenerateDialog((currentDialog) =>
appendGenerationReference(currentDialog, layer),
);
setIsPickingQuickEditReferenceFromCanvas(false);
setIsGenerationReferenceMenuOpen(false);
setImageContextMenu(null);
},
[setImageContextMenu],
[setGenerateDialog, setImageContextMenu],
);
const pickCharacterReferenceFromLayer = useCallback(
@@ -1626,31 +1651,39 @@ export function useImageCanvasGenerationWorkflow({
draftMark: null,
marks: [...currentSelectionState.marks, normalizedMark],
}));
setQuickEditPanel((currentPanel) =>
currentPanel
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'quick-edit' &&
currentDialog.sourceLayerId === currentSelectionState.sourceLayerId
? {
...currentPanel,
...currentDialog,
status:
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
currentDialog.status === 'failed' ? 'idle' : currentDialog.status,
errorMessage:
currentPanel.status === 'failed'
currentDialog.status === 'failed'
? undefined
: currentPanel.errorMessage,
: currentDialog.errorMessage,
prompt: appendQuickEditSelectionPrompt(
currentPanel.prompt,
currentDialog.prompt,
nextMarkNumber,
),
composerOpen: true,
}
: currentPanel,
: currentDialog,
);
}, [updateQuickEditSelectionState]);
}, [setGenerateDialog, updateQuickEditSelectionState]);
useEffect(() => {
if (!quickEditPanel && quickEditSelectionState) {
if (!quickEditSelectionState) {
return;
}
const hasActiveQuickEditDialog =
generateDialog?.mode === 'quick-edit' &&
generateDialog.sourceLayerId === quickEditSelectionState.sourceLayerId;
if (!hasActiveQuickEditDialog && !quickEditPanel) {
quickEditSelectionStateRef.current = null;
setQuickEditSelectionState(null);
}
}, [quickEditPanel, quickEditSelectionState]);
}, [generateDialog, quickEditPanel, quickEditSelectionState]);
const cancelUiAssetExtraction = useCallback(() => {
setUiAssetExtractionState(null);