固定生成规范参数并支持删除生成中占位
生成规范类图片固定提交 16:9、2K 和 gpt-image-2,并在面板底部只读展示。 生成中占位支持通过 Delete 或 Backspace 删除,不新增可见删除按钮。 异步生成回写前按生成对象 ID 校验,避免已删除占位重新落回画布。 更新编辑器生成面板与图片画布技术文档及相关测试。
This commit is contained in:
@@ -224,6 +224,8 @@ export function ImageCanvasEditorView() {
|
||||
canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
activateCanvasGenerationDialog,
|
||||
restoreCanvasGenerationDialogs,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
@@ -456,6 +458,7 @@ export function ImageCanvasEditorView() {
|
||||
canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
@@ -615,6 +618,11 @@ export function ImageCanvasEditorView() {
|
||||
redoCanvasChange,
|
||||
undoCanvasChange,
|
||||
deleteLayerById: deleteLayerByIdFromShortcut,
|
||||
removeCanvasGenerationDialogById: (dialogId: string) => {
|
||||
captureCanvasHistory();
|
||||
removeCanvasGenerationDialogById(dialogId);
|
||||
setActiveTool('select');
|
||||
},
|
||||
setActiveTool,
|
||||
setGenerateDialog,
|
||||
setImageContextMenu,
|
||||
|
||||
@@ -46,6 +46,9 @@ export const UI_DESIGN_FRAME_DISPLAY_SIZE = { width: 560, height: 315 };
|
||||
export const IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2';
|
||||
export const IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
|
||||
export const DEFAULT_IMAGE_MODEL = IMAGE_MODEL_NANOBANANA2;
|
||||
export const SPEC_GENERATION_MODEL = IMAGE_MODEL_GPT_IMAGE_2;
|
||||
export const SPEC_GENERATION_ASPECT_RATIO = '16:9';
|
||||
export const SPEC_GENERATION_IMAGE_SIZE = '2K';
|
||||
export const ICON_DESCRIPTION_LIMIT = 100;
|
||||
// 图标素材面板按描述项扩宽,避免在画布子面板里做滑动列表。
|
||||
export const ICON_DESCRIPTION_CARD_WIDTH_REM = 8.4;
|
||||
|
||||
@@ -116,6 +116,9 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
|
||||
normalizedPrompt: 'AI 生成图片',
|
||||
input: {
|
||||
size: '2048x1152',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
kind: 'spec',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
prompt: expect.stringContaining('参考图生成规范'),
|
||||
|
||||
@@ -20,6 +20,9 @@ import {
|
||||
DEFAULT_VIDEO_MODEL,
|
||||
EDITOR_GENERATION_MUD_POINT_CONFIG,
|
||||
SEEDANCE_VIDEO_REFERENCE_LIMITS,
|
||||
SPEC_GENERATION_ASPECT_RATIO,
|
||||
SPEC_GENERATION_IMAGE_SIZE,
|
||||
SPEC_GENERATION_MODEL,
|
||||
SPEC_GENERATION_SIZE,
|
||||
SPEC_TYPE_LABEL,
|
||||
buildIconGenerationInputs,
|
||||
@@ -210,7 +213,9 @@ export function buildImageGenerationSubmissionPlan({
|
||||
Boolean(dialog.specReference?.src),
|
||||
),
|
||||
size: SPEC_GENERATION_SIZE,
|
||||
model: DEFAULT_IMAGE_MODEL,
|
||||
model: SPEC_GENERATION_MODEL,
|
||||
aspectRatio: SPEC_GENERATION_ASPECT_RATIO,
|
||||
imageSize: SPEC_GENERATION_IMAGE_SIZE,
|
||||
kind: 'spec',
|
||||
...(dialog.specReference?.src
|
||||
? { referenceImageSrcs: [dialog.specReference.src] }
|
||||
|
||||
@@ -89,6 +89,15 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
).toBe(true);
|
||||
const submitButton = screen.getByRole('button', { name: '提交生成规范' });
|
||||
expect(submitButton.textContent).toBe('生成5泥点');
|
||||
expect(screen.getByLabelText('生成规范固定参数').textContent).toBe(
|
||||
'16:92Kgpt-image-2',
|
||||
);
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /生成图片尺寸/u }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /生成图片模型/u }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('renders UI design as a single borderless prompt below the first reference row', () => {
|
||||
|
||||
@@ -29,7 +29,10 @@ import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
||||
import {
|
||||
CHARACTER_SPEC_VIEW_OPTIONS,
|
||||
EDITOR_GENERATION_MUD_POINT_CONFIG,
|
||||
SPEC_GENERATION_ASPECT_RATIO,
|
||||
SPEC_GENERATION_COST,
|
||||
SPEC_GENERATION_IMAGE_SIZE,
|
||||
SPEC_GENERATION_MODEL,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
||||
|
||||
@@ -82,6 +85,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
const reference = isUiDesignDialog
|
||||
? dialog.uiDesignSpecReference
|
||||
: dialog.specReference;
|
||||
const isGenerating = dialog.status === 'generating';
|
||||
useImageCanvasFloatingOptionDismiss({
|
||||
isOpen: isGenerationReferenceMenuOpen,
|
||||
boundaryRefs: [generationReferenceButtonRef],
|
||||
@@ -127,7 +131,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
label={referenceSlotLabel}
|
||||
ariaLabel={referenceLabel}
|
||||
title={reference?.label ?? referenceLabel}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
isAdd={!reference}
|
||||
onClick={openReferenceMenu}
|
||||
onRemove={
|
||||
@@ -178,7 +182,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
variant="textarea"
|
||||
aria-label="UI设计要求"
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
placeholder="你希望这个 UI 长什么样?"
|
||||
size="sm"
|
||||
density="compact"
|
||||
@@ -220,7 +224,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
variant="textarea"
|
||||
aria-label="自定义规范提示词"
|
||||
value={dialog.specValues?.customPrompt ?? ''}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__spec-textarea"
|
||||
@@ -241,7 +245,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
<PlatformTextField
|
||||
aria-label="玩法设定"
|
||||
value={dialog.specValues?.playSetting ?? ''}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__spec-input"
|
||||
@@ -260,7 +264,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
<PlatformTextField
|
||||
aria-label="美术风格"
|
||||
value={dialog.specValues?.artStyle ?? ''}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__spec-input"
|
||||
@@ -281,7 +285,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
<PlatformSelectField
|
||||
aria-label="头身比"
|
||||
value={dialog.specValues?.bodyRatio ?? '3'}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__spec-input"
|
||||
@@ -306,7 +310,7 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
<PlatformSelectField
|
||||
aria-label="角色视角"
|
||||
value={dialog.specValues?.characterView ?? ''}
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__spec-input"
|
||||
@@ -353,26 +357,33 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<span aria-hidden="true" />
|
||||
<PlatformActionButton
|
||||
<div
|
||||
className="image-canvas-editor__readonly-generation-options"
|
||||
aria-label="生成规范固定参数"
|
||||
>
|
||||
<span>{SPEC_GENERATION_ASPECT_RATIO}</span>
|
||||
<span>{SPEC_GENERATION_IMAGE_SIZE}</span>
|
||||
<span>{SPEC_GENERATION_MODEL}</span>
|
||||
</div>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__generation-submit image-canvas-editor__spec-submit"
|
||||
disabled={dialog.status === 'generating'}
|
||||
disabled={isGenerating}
|
||||
aria-label="提交生成规范"
|
||||
>
|
||||
{dialog.status === 'generating' ? (
|
||||
'生成中'
|
||||
) : (
|
||||
<>
|
||||
<span>生成</span>
|
||||
<span className="image-canvas-editor__mud-point-inline">
|
||||
{SPEC_GENERATION_COST}泥点
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{isGenerating ? (
|
||||
'生成中'
|
||||
) : (
|
||||
<>
|
||||
<span>生成</span>
|
||||
<span className="image-canvas-editor__mud-point-inline">
|
||||
{SPEC_GENERATION_COST}泥点
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</PlatformActionButton>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -100,6 +100,14 @@ export function useCanvasGenerationDialogs({
|
||||
[updateCanvasGenerationDialogById],
|
||||
);
|
||||
|
||||
const hasCanvasGenerationDialogById = useCallback((dialogId: string) => {
|
||||
const currentDialog = generateDialogRef.current;
|
||||
return [
|
||||
...(isCanvasGenerationDialog(currentDialog) ? [currentDialog] : []),
|
||||
...inactiveGenerateDialogsRef.current,
|
||||
].some((dialog) => dialog.id === dialogId);
|
||||
}, []);
|
||||
|
||||
const activateCanvasGenerationDialog = useCallback(
|
||||
(targetDialog: CanvasGenerationDialogState) => {
|
||||
setInactiveGenerateDialogs((currentDialogs) => {
|
||||
@@ -216,6 +224,7 @@ export function useCanvasGenerationDialogs({
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
activateCanvasGenerationDialog,
|
||||
restoreCanvasGenerationDialogs,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
|
||||
@@ -133,6 +133,7 @@ function SubmissionWorkflowHarness({
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog: dialogs.setGenerateDialog,
|
||||
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById,
|
||||
getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources: (nextLayers) =>
|
||||
setLayers((currentLayers) => [...currentLayers, ...nextLayers]),
|
||||
|
||||
@@ -69,6 +69,7 @@ type GenerationSubmissionWorkflowOptions = {
|
||||
dialogId: string,
|
||||
updater: CanvasGenerationDialogUpdater,
|
||||
) => void;
|
||||
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
|
||||
getGeneratingDialogPlaceholder: (
|
||||
dialog: GenerateDialogState,
|
||||
) => GenerateDialogState['placeholder'];
|
||||
@@ -94,6 +95,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
selectSingleLayer,
|
||||
@@ -127,6 +129,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
generationInputs?: CanvasGenerationInputs;
|
||||
} = {},
|
||||
) => {
|
||||
if (options.dialogId && !hasCanvasGenerationDialogById(options.dialogId)) {
|
||||
return;
|
||||
}
|
||||
layerCounterRef.current += 1;
|
||||
const generatedIndex = layerCounterRef.current;
|
||||
const nextLayer = createGeneratedResultLayer({
|
||||
@@ -170,6 +175,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setActiveTool,
|
||||
setGenerateDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
@@ -219,6 +225,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
frame?: GenerateDialogState['placeholder'],
|
||||
dialogId?: string,
|
||||
) => {
|
||||
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
||||
return;
|
||||
}
|
||||
const startIndex = layerCounterRef.current + 1;
|
||||
const nextLayers = createIconSpritesheetResultLayers({
|
||||
generated,
|
||||
@@ -256,6 +265,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setActiveSidebarPanel,
|
||||
setActiveTool,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
@@ -268,6 +278,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
frame?: GenerateDialogState['placeholder'],
|
||||
dialogId?: string,
|
||||
) => {
|
||||
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
||||
return;
|
||||
}
|
||||
layerCounterRef.current += 1;
|
||||
const generatedIndex = layerCounterRef.current;
|
||||
const nextLayer = createVideoResultLayer({
|
||||
@@ -302,6 +315,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setActiveSidebarPanel,
|
||||
setActiveTool,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
@@ -314,6 +328,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
frame?: GenerateDialogState['placeholder'],
|
||||
dialogId?: string,
|
||||
) => {
|
||||
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
||||
return;
|
||||
}
|
||||
layerCounterRef.current += 1;
|
||||
const generatedIndex = layerCounterRef.current;
|
||||
const nextLayer = createAudioResultLayer({
|
||||
@@ -348,6 +365,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
setActiveSidebarPanel,
|
||||
setActiveTool,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -90,6 +90,7 @@ function GenerationSurfaceHarness() {
|
||||
canvasGenerationDialogs: dialogs.canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId:
|
||||
dialogs.removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder,
|
||||
|
||||
@@ -64,6 +64,7 @@ type ImageCanvasGenerationSurfaceOptions = {
|
||||
dialogId: string,
|
||||
updater: CanvasGenerationDialogUpdater,
|
||||
) => void;
|
||||
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
|
||||
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
|
||||
getGeneratingDialogPlaceholder: (
|
||||
dialog: GenerateDialogState,
|
||||
@@ -132,6 +133,7 @@ export function useImageCanvasGenerationSurface({
|
||||
canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
@@ -158,6 +160,7 @@ export function useImageCanvasGenerationSurface({
|
||||
setGenerateDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
|
||||
@@ -110,6 +110,7 @@ function GenerationWorkflowHarness({
|
||||
setGenerateDialog: dialogs.setGenerateDialog,
|
||||
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId:
|
||||
dialogs.removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder,
|
||||
@@ -459,7 +460,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect.objectContaining({
|
||||
size: '2048x1152',
|
||||
kind: 'spec',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
model: 'gpt-image-2',
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
prompt: expect.stringContaining('参考图生成规范'),
|
||||
}),
|
||||
|
||||
@@ -79,6 +79,7 @@ type GenerationWorkflowOptions = {
|
||||
dialogId: string,
|
||||
updater: CanvasGenerationDialogUpdater,
|
||||
) => void;
|
||||
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
|
||||
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
|
||||
getGeneratingDialogPlaceholder: (
|
||||
dialog: GenerateDialogState,
|
||||
@@ -104,6 +105,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setGenerateDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
@@ -485,6 +487,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setCharacterAnimationPanel,
|
||||
setGenerateDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
selectSingleLayer,
|
||||
|
||||
@@ -59,6 +59,7 @@ function KeyboardShortcutsHarness({
|
||||
undoCanvasChange = vi.fn(),
|
||||
redoCanvasChange = vi.fn(),
|
||||
deleteLayerById = vi.fn(),
|
||||
removeCanvasGenerationDialogById = vi.fn(),
|
||||
closeEditorChromePanels = vi.fn(),
|
||||
}: {
|
||||
selectedLayerId?: string | null;
|
||||
@@ -68,6 +69,7 @@ function KeyboardShortcutsHarness({
|
||||
undoCanvasChange?: () => void;
|
||||
redoCanvasChange?: () => void;
|
||||
deleteLayerById?: (layerId: string | null) => void;
|
||||
removeCanvasGenerationDialogById?: (dialogId: string) => void;
|
||||
closeEditorChromePanels?: () => void;
|
||||
}) {
|
||||
const [activeTool, setActiveTool] = useState<CanvasTool>(initialTool);
|
||||
@@ -108,6 +110,7 @@ function KeyboardShortcutsHarness({
|
||||
redoCanvasChange,
|
||||
undoCanvasChange,
|
||||
deleteLayerById,
|
||||
removeCanvasGenerationDialogById,
|
||||
setActiveTool: (tool) => setActiveTool(tool),
|
||||
setGenerateDialog: (updater) =>
|
||||
setGenerateDialogState((currentDialog) =>
|
||||
@@ -227,10 +230,12 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
});
|
||||
|
||||
it('removes editable generation placeholders with Backspace', () => {
|
||||
const removeCanvasGenerationDialogById = vi.fn();
|
||||
render(
|
||||
<KeyboardShortcutsHarness
|
||||
initialTool="character"
|
||||
initialGenerateDialog={createPlaceholderDialog('character')}
|
||||
removeCanvasGenerationDialogById={removeCanvasGenerationDialogById}
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -238,7 +243,10 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
fireEvent.keyDown(window, { key: 'Backspace', code: 'Backspace' });
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('generate-dialog').textContent).toBe('none');
|
||||
expect(removeCanvasGenerationDialogById).toHaveBeenCalledWith('dialog-1');
|
||||
expect(screen.getByTestId('generate-dialog').textContent).toBe(
|
||||
'character:idle:true',
|
||||
);
|
||||
expect(screen.getByTestId('active-tool').textContent).toBe('select');
|
||||
expect(screen.getByTestId('character-menu').textContent).toBe('false');
|
||||
expect(screen.getByTestId('character-picking').textContent).toBe('false');
|
||||
@@ -246,6 +254,26 @@ describe('useImageCanvasKeyboardShortcuts', () => {
|
||||
expect(screen.getByTestId('icon-picking').textContent).toBe('false');
|
||||
});
|
||||
|
||||
it('removes generating placeholders with Delete', () => {
|
||||
const removeCanvasGenerationDialogById = vi.fn();
|
||||
const generatingDialog = createPlaceholderDialog('generate');
|
||||
generatingDialog.status = 'generating';
|
||||
render(
|
||||
<KeyboardShortcutsHarness
|
||||
initialTool="generate"
|
||||
initialGenerateDialog={generatingDialog}
|
||||
removeCanvasGenerationDialogById={removeCanvasGenerationDialogById}
|
||||
/>,
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.keyDown(window, { key: 'Delete', code: 'Delete' });
|
||||
});
|
||||
|
||||
expect(removeCanvasGenerationDialogById).toHaveBeenCalledWith('dialog-1');
|
||||
expect(screen.getByTestId('active-tool').textContent).toBe('select');
|
||||
});
|
||||
|
||||
it('closes transient editor panels with Escape and collapses idle composers', () => {
|
||||
const closeEditorChromePanels = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type RefObject, useEffect } from 'react';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
GenerateDialogState,
|
||||
QuickEditPanelState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
@@ -11,6 +12,7 @@ type UseImageCanvasKeyboardShortcutsOptions = {
|
||||
redoCanvasChange: () => void;
|
||||
undoCanvasChange: () => void;
|
||||
deleteLayerById: (layerId: string | null) => void;
|
||||
removeCanvasGenerationDialogById?: (dialogId: string) => void;
|
||||
setActiveTool: (tool: 'select') => void;
|
||||
setGenerateDialog: (
|
||||
updater:
|
||||
@@ -61,7 +63,6 @@ function isCanvasGenerationPlaceholderDialog(
|
||||
) {
|
||||
return (
|
||||
Boolean(dialog?.placeholder) &&
|
||||
dialog?.status !== 'generating' &&
|
||||
(dialog?.mode === 'generate' ||
|
||||
dialog?.mode === 'spec' ||
|
||||
dialog?.mode === 'character' ||
|
||||
@@ -71,12 +72,19 @@ function isCanvasGenerationPlaceholderDialog(
|
||||
);
|
||||
}
|
||||
|
||||
function isCanvasGenerationDialogState(
|
||||
dialog: GenerateDialogState | null,
|
||||
): dialog is CanvasGenerationDialogState {
|
||||
return Boolean(dialog?.id && isCanvasGenerationPlaceholderDialog(dialog));
|
||||
}
|
||||
|
||||
export function useImageCanvasKeyboardShortcuts({
|
||||
generateDialogRef,
|
||||
selectedLayerIdRef,
|
||||
redoCanvasChange,
|
||||
undoCanvasChange,
|
||||
deleteLayerById,
|
||||
removeCanvasGenerationDialogById,
|
||||
setActiveTool,
|
||||
setGenerateDialog,
|
||||
setImageContextMenu,
|
||||
@@ -171,7 +179,15 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
}
|
||||
if (isCanvasGenerationPlaceholderDialog(generateDialogRef.current)) {
|
||||
event.preventDefault();
|
||||
setGenerateDialog(null);
|
||||
const currentDialog = generateDialogRef.current;
|
||||
if (
|
||||
isCanvasGenerationDialogState(currentDialog) &&
|
||||
removeCanvasGenerationDialogById
|
||||
) {
|
||||
removeCanvasGenerationDialogById(currentDialog.id);
|
||||
} else {
|
||||
setGenerateDialog(null);
|
||||
}
|
||||
setActiveTool('select');
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setIsPickingGenerationReferenceFromCanvas(false);
|
||||
@@ -217,6 +233,7 @@ export function useImageCanvasKeyboardShortcuts({
|
||||
closeEditorChromePanels,
|
||||
deleteLayerById,
|
||||
generateDialogRef,
|
||||
removeCanvasGenerationDialogById,
|
||||
redoCanvasChange,
|
||||
selectedLayerIdRef,
|
||||
setActiveTool,
|
||||
|
||||
@@ -5917,6 +5917,30 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
gap: 0.24rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__readonly-generation-options {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
min-height: 2.25rem;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
justify-self: start;
|
||||
color: #475569;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 820;
|
||||
}
|
||||
|
||||
.image-canvas-editor__readonly-generation-options span {
|
||||
display: inline-flex;
|
||||
min-height: 1.72rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||||
border-radius: 999px;
|
||||
background: #f8fafc;
|
||||
padding: 0 0.58rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__context-menu {
|
||||
position: fixed;
|
||||
z-index: 35;
|
||||
@@ -6325,6 +6349,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
|
||||
.image-canvas-editor__option-cluster--dimensions,
|
||||
.image-canvas-editor__option-cluster--model,
|
||||
.image-canvas-editor__readonly-generation-options,
|
||||
.image-canvas-editor__generation-submit,
|
||||
.image-canvas-editor__character-animation-submit {
|
||||
grid-column: auto;
|
||||
@@ -6337,6 +6362,10 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.image-canvas-editor__readonly-generation-options {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.image-canvas-editor__character-composer {
|
||||
position: fixed;
|
||||
left: 0.75rem !important;
|
||||
|
||||
Reference in New Issue
Block a user