修复图片画布新增素材持久化

新增画布图层资源创建后的即时布局保存
补充素材库图片加入画布的持久化回归测试
更新图片画布回归验证记录
This commit is contained in:
2026-06-17 04:42:09 +08:00
parent b5cbe62b47
commit f794a8dd1f
4 changed files with 276 additions and 132 deletions

View File

@@ -242,26 +242,26 @@ describe('ImageCanvasEditorView', () => {
beforeEach(() => {
loadOrCreateRecentEditorProjectMock.mockImplementation(() =>
immediateAsync({
projectId: 'editor-project-default',
title: '默认项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: defaultEditorProjectLayers,
resources: defaultEditorProjectResources,
updatedAt: '2026-06-12T00:00:00.000Z',
projectId: 'editor-project-default',
title: '默认项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: defaultEditorProjectLayers,
resources: defaultEditorProjectResources,
updatedAt: '2026-06-12T00:00:00.000Z',
}),
);
loadEditorAssetLibraryMock.mockImplementation(() =>
immediateAsync({
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: defaultEditorAssetLibraryAssets,
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: defaultEditorAssetLibraryAssets,
}),
);
createEditorAssetMock.mockImplementation(async (input) => ({
@@ -378,7 +378,9 @@ describe('ImageCanvasEditorView', () => {
it('shows the loaded project title and a topbar entry back to projects', async () => {
render(<ImageCanvasEditorView />);
expect(await screen.findByRole('heading', { name: '默认项目' })).toBeTruthy();
expect(
await screen.findByRole('heading', { name: '默认项目' }),
).toBeTruthy();
const projectLink = screen.getByRole('link', { name: '返回项目页面' });
expect(projectLink.getAttribute('href')).toBe('/project');
@@ -422,7 +424,9 @@ describe('ImageCanvasEditorView', () => {
'新画布项目',
);
});
expect(await screen.findByRole('heading', { name: '新画布项目' })).toBeTruthy();
expect(
await screen.findByRole('heading', { name: '新画布项目' }),
).toBeTruthy();
});
it('does not inject built-in mock assets when the persisted library is empty', async () => {
@@ -974,7 +978,9 @@ describe('ImageCanvasEditorView', () => {
expect(openLoginModal).toHaveBeenCalledTimes(1);
expect(createEditorAssetMock).not.toHaveBeenCalled();
expect(screen.queryByRole('button', { name: '上传失败登录后上传.png' })).toBeNull();
expect(
screen.queryByRole('button', { name: '上传失败登录后上传.png' }),
).toBeNull();
const resumeUpload = openLoginModal.mock.calls[0]?.[0];
expect(typeof resumeUpload).toBe('function');
@@ -1027,7 +1033,9 @@ describe('ImageCanvasEditorView', () => {
expect(
await screen.findByLabelText('素材素材上传进度.png上传进度'),
).toBeTruthy();
expect(screen.getByRole('button', { name: '上传中素材上传进度.png' })).toBeTruthy();
expect(
screen.getByRole('button', { name: '上传中素材上传进度.png' }),
).toBeTruthy();
deferredAsset.resolve({
assetId: 'asset-upload-progress',
@@ -1044,9 +1052,7 @@ describe('ImageCanvasEditorView', () => {
screen.getByRole('button', { name: '添加素材上传进度.png' }),
).toBeTruthy();
});
expect(
screen.queryByLabelText('素材素材上传进度.png上传进度'),
).toBeNull();
expect(screen.queryByLabelText('素材素材上传进度.png上传进度')).toBeNull();
});
it('opens login when asset creation returns unauthorized during upload', async () => {
@@ -1212,6 +1218,69 @@ describe('ImageCanvasEditorView', () => {
expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-b');
});
it('saves a library asset layer right after creating its canvas resource', async () => {
const user = userEvent.setup();
createEditorProjectResourceMock.mockResolvedValueOnce({
resourceId: 'resource-added-asset-a',
projectId: 'editor-project-default',
imageSrc: 'data:image/png;base64,YQ==',
width: 320,
height: 240,
sourceType: 'uploaded',
});
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({
projectId: 'editor-project-default',
title: '空画布项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [],
resources: [],
updatedAt: '2026-06-12T00:00:00.000Z',
});
loadEditorAssetLibraryMock.mockResolvedValueOnce({
folders: [
{
folderId: 'project',
label: '项目素材',
sortOrder: 0,
collapsed: false,
systemDefault: true,
},
],
assets: [
{
assetId: 'asset-a',
folderId: 'project',
label: '账号素材A',
imageSrc: 'data:image/png;base64,YQ==',
width: 320,
height: 240,
sourceType: 'uploaded',
},
],
});
render(<ImageCanvasEditorView />);
await user.click(
await screen.findByRole('button', { name: '添加账号素材A' }),
);
expect(await screen.findByAltText('画布图片账号素材A')).toBeTruthy();
await waitFor(() => {
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
'editor-project-default',
expect.objectContaining({
layers: expect.arrayContaining([
expect.objectContaining({
title: '账号素材A',
resourceId: 'resource-added-asset-a',
sourceAssetId: 'asset-a',
}),
]),
}),
);
});
});
it('selects multiple assets with a marquee in asset selection mode', async () => {
const user = userEvent.setup();
loadEditorAssetLibraryMock.mockResolvedValueOnce({
@@ -1601,8 +1670,11 @@ describe('ImageCanvasEditorView', () => {
const menu = screen.getByRole('menu', { name: '画布右键菜单' });
expect(
(within(menu).getByRole('menuitem', { name: '粘贴' }) as HTMLButtonElement)
.disabled,
(
within(menu).getByRole('menuitem', {
name: '粘贴',
}) as HTMLButtonElement
).disabled,
).toBe(true);
expect(within(menu).getByRole('menuitem', { name: '放大' })).toBeTruthy();
expect(
@@ -1656,11 +1728,15 @@ describe('ImageCanvasEditorView', () => {
});
const copyPasteMenu = screen.getByRole('menu', { name: '画布右键菜单' });
expect(
(within(copyPasteMenu).getByRole('menuitem', {
name: '粘贴',
}) as HTMLButtonElement).disabled,
(
within(copyPasteMenu).getByRole('menuitem', {
name: '粘贴',
}) as HTMLButtonElement
).disabled,
).toBe(false);
fireEvent.click(within(copyPasteMenu).getByRole('menuitem', { name: '粘贴' }));
fireEvent.click(
within(copyPasteMenu).getByRole('menuitem', { name: '粘贴' }),
);
expect(screen.getAllByAltText(//u)).toHaveLength(2);
fireEvent.contextMenu(
@@ -1885,7 +1961,9 @@ describe('ImageCanvasEditorView', () => {
).toBeTruthy();
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至100%' }));
expect(screen.getByRole('button', { name: / \d+%/u })).toBeTruthy();
expect(
screen.getByRole('button', { name: / \d+%/u }),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '当前缩放比例 100%' }));
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至50%' }));
@@ -1915,25 +1993,23 @@ describe('ImageCanvasEditorView', () => {
});
expect(within(settingsPanel).getByText('画布背景')).toBeTruthy();
fireEvent.click(within(settingsPanel).getByRole('button', { name: '暖灰' }));
fireEvent.click(
within(settingsPanel).getByRole('button', { name: '暖灰' }),
);
expect((viewport as HTMLElement).style.backgroundColor).toBe(
'rgb(243, 240, 234)',
);
fireEvent.change(
within(settingsPanel).getByLabelText('自定义画布背景色'),
{
target: { value: '#ffffff' },
},
);
fireEvent.change(within(settingsPanel).getByLabelText('自定义画布背景色'), {
target: { value: '#ffffff' },
});
expect((viewport as HTMLElement).style.backgroundColor).toBe(
'rgb(255, 255, 255)',
);
const hexInput = within(settingsPanel).getByLabelText(
'画布背景十六进制颜色',
);
const hexInput =
within(settingsPanel).getByLabelText('画布背景十六进制颜色');
fireEvent.change(hexInput, { target: { value: '#abc' } });
expect((hexInput as HTMLInputElement).value).toBe('#aabbcc');
expect((viewport as HTMLElement).style.backgroundColor).toBe(
@@ -1954,9 +2030,7 @@ describe('ImageCanvasEditorView', () => {
);
fireEvent.keyDown(window, { key: 'Escape' });
expect(
screen.queryByRole('dialog', { name: '画布背景设置' }),
).toBeNull();
expect(screen.queryByRole('dialog', { name: '画布背景设置' })).toBeNull();
fireEvent.click(
within(panelToolbar).getByRole('button', { name: '切换小地图' }),
@@ -2927,7 +3001,9 @@ describe('ImageCanvasEditorView', () => {
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
target: { value: '高个子游侠' },
});
fireEvent.click(within(characterPanel).getByRole('button', { name: '生成' }));
fireEvent.click(
within(characterPanel).getByRole('button', { name: '生成' }),
);
await waitFor(() => {
expect(generateEditorImageMock).toHaveBeenCalledWith(
@@ -2937,9 +3013,7 @@ describe('ImageCanvasEditorView', () => {
}),
);
});
expect(
generateEditorImageMock.mock.calls[0]?.[0],
).not.toEqual(
expect(generateEditorImageMock.mock.calls[0]?.[0]).not.toEqual(
expect.objectContaining({
aspectRatio: expect.any(String),
imageSize: expect.any(String),
@@ -3019,12 +3093,16 @@ describe('ImageCanvasEditorView', () => {
const characterFrame = screen.getByLabelText('角色生成占位图');
expect(characterFrame).toBeTruthy();
dispatchPointerEvent(screen.getByLabelText('图像生成占位图'), 'pointerdown', {
button: 0,
pointerId: 1702,
clientX: 500,
clientY: 260,
});
dispatchPointerEvent(
screen.getByLabelText('图像生成占位图'),
'pointerdown',
{
button: 0,
pointerId: 1702,
clientX: 500,
clientY: 260,
},
);
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', {
pointerId: 1702,
clientX: 650,
@@ -3036,9 +3114,7 @@ describe('ImageCanvasEditorView', () => {
clientY: 390,
});
const movedFrame = screen.getByLabelText('图像生成占位图');
const movedLeft = Number.parseFloat(
(movedFrame as HTMLElement).style.left,
);
const movedLeft = Number.parseFloat((movedFrame as HTMLElement).style.left);
const movedTop = Number.parseFloat((movedFrame as HTMLElement).style.top);
expect(movedLeft).toBeGreaterThan(originalLeft);
expect(movedTop).toBeGreaterThan(originalTop);
@@ -3077,9 +3153,13 @@ describe('ImageCanvasEditorView', () => {
.getByAltText(/画布图片:生成图片/)
.closest('button') as HTMLElement;
const expectedLayerLeft =
movedLeft + Number.parseFloat((movedFrame as HTMLElement).style.width) / 2 - 512;
movedLeft +
Number.parseFloat((movedFrame as HTMLElement).style.width) / 2 -
512;
const expectedLayerTop =
movedTop + Number.parseFloat((movedFrame as HTMLElement).style.height) / 2 - 512;
movedTop +
Number.parseFloat((movedFrame as HTMLElement).style.height) / 2 -
512;
expect(Number.parseFloat(generatedLayer.style.left)).toBeCloseTo(
expectedLayerLeft,
1,
@@ -3160,7 +3240,9 @@ describe('ImageCanvasEditorView', () => {
iconPanel.querySelector('.image-canvas-editor__icon-spec-card'),
).toBeTruthy();
fireEvent.click(within(iconPanel).getByRole('button', { name: '添加素材描述' }));
fireEvent.click(
within(iconPanel).getByRole('button', { name: '添加素材描述' }),
);
expect(Number.parseFloat(iconPanel.style.width)).toBeCloseTo(61.2, 1);
expect(within(iconPanel).getAllByRole('textbox')).toHaveLength(7);
@@ -4358,12 +4440,16 @@ describe('ImageCanvasEditorView', () => {
});
expect(within(editedMetadataDialog).queryByText('Prompt')).toBeNull();
expect(within(editedMetadataDialog).getByText('修改要求')).toBeTruthy();
expect(within(editedMetadataDialog).getByText('把画面改成黄昏光线')).toBeTruthy();
expect(
within(editedMetadataDialog).getByText('把画面改成黄昏光线'),
).toBeTruthy();
expect(within(editedMetadataDialog).getByText('参考图')).toBeTruthy();
expect(
within(editedMetadataDialog).getByText(/^ \d+$/u),
).toBeTruthy();
expect(screen.getByRole('button', { name: / \d+%/u })).toBeTruthy();
expect(
screen.getByRole('button', { name: / \d+%/u }),
).toBeTruthy();
});
it('hides the edit image panel after generation starts while keeping the source preview visible', async () => {

View File

@@ -1,10 +1,4 @@
import {
Check,
ChevronLeft,
Download,
Pencil,
X,
} from 'lucide-react';
import { Check, ChevronLeft, Download, Pencil, X } from 'lucide-react';
import JSZip from 'jszip';
import {
type CSSProperties,
@@ -272,7 +266,10 @@ export function ImageCanvasEditorView() {
const pendingProjectResourceLayersRef = useRef<
Array<{
layer: CanvasLayer;
options: { onCreated?: (resourceId: string) => void };
options: {
onCreated?: (resourceId: string) => void;
snapshotLayers?: CanvasLayer[];
};
}>
>([]);
const selectedLayerIdRef = useRef<string | null>(null);
@@ -373,8 +370,9 @@ export function ImageCanvasEditorView() {
useState(false);
const [imageContextMenu, setImageContextMenu] =
useState<ImageContextMenuState | null>(null);
const [contextMenu, setContextMenu] =
useState<CanvasContextMenuState | null>(null);
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
null,
);
const [canvasClipboard, setCanvasClipboard] =
useState<CanvasClipboard | null>(null);
const [historyVersion, setHistoryVersion] = useState(0);
@@ -447,12 +445,11 @@ export function ImageCanvasEditorView() {
: null,
[activeCanvasGenerationDialog, layers],
);
const generationAnchor =
activeCanvasGenerationDialog
? (activeGenerationLayer ??
activeCanvasGenerationDialog.placeholder ??
null)
: null;
const generationAnchor = activeCanvasGenerationDialog
? (activeGenerationLayer ??
activeCanvasGenerationDialog.placeholder ??
null)
: null;
const generationComposerStyle =
activeCanvasGenerationDialog?.status !== 'generating' &&
activeCanvasGenerationDialog?.composerOpen !== false &&
@@ -630,8 +627,7 @@ export function ImageCanvasEditorView() {
) => CanvasGenerationDialogState | null,
) => {
setGenerateDialog((currentDialog) =>
isCanvasGenerationDialog(currentDialog) &&
currentDialog.id === dialogId
isCanvasGenerationDialog(currentDialog) && currentDialog.id === dialogId
? updater(currentDialog)
: currentDialog,
);
@@ -707,7 +703,9 @@ export function ImageCanvasEditorView() {
inactiveGenerateDialogs: inactiveGenerateDialogsRef.current.map(
(dialog) => ({
...dialog,
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
placeholder: dialog.placeholder
? { ...dialog.placeholder }
: undefined,
}),
),
selectedLayerId: selectedLayerIdRef.current,
@@ -733,7 +731,9 @@ export function ImageCanvasEditorView() {
setInactiveGenerateDialogs(
snapshot.inactiveGenerateDialogs.map((dialog) => ({
...dialog,
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
placeholder: dialog.placeholder
? { ...dialog.placeholder }
: undefined,
})),
);
setSelectedLayerId(snapshot.selectedLayerId);
@@ -859,7 +859,10 @@ export function ImageCanvasEditorView() {
const createProjectResourceForLayer = useCallback(
(
layer: CanvasLayer,
options: { onCreated?: (resourceId: string) => void } = {},
options: {
onCreated?: (resourceId: string) => void;
snapshotLayers?: CanvasLayer[];
} = {},
) => {
const readyProjectId = projectIdRef.current;
if (!readyProjectId) {
@@ -882,16 +885,40 @@ export function ImageCanvasEditorView() {
})
.then((resource) => {
options.onCreated?.(resource.resourceId);
setLayers((currentLayers) =>
currentLayers.map((currentLayer) =>
currentLayer.id === layer.id
? {
...currentLayer,
resourceId: resource.resourceId,
}
: currentLayer,
),
);
const layerWithResourceId = {
...layer,
resourceId: resource.resourceId,
};
const currentLayers = layersRef.current;
const nextLayers = currentLayers.some(
(currentLayer) => currentLayer.id === layer.id,
)
? currentLayers.map((currentLayer) =>
currentLayer.id === layer.id
? layerWithResourceId
: currentLayer,
)
: options.snapshotLayers?.some(
(snapshotLayer) => snapshotLayer.id === layer.id,
)
? options.snapshotLayers.map((snapshotLayer) =>
snapshotLayer.id === layer.id
? layerWithResourceId
: snapshotLayer,
)
: currentLayers;
layersRef.current = nextLayers;
setLayers(nextLayers);
if (nextLayers.length) {
void saveEditorProjectLayout(readyProjectId, {
viewport: viewportRef.current,
layers: nextLayers.map(serializeLayer),
}).catch((error: unknown) => {
if (isEditorAuthError(error)) {
openEditorLoginModal();
}
});
}
})
.catch((error: unknown) => {
if (isEditorAuthError(error)) {
@@ -1019,8 +1046,8 @@ export function ImageCanvasEditorView() {
return () => observer.disconnect();
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (
(event.ctrlKey || event.metaKey) &&
event.code === 'KeyZ' &&
@@ -1034,9 +1061,9 @@ export function ImageCanvasEditorView() {
}
return;
}
if (event.key === 'Shift') {
isShiftPressedRef.current = true;
}
if (event.key === 'Shift') {
isShiftPressedRef.current = true;
}
if (
(event.key === 'Backspace' || event.key === 'Delete') &&
!event.repeat &&
@@ -1070,9 +1097,9 @@ export function ImageCanvasEditorView() {
if (event.key === 'Escape') {
setActiveSidebarPanel(null);
setIsZoomMenuOpen(false);
setIsBackgroundSettingsOpen(false);
setIsSpecMenuOpen(false);
setImageContextMenu(null);
setIsBackgroundSettingsOpen(false);
setIsSpecMenuOpen(false);
setImageContextMenu(null);
setContextMenu(null);
setQuickEditPanel((currentPanel) =>
currentPanel?.status === 'generating' ? currentPanel : null,
@@ -1325,9 +1352,13 @@ export function ImageCanvasEditorView() {
if (!canvasClipboard?.layers.length) {
return;
}
const nextLayers = duplicateLayersToPoint(canvasClipboard.layers, canvasPoint, {
renameCopies: canvasClipboard.mode !== 'cut',
});
const nextLayers = duplicateLayersToPoint(
canvasClipboard.layers,
canvasPoint,
{
renameCopies: canvasClipboard.mode !== 'cut',
},
);
if (!nextLayers.length) {
return;
}
@@ -1352,7 +1383,9 @@ export function ImageCanvasEditorView() {
setCanvasClipboard(clipboard);
if (options.cut) {
captureCanvasHistory();
setLayers((currentLayers) => removeCanvasLayers(currentLayers, targetIds));
setLayers((currentLayers) =>
removeCanvasLayers(currentLayers, targetIds),
);
selectSingleLayer(null);
setMetadataLayer((currentLayer) =>
currentLayer && targetIds.includes(currentLayer.id)
@@ -1534,13 +1567,29 @@ export function ImageCanvasEditorView() {
const listRect = listElement?.getBoundingClientRect();
const headerRect = header?.getBoundingClientRect();
setPinnedAssetMoveFolderId(
listRect && headerRect &&
listRect &&
headerRect &&
(headerRect.bottom < listRect.top || headerRect.top > listRect.bottom)
? folderId
: null,
);
};
const appendCanvasLayersWithResources = useCallback(
(nextLayers: CanvasLayer[]) => {
if (!nextLayers.length) {
return;
}
const snapshotLayers = [...layersRef.current, ...nextLayers];
layersRef.current = snapshotLayers;
setLayers(snapshotLayers);
nextLayers.forEach((layer) =>
createProjectResourceForLayer(layer, { snapshotLayers }),
);
},
[createProjectResourceForLayer],
);
const addAssetLayer = (
asset: EditorAsset,
position?: { x: number; y: number },
@@ -1557,10 +1606,9 @@ export function ImageCanvasEditorView() {
},
);
captureCanvasHistory();
setLayers((currentLayers) => [...currentLayers, nextLayer]);
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
setHoveredLayerId(null);
createProjectResourceForLayer(nextLayer);
};
addAssetLayerRef.current = addAssetLayer;
@@ -1609,7 +1657,10 @@ export function ImageCanvasEditorView() {
try {
const blob = await readLayerImageBlob(layer);
const extension = getImageExtensionFromTypeOrSrc(blob.type, layer.src);
const extension = getImageExtensionFromTypeOrSrc(
blob.type,
layer.src,
);
const file = `images/${indexedFileName}.${extension}`;
imageByKey.set(key, {
key,
@@ -1872,7 +1923,8 @@ export function ImageCanvasEditorView() {
setSelectedLayerIds((currentIds) =>
currentIds.filter((layerId) =>
layers.every(
(layer) => layer.id !== layerId || !isLayerLinkedToAsset(layer, asset),
(layer) =>
layer.id !== layerId || !isLayerLinkedToAsset(layer, asset),
),
),
);
@@ -1978,7 +2030,9 @@ export function ImageCanvasEditorView() {
const deleteSelectedAssets = () => {
const ids = [...selectedAssetIds];
const deletedAssets = assets.filter((asset) => selectedAssetIds.has(asset.id));
const deletedAssets = assets.filter((asset) =>
selectedAssetIds.has(asset.id),
);
setAssets((currentAssets) =>
currentAssets.filter((asset) => !selectedAssetIds.has(asset.id)),
);
@@ -2363,7 +2417,7 @@ export function ImageCanvasEditorView() {
};
if (options.addToCanvas) {
setLayers((currentLayers) => [...currentLayers, nextLayer]);
appendCanvasLayersWithResources([nextLayer]);
}
if (options.addToCanvas) {
selectSingleLayer(nextLayer.id);
@@ -2446,10 +2500,6 @@ export function ImageCanvasEditorView() {
);
});
if (options.addToCanvas) {
createProjectResourceForLayer(nextLayer);
}
if (imageSrc) {
const uploadedImage = new Image();
uploadedImage.onload = () => {
@@ -2825,7 +2875,7 @@ export function ImageCanvasEditorView() {
generationInputs: options.generationInputs,
};
setLayers((currentLayers) => [...currentLayers, nextLayer]);
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
if (options.sourceLayer) {
@@ -2848,7 +2898,6 @@ export function ImageCanvasEditorView() {
if (options.sourceLayer) {
fitLayers([options.sourceLayer, nextLayer]);
}
createProjectResourceForLayer(nextLayer);
};
const addQuickEditResultLayer = (
@@ -2859,7 +2908,8 @@ export function ImageCanvasEditorView() {
layerCounterRef.current += 1;
const generatedIndex = layerCounterRef.current;
const originalWidth = generated.width || sourceLayer.originalWidth || 1024;
const originalHeight = generated.height || sourceLayer.originalHeight || 1024;
const originalHeight =
generated.height || sourceLayer.originalHeight || 1024;
const { width, height } = resolveLayerResolutionSize(
originalWidth,
originalHeight,
@@ -2894,13 +2944,12 @@ export function ImageCanvasEditorView() {
generationInputs,
};
setLayers((currentLayers) => [...currentLayers, nextLayer]);
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
setActiveSidebarPanel('layers');
setQuickEditPanel(null);
setActiveTool('select');
fitLayers([sourceLayer, nextLayer]);
createProjectResourceForLayer(nextLayer);
};
const addIconSpritesheetResultLayers = (
@@ -2970,14 +3019,13 @@ export function ImageCanvasEditorView() {
if (!nextLayers.length) {
return;
}
setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
appendCanvasLayersWithResources(nextLayers);
selectSingleLayer(nextLayers[0]?.id ?? null);
setActiveSidebarPanel('layers');
if (dialogId) {
removeCanvasGenerationDialogById(dialogId);
}
setActiveTool('select');
nextLayers.forEach((layer) => createProjectResourceForLayer(layer));
};
const updateIconDescription = (index: number, value: string) => {
@@ -3101,8 +3149,9 @@ export function ImageCanvasEditorView() {
});
try {
const referenceImageSrc =
await resolveEditorImageReferenceDataUrl(quickEditSourceLayer.src);
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
quickEditSourceLayer.src,
);
const generated = await generateEditorImage({
prompt: normalizedPrompt,
size: quickEditPanel.size,
@@ -3388,9 +3437,7 @@ export function ImageCanvasEditorView() {
});
};
const handleCanvasContextMenu = (
event: ReactMouseEvent<HTMLDivElement>,
) => {
const handleCanvasContextMenu = (event: ReactMouseEvent<HTMLDivElement>) => {
event.preventDefault();
event.stopPropagation();
const position = resolveContextMenuPosition(
@@ -4241,14 +4288,18 @@ export function ImageCanvasEditorView() {
setCharacterAnimationPanel={setCharacterAnimationPanel}
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
setIsPickingCharacterSpecFromCanvas={setIsPickingCharacterSpecFromCanvas}
setIsPickingCharacterSpecFromCanvas={
setIsPickingCharacterSpecFromCanvas
}
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
onOpenSpecDialog={openSpecDialog}
onRequestUpload={(target) => {
setUploadTarget(target);
uploadInputRef.current?.click();
}}
onSubmitImageGeneration={(dialog) => void submitImageGeneration(dialog)}
onSubmitImageGeneration={(dialog) =>
void submitImageGeneration(dialog)
}
onSubmitIconSpritesheetGeneration={(dialog) =>
void submitIconSpritesheetGeneration(dialog)
}
@@ -4268,9 +4319,10 @@ export function ImageCanvasEditorView() {
onUpdateSpecFormValue={updateSpecFormValue}
onUpdateIconDescription={updateIconDescription}
onAddIconDescription={addIconDescription}
onUpdateCharacterAnimationDuration={updateCharacterAnimationDuration}
onUpdateCharacterAnimationDuration={
updateCharacterAnimationDuration
}
/>
</ImageCanvasStageView>
</div>
@@ -4311,7 +4363,11 @@ export function ImageCanvasEditorView() {
key={`${reference.title}-${reference.label}-${reference.src}`}
className="image-canvas-editor__metadata-reference-card"
>
<img src={reference.src} alt="" aria-hidden="true" />
<img
src={reference.src}
alt=""
aria-hidden="true"
/>
<span className="image-canvas-editor__metadata-reference-copy">
<span className="image-canvas-editor__metadata-input-title">
{reference.title}