修复图片画布新增素材持久化
新增画布图层资源创建后的即时布局保存 补充素材库图片加入画布的持久化回归测试 更新图片画布回归验证记录
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user