修复图片画布新增素材持久化
新增画布图层资源创建后的即时布局保存 补充素材库图片加入画布的持久化回归测试 更新图片画布回归验证记录
This commit is contained in:
@@ -120,3 +120,4 @@
|
|||||||
- 2026-06-17 生成面板拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空浏览器数据后未登录刷新弹出 `账号入口`;关闭登录后 `画布背景色` 打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;点击 `生成工具` 后画布显示 `Image Generator` 占位框和 `生成图片` 跟随对话框,`AI画布工具栏` 保持可见;使用临时开发账号密码登录后上传素材成功,点击素材可添加到画布,切换 `图层` 面板可看到对应图层。
|
- 2026-06-17 生成面板拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空浏览器数据后未登录刷新弹出 `账号入口`;关闭登录后 `画布背景色` 打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;点击 `生成工具` 后画布显示 `Image Generator` 占位框和 `生成图片` 跟随对话框,`AI画布工具栏` 保持可见;使用临时开发账号密码登录后上传素材成功,点击素材可添加到画布,切换 `图层` 面板可看到对应图层。
|
||||||
- 2026-06-17 前端拆分第五阶段:新增 `ImageCanvasLayerCommandModel`,把右键图层目标解析、复制 / 粘贴 / 创建副本、层级移动、分组 / 解组、显隐、锁定、翻转和删除的数据规则从主视图抽出;主视图只保留历史、选中态、菜单关闭、元数据清理和导出下载副作用。验证命令:`npm run test -- src/components/image-editor/ImageCanvasLayerCommandModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,背景入口打开完整 `画布背景设置` 面板;登录后上传素材成功,点击素材可加入画布,图片右键打开 `图片功能面板`,创建副本、水平翻转、锁定和隐藏均生效,`AI画布工具栏` 保持可见。
|
- 2026-06-17 前端拆分第五阶段:新增 `ImageCanvasLayerCommandModel`,把右键图层目标解析、复制 / 粘贴 / 创建副本、层级移动、分组 / 解组、显隐、锁定、翻转和删除的数据规则从主视图抽出;主视图只保留历史、选中态、菜单关闭、元数据清理和导出下载副作用。验证命令:`npm run test -- src/components/image-editor/ImageCanvasLayerCommandModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`,背景入口打开完整 `画布背景设置` 面板;登录后上传素材成功,点击素材可加入画布,图片右键打开 `图片功能面板`,创建副本、水平翻转、锁定和隐藏均生效,`AI画布工具栏` 保持可见。
|
||||||
- 2026-06-17 前端拆分第六阶段:新增 `ImageCanvasInteractionModel`,把适合视图、中心缩放、普通滚轮纵向滚动、Ctrl / Cmd 滚轮缩放、坐标换算、框选命中、平移、生成占位框拖拽、图层拖拽吸附、小地图投影、小地图点击定位和小地图拖拽视图移动的纯规则从主视图抽出;主视图保留事件、pointer capture、history、生成对象回写、选中态和状态更新。验证命令:`npm run test -- src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材、画布图层、小地图和 `AI画布工具栏` 保持可见,Ctrl 滚轮从 110% 缩放到 121%,普通滚轮不改变缩放,浏览器控制台无 passive wheel 错误。
|
- 2026-06-17 前端拆分第六阶段:新增 `ImageCanvasInteractionModel`,把适合视图、中心缩放、普通滚轮纵向滚动、Ctrl / Cmd 滚轮缩放、坐标换算、框选命中、平移、生成占位框拖拽、图层拖拽吸附、小地图投影、小地图点击定位和小地图拖拽视图移动的纯规则从主视图抽出;主视图保留事件、pointer capture、history、生成对象回写、选中态和状态更新。验证命令:`npm run test -- src/components/image-editor/ImageCanvasInteractionModel.test.ts src/components/image-editor/ImageCanvasEditorModel.test.ts src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 登录态刷新后素材、画布图层、小地图和 `AI画布工具栏` 保持可见,Ctrl 滚轮从 110% 缩放到 121%,普通滚轮不改变缩放,浏览器控制台无 passive wheel 错误。
|
||||||
|
- 2026-06-17 新增素材持久化修正:素材库图片、上传到画布、生成图、修改图和图标素材加入画布时会先用当前图层快照更新本地画布,再在资源创建完成后立刻保存带真实 `resourceId` 的 layout,避免资源创建异步返回时把空 `layers` 写回工程。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录弹出 `账号入口`,登录后上传素材、点击素材加入画布并刷新,画布图片和 `AI画布工具栏` 均保持可见。
|
||||||
|
|||||||
@@ -75,6 +75,7 @@
|
|||||||
|
|
||||||
- 生成状态机模型:等生成对象归档、占位框拖拽、生成完成回写、失败恢复和 undo / redo 规则进一步稳定后,再从主视图抽出深层状态模型。
|
- 生成状态机模型:等生成对象归档、占位框拖拽、生成完成回写、失败恢复和 undo / redo 规则进一步稳定后,再从主视图抽出深层状态模型。
|
||||||
- 上传 / 素材状态模型:上传占位卡片、素材文件夹移动、账号级素材库和拖拽遮罩仍在主视图与侧栏之间协作,后续需要等上传错误恢复和批量操作规则稳定后再收口。
|
- 上传 / 素材状态模型:上传占位卡片、素材文件夹移动、账号级素材库和拖拽遮罩仍在主视图与侧栏之间协作,后续需要等上传错误恢复和批量操作规则稳定后再收口。
|
||||||
|
- 资源持久化稳定性:新增图层时先使用当前画布图层快照更新本地状态,再等待工程资源创建并即时保存带真实 `resourceId` 的 layout。后续如果继续拆上传或生成状态机,必须保留这一时序,避免 React 状态刷新和异步资源返回交错时写回空图层。
|
||||||
|
|
||||||
## 验证计划
|
## 验证计划
|
||||||
|
|
||||||
|
|||||||
@@ -378,7 +378,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
it('shows the loaded project title and a topbar entry back to projects', async () => {
|
it('shows the loaded project title and a topbar entry back to projects', async () => {
|
||||||
render(<ImageCanvasEditorView />);
|
render(<ImageCanvasEditorView />);
|
||||||
|
|
||||||
expect(await screen.findByRole('heading', { name: '默认项目' })).toBeTruthy();
|
expect(
|
||||||
|
await screen.findByRole('heading', { name: '默认项目' }),
|
||||||
|
).toBeTruthy();
|
||||||
const projectLink = screen.getByRole('link', { name: '返回项目页面' });
|
const projectLink = screen.getByRole('link', { name: '返回项目页面' });
|
||||||
|
|
||||||
expect(projectLink.getAttribute('href')).toBe('/project');
|
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 () => {
|
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(openLoginModal).toHaveBeenCalledTimes(1);
|
||||||
expect(createEditorAssetMock).not.toHaveBeenCalled();
|
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];
|
const resumeUpload = openLoginModal.mock.calls[0]?.[0];
|
||||||
expect(typeof resumeUpload).toBe('function');
|
expect(typeof resumeUpload).toBe('function');
|
||||||
@@ -1027,7 +1033,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
expect(
|
expect(
|
||||||
await screen.findByLabelText('素材素材上传进度.png上传进度'),
|
await screen.findByLabelText('素材素材上传进度.png上传进度'),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(screen.getByRole('button', { name: '上传中素材上传进度.png' })).toBeTruthy();
|
expect(
|
||||||
|
screen.getByRole('button', { name: '上传中素材上传进度.png' }),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
deferredAsset.resolve({
|
deferredAsset.resolve({
|
||||||
assetId: 'asset-upload-progress',
|
assetId: 'asset-upload-progress',
|
||||||
@@ -1044,9 +1052,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
screen.getByRole('button', { name: '添加素材上传进度.png' }),
|
screen.getByRole('button', { name: '添加素材上传进度.png' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
expect(
|
expect(screen.queryByLabelText('素材素材上传进度.png上传进度')).toBeNull();
|
||||||
screen.queryByLabelText('素材素材上传进度.png上传进度'),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens login when asset creation returns unauthorized during upload', async () => {
|
it('opens login when asset creation returns unauthorized during upload', async () => {
|
||||||
@@ -1212,6 +1218,69 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
expect(deleteEditorAssetMock).toHaveBeenCalledWith('asset-b');
|
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 () => {
|
it('selects multiple assets with a marquee in asset selection mode', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||||||
@@ -1601,8 +1670,11 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
|
|
||||||
const menu = screen.getByRole('menu', { name: '画布右键菜单' });
|
const menu = screen.getByRole('menu', { name: '画布右键菜单' });
|
||||||
expect(
|
expect(
|
||||||
(within(menu).getByRole('menuitem', { name: '粘贴' }) as HTMLButtonElement)
|
(
|
||||||
.disabled,
|
within(menu).getByRole('menuitem', {
|
||||||
|
name: '粘贴',
|
||||||
|
}) as HTMLButtonElement
|
||||||
|
).disabled,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
expect(within(menu).getByRole('menuitem', { name: '放大' })).toBeTruthy();
|
expect(within(menu).getByRole('menuitem', { name: '放大' })).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
@@ -1656,11 +1728,15 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
});
|
});
|
||||||
const copyPasteMenu = screen.getByRole('menu', { name: '画布右键菜单' });
|
const copyPasteMenu = screen.getByRole('menu', { name: '画布右键菜单' });
|
||||||
expect(
|
expect(
|
||||||
(within(copyPasteMenu).getByRole('menuitem', {
|
(
|
||||||
|
within(copyPasteMenu).getByRole('menuitem', {
|
||||||
name: '粘贴',
|
name: '粘贴',
|
||||||
}) as HTMLButtonElement).disabled,
|
}) as HTMLButtonElement
|
||||||
|
).disabled,
|
||||||
).toBe(false);
|
).toBe(false);
|
||||||
fireEvent.click(within(copyPasteMenu).getByRole('menuitem', { name: '粘贴' }));
|
fireEvent.click(
|
||||||
|
within(copyPasteMenu).getByRole('menuitem', { name: '粘贴' }),
|
||||||
|
);
|
||||||
expect(screen.getAllByAltText(/画布图片:拼图素材/u)).toHaveLength(2);
|
expect(screen.getAllByAltText(/画布图片:拼图素材/u)).toHaveLength(2);
|
||||||
|
|
||||||
fireEvent.contextMenu(
|
fireEvent.contextMenu(
|
||||||
@@ -1885,7 +1961,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至100%' }));
|
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('button', { name: '当前缩放比例 100%' }));
|
||||||
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至50%' }));
|
fireEvent.click(screen.getByRole('menuitem', { name: '缩放至50%' }));
|
||||||
@@ -1915,25 +1993,23 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
});
|
});
|
||||||
expect(within(settingsPanel).getByText('画布背景')).toBeTruthy();
|
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(
|
expect((viewport as HTMLElement).style.backgroundColor).toBe(
|
||||||
'rgb(243, 240, 234)',
|
'rgb(243, 240, 234)',
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.change(
|
fireEvent.change(within(settingsPanel).getByLabelText('自定义画布背景色'), {
|
||||||
within(settingsPanel).getByLabelText('自定义画布背景色'),
|
|
||||||
{
|
|
||||||
target: { value: '#ffffff' },
|
target: { value: '#ffffff' },
|
||||||
},
|
});
|
||||||
);
|
|
||||||
expect((viewport as HTMLElement).style.backgroundColor).toBe(
|
expect((viewport as HTMLElement).style.backgroundColor).toBe(
|
||||||
'rgb(255, 255, 255)',
|
'rgb(255, 255, 255)',
|
||||||
);
|
);
|
||||||
|
|
||||||
const hexInput = within(settingsPanel).getByLabelText(
|
const hexInput =
|
||||||
'画布背景十六进制颜色',
|
within(settingsPanel).getByLabelText('画布背景十六进制颜色');
|
||||||
);
|
|
||||||
fireEvent.change(hexInput, { target: { value: '#abc' } });
|
fireEvent.change(hexInput, { target: { value: '#abc' } });
|
||||||
expect((hexInput as HTMLInputElement).value).toBe('#aabbcc');
|
expect((hexInput as HTMLInputElement).value).toBe('#aabbcc');
|
||||||
expect((viewport as HTMLElement).style.backgroundColor).toBe(
|
expect((viewport as HTMLElement).style.backgroundColor).toBe(
|
||||||
@@ -1954,9 +2030,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.keyDown(window, { key: 'Escape' });
|
fireEvent.keyDown(window, { key: 'Escape' });
|
||||||
expect(
|
expect(screen.queryByRole('dialog', { name: '画布背景设置' })).toBeNull();
|
||||||
screen.queryByRole('dialog', { name: '画布背景设置' }),
|
|
||||||
).toBeNull();
|
|
||||||
|
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(panelToolbar).getByRole('button', { name: '切换小地图' }),
|
within(panelToolbar).getByRole('button', { name: '切换小地图' }),
|
||||||
@@ -2927,7 +3001,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
||||||
target: { value: '高个子游侠' },
|
target: { value: '高个子游侠' },
|
||||||
});
|
});
|
||||||
fireEvent.click(within(characterPanel).getByRole('button', { name: '生成' }));
|
fireEvent.click(
|
||||||
|
within(characterPanel).getByRole('button', { name: '生成' }),
|
||||||
|
);
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||||
@@ -2937,9 +3013,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(
|
expect(generateEditorImageMock.mock.calls[0]?.[0]).not.toEqual(
|
||||||
generateEditorImageMock.mock.calls[0]?.[0],
|
|
||||||
).not.toEqual(
|
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
aspectRatio: expect.any(String),
|
aspectRatio: expect.any(String),
|
||||||
imageSize: expect.any(String),
|
imageSize: expect.any(String),
|
||||||
@@ -3019,12 +3093,16 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
const characterFrame = screen.getByLabelText('角色生成占位图');
|
const characterFrame = screen.getByLabelText('角色生成占位图');
|
||||||
expect(characterFrame).toBeTruthy();
|
expect(characterFrame).toBeTruthy();
|
||||||
|
|
||||||
dispatchPointerEvent(screen.getByLabelText('图像生成占位图'), 'pointerdown', {
|
dispatchPointerEvent(
|
||||||
|
screen.getByLabelText('图像生成占位图'),
|
||||||
|
'pointerdown',
|
||||||
|
{
|
||||||
button: 0,
|
button: 0,
|
||||||
pointerId: 1702,
|
pointerId: 1702,
|
||||||
clientX: 500,
|
clientX: 500,
|
||||||
clientY: 260,
|
clientY: 260,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', {
|
dispatchPointerEvent(screen.getByLabelText('画布工作区'), 'pointermove', {
|
||||||
pointerId: 1702,
|
pointerId: 1702,
|
||||||
clientX: 650,
|
clientX: 650,
|
||||||
@@ -3036,9 +3114,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
clientY: 390,
|
clientY: 390,
|
||||||
});
|
});
|
||||||
const movedFrame = screen.getByLabelText('图像生成占位图');
|
const movedFrame = screen.getByLabelText('图像生成占位图');
|
||||||
const movedLeft = Number.parseFloat(
|
const movedLeft = Number.parseFloat((movedFrame as HTMLElement).style.left);
|
||||||
(movedFrame as HTMLElement).style.left,
|
|
||||||
);
|
|
||||||
const movedTop = Number.parseFloat((movedFrame as HTMLElement).style.top);
|
const movedTop = Number.parseFloat((movedFrame as HTMLElement).style.top);
|
||||||
expect(movedLeft).toBeGreaterThan(originalLeft);
|
expect(movedLeft).toBeGreaterThan(originalLeft);
|
||||||
expect(movedTop).toBeGreaterThan(originalTop);
|
expect(movedTop).toBeGreaterThan(originalTop);
|
||||||
@@ -3077,9 +3153,13 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
.getByAltText(/画布图片:生成图片/)
|
.getByAltText(/画布图片:生成图片/)
|
||||||
.closest('button') as HTMLElement;
|
.closest('button') as HTMLElement;
|
||||||
const expectedLayerLeft =
|
const expectedLayerLeft =
|
||||||
movedLeft + Number.parseFloat((movedFrame as HTMLElement).style.width) / 2 - 512;
|
movedLeft +
|
||||||
|
Number.parseFloat((movedFrame as HTMLElement).style.width) / 2 -
|
||||||
|
512;
|
||||||
const expectedLayerTop =
|
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(
|
expect(Number.parseFloat(generatedLayer.style.left)).toBeCloseTo(
|
||||||
expectedLayerLeft,
|
expectedLayerLeft,
|
||||||
1,
|
1,
|
||||||
@@ -3160,7 +3240,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
iconPanel.querySelector('.image-canvas-editor__icon-spec-card'),
|
iconPanel.querySelector('.image-canvas-editor__icon-spec-card'),
|
||||||
).toBeTruthy();
|
).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(Number.parseFloat(iconPanel.style.width)).toBeCloseTo(61.2, 1);
|
||||||
expect(within(iconPanel).getAllByRole('textbox')).toHaveLength(7);
|
expect(within(iconPanel).getAllByRole('textbox')).toHaveLength(7);
|
||||||
@@ -4358,12 +4440,16 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
});
|
});
|
||||||
expect(within(editedMetadataDialog).queryByText('Prompt')).toBeNull();
|
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('参考图')).toBeTruthy();
|
expect(within(editedMetadataDialog).getByText('参考图')).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(editedMetadataDialog).getByText(/^生成图片 \d+$/u),
|
within(editedMetadataDialog).getByText(/^生成图片 \d+$/u),
|
||||||
).toBeTruthy();
|
).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 () => {
|
it('hides the edit image panel after generation starts while keeping the source preview visible', async () => {
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Check, ChevronLeft, Download, Pencil, X } from 'lucide-react';
|
||||||
Check,
|
|
||||||
ChevronLeft,
|
|
||||||
Download,
|
|
||||||
Pencil,
|
|
||||||
X,
|
|
||||||
} from 'lucide-react';
|
|
||||||
import JSZip from 'jszip';
|
import JSZip from 'jszip';
|
||||||
import {
|
import {
|
||||||
type CSSProperties,
|
type CSSProperties,
|
||||||
@@ -272,7 +266,10 @@ export function ImageCanvasEditorView() {
|
|||||||
const pendingProjectResourceLayersRef = useRef<
|
const pendingProjectResourceLayersRef = useRef<
|
||||||
Array<{
|
Array<{
|
||||||
layer: CanvasLayer;
|
layer: CanvasLayer;
|
||||||
options: { onCreated?: (resourceId: string) => void };
|
options: {
|
||||||
|
onCreated?: (resourceId: string) => void;
|
||||||
|
snapshotLayers?: CanvasLayer[];
|
||||||
|
};
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
const selectedLayerIdRef = useRef<string | null>(null);
|
const selectedLayerIdRef = useRef<string | null>(null);
|
||||||
@@ -373,8 +370,9 @@ export function ImageCanvasEditorView() {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [imageContextMenu, setImageContextMenu] =
|
const [imageContextMenu, setImageContextMenu] =
|
||||||
useState<ImageContextMenuState | null>(null);
|
useState<ImageContextMenuState | null>(null);
|
||||||
const [contextMenu, setContextMenu] =
|
const [contextMenu, setContextMenu] = useState<CanvasContextMenuState | null>(
|
||||||
useState<CanvasContextMenuState | null>(null);
|
null,
|
||||||
|
);
|
||||||
const [canvasClipboard, setCanvasClipboard] =
|
const [canvasClipboard, setCanvasClipboard] =
|
||||||
useState<CanvasClipboard | null>(null);
|
useState<CanvasClipboard | null>(null);
|
||||||
const [historyVersion, setHistoryVersion] = useState(0);
|
const [historyVersion, setHistoryVersion] = useState(0);
|
||||||
@@ -447,8 +445,7 @@ export function ImageCanvasEditorView() {
|
|||||||
: null,
|
: null,
|
||||||
[activeCanvasGenerationDialog, layers],
|
[activeCanvasGenerationDialog, layers],
|
||||||
);
|
);
|
||||||
const generationAnchor =
|
const generationAnchor = activeCanvasGenerationDialog
|
||||||
activeCanvasGenerationDialog
|
|
||||||
? (activeGenerationLayer ??
|
? (activeGenerationLayer ??
|
||||||
activeCanvasGenerationDialog.placeholder ??
|
activeCanvasGenerationDialog.placeholder ??
|
||||||
null)
|
null)
|
||||||
@@ -630,8 +627,7 @@ export function ImageCanvasEditorView() {
|
|||||||
) => CanvasGenerationDialogState | null,
|
) => CanvasGenerationDialogState | null,
|
||||||
) => {
|
) => {
|
||||||
setGenerateDialog((currentDialog) =>
|
setGenerateDialog((currentDialog) =>
|
||||||
isCanvasGenerationDialog(currentDialog) &&
|
isCanvasGenerationDialog(currentDialog) && currentDialog.id === dialogId
|
||||||
currentDialog.id === dialogId
|
|
||||||
? updater(currentDialog)
|
? updater(currentDialog)
|
||||||
: currentDialog,
|
: currentDialog,
|
||||||
);
|
);
|
||||||
@@ -707,7 +703,9 @@ export function ImageCanvasEditorView() {
|
|||||||
inactiveGenerateDialogs: inactiveGenerateDialogsRef.current.map(
|
inactiveGenerateDialogs: inactiveGenerateDialogsRef.current.map(
|
||||||
(dialog) => ({
|
(dialog) => ({
|
||||||
...dialog,
|
...dialog,
|
||||||
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
|
placeholder: dialog.placeholder
|
||||||
|
? { ...dialog.placeholder }
|
||||||
|
: undefined,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
selectedLayerId: selectedLayerIdRef.current,
|
selectedLayerId: selectedLayerIdRef.current,
|
||||||
@@ -733,7 +731,9 @@ export function ImageCanvasEditorView() {
|
|||||||
setInactiveGenerateDialogs(
|
setInactiveGenerateDialogs(
|
||||||
snapshot.inactiveGenerateDialogs.map((dialog) => ({
|
snapshot.inactiveGenerateDialogs.map((dialog) => ({
|
||||||
...dialog,
|
...dialog,
|
||||||
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
|
placeholder: dialog.placeholder
|
||||||
|
? { ...dialog.placeholder }
|
||||||
|
: undefined,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
setSelectedLayerId(snapshot.selectedLayerId);
|
setSelectedLayerId(snapshot.selectedLayerId);
|
||||||
@@ -859,7 +859,10 @@ export function ImageCanvasEditorView() {
|
|||||||
const createProjectResourceForLayer = useCallback(
|
const createProjectResourceForLayer = useCallback(
|
||||||
(
|
(
|
||||||
layer: CanvasLayer,
|
layer: CanvasLayer,
|
||||||
options: { onCreated?: (resourceId: string) => void } = {},
|
options: {
|
||||||
|
onCreated?: (resourceId: string) => void;
|
||||||
|
snapshotLayers?: CanvasLayer[];
|
||||||
|
} = {},
|
||||||
) => {
|
) => {
|
||||||
const readyProjectId = projectIdRef.current;
|
const readyProjectId = projectIdRef.current;
|
||||||
if (!readyProjectId) {
|
if (!readyProjectId) {
|
||||||
@@ -882,16 +885,40 @@ export function ImageCanvasEditorView() {
|
|||||||
})
|
})
|
||||||
.then((resource) => {
|
.then((resource) => {
|
||||||
options.onCreated?.(resource.resourceId);
|
options.onCreated?.(resource.resourceId);
|
||||||
setLayers((currentLayers) =>
|
const layerWithResourceId = {
|
||||||
currentLayers.map((currentLayer) =>
|
...layer,
|
||||||
currentLayer.id === layer.id
|
|
||||||
? {
|
|
||||||
...currentLayer,
|
|
||||||
resourceId: resource.resourceId,
|
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,
|
: 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) => {
|
.catch((error: unknown) => {
|
||||||
if (isEditorAuthError(error)) {
|
if (isEditorAuthError(error)) {
|
||||||
@@ -1325,9 +1352,13 @@ export function ImageCanvasEditorView() {
|
|||||||
if (!canvasClipboard?.layers.length) {
|
if (!canvasClipboard?.layers.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nextLayers = duplicateLayersToPoint(canvasClipboard.layers, canvasPoint, {
|
const nextLayers = duplicateLayersToPoint(
|
||||||
|
canvasClipboard.layers,
|
||||||
|
canvasPoint,
|
||||||
|
{
|
||||||
renameCopies: canvasClipboard.mode !== 'cut',
|
renameCopies: canvasClipboard.mode !== 'cut',
|
||||||
});
|
},
|
||||||
|
);
|
||||||
if (!nextLayers.length) {
|
if (!nextLayers.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1352,7 +1383,9 @@ export function ImageCanvasEditorView() {
|
|||||||
setCanvasClipboard(clipboard);
|
setCanvasClipboard(clipboard);
|
||||||
if (options.cut) {
|
if (options.cut) {
|
||||||
captureCanvasHistory();
|
captureCanvasHistory();
|
||||||
setLayers((currentLayers) => removeCanvasLayers(currentLayers, targetIds));
|
setLayers((currentLayers) =>
|
||||||
|
removeCanvasLayers(currentLayers, targetIds),
|
||||||
|
);
|
||||||
selectSingleLayer(null);
|
selectSingleLayer(null);
|
||||||
setMetadataLayer((currentLayer) =>
|
setMetadataLayer((currentLayer) =>
|
||||||
currentLayer && targetIds.includes(currentLayer.id)
|
currentLayer && targetIds.includes(currentLayer.id)
|
||||||
@@ -1534,13 +1567,29 @@ export function ImageCanvasEditorView() {
|
|||||||
const listRect = listElement?.getBoundingClientRect();
|
const listRect = listElement?.getBoundingClientRect();
|
||||||
const headerRect = header?.getBoundingClientRect();
|
const headerRect = header?.getBoundingClientRect();
|
||||||
setPinnedAssetMoveFolderId(
|
setPinnedAssetMoveFolderId(
|
||||||
listRect && headerRect &&
|
listRect &&
|
||||||
|
headerRect &&
|
||||||
(headerRect.bottom < listRect.top || headerRect.top > listRect.bottom)
|
(headerRect.bottom < listRect.top || headerRect.top > listRect.bottom)
|
||||||
? folderId
|
? folderId
|
||||||
: null,
|
: 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 = (
|
const addAssetLayer = (
|
||||||
asset: EditorAsset,
|
asset: EditorAsset,
|
||||||
position?: { x: number; y: number },
|
position?: { x: number; y: number },
|
||||||
@@ -1557,10 +1606,9 @@ export function ImageCanvasEditorView() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
captureCanvasHistory();
|
captureCanvasHistory();
|
||||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
appendCanvasLayersWithResources([nextLayer]);
|
||||||
selectSingleLayer(nextLayer.id);
|
selectSingleLayer(nextLayer.id);
|
||||||
setHoveredLayerId(null);
|
setHoveredLayerId(null);
|
||||||
createProjectResourceForLayer(nextLayer);
|
|
||||||
};
|
};
|
||||||
addAssetLayerRef.current = addAssetLayer;
|
addAssetLayerRef.current = addAssetLayer;
|
||||||
|
|
||||||
@@ -1609,7 +1657,10 @@ export function ImageCanvasEditorView() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const blob = await readLayerImageBlob(layer);
|
const blob = await readLayerImageBlob(layer);
|
||||||
const extension = getImageExtensionFromTypeOrSrc(blob.type, layer.src);
|
const extension = getImageExtensionFromTypeOrSrc(
|
||||||
|
blob.type,
|
||||||
|
layer.src,
|
||||||
|
);
|
||||||
const file = `images/${indexedFileName}.${extension}`;
|
const file = `images/${indexedFileName}.${extension}`;
|
||||||
imageByKey.set(key, {
|
imageByKey.set(key, {
|
||||||
key,
|
key,
|
||||||
@@ -1872,7 +1923,8 @@ export function ImageCanvasEditorView() {
|
|||||||
setSelectedLayerIds((currentIds) =>
|
setSelectedLayerIds((currentIds) =>
|
||||||
currentIds.filter((layerId) =>
|
currentIds.filter((layerId) =>
|
||||||
layers.every(
|
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 deleteSelectedAssets = () => {
|
||||||
const ids = [...selectedAssetIds];
|
const ids = [...selectedAssetIds];
|
||||||
const deletedAssets = assets.filter((asset) => selectedAssetIds.has(asset.id));
|
const deletedAssets = assets.filter((asset) =>
|
||||||
|
selectedAssetIds.has(asset.id),
|
||||||
|
);
|
||||||
setAssets((currentAssets) =>
|
setAssets((currentAssets) =>
|
||||||
currentAssets.filter((asset) => !selectedAssetIds.has(asset.id)),
|
currentAssets.filter((asset) => !selectedAssetIds.has(asset.id)),
|
||||||
);
|
);
|
||||||
@@ -2363,7 +2417,7 @@ export function ImageCanvasEditorView() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (options.addToCanvas) {
|
if (options.addToCanvas) {
|
||||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
appendCanvasLayersWithResources([nextLayer]);
|
||||||
}
|
}
|
||||||
if (options.addToCanvas) {
|
if (options.addToCanvas) {
|
||||||
selectSingleLayer(nextLayer.id);
|
selectSingleLayer(nextLayer.id);
|
||||||
@@ -2446,10 +2500,6 @@ export function ImageCanvasEditorView() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (options.addToCanvas) {
|
|
||||||
createProjectResourceForLayer(nextLayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imageSrc) {
|
if (imageSrc) {
|
||||||
const uploadedImage = new Image();
|
const uploadedImage = new Image();
|
||||||
uploadedImage.onload = () => {
|
uploadedImage.onload = () => {
|
||||||
@@ -2825,7 +2875,7 @@ export function ImageCanvasEditorView() {
|
|||||||
generationInputs: options.generationInputs,
|
generationInputs: options.generationInputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
appendCanvasLayersWithResources([nextLayer]);
|
||||||
selectSingleLayer(nextLayer.id);
|
selectSingleLayer(nextLayer.id);
|
||||||
setActiveSidebarPanel('layers');
|
setActiveSidebarPanel('layers');
|
||||||
if (options.sourceLayer) {
|
if (options.sourceLayer) {
|
||||||
@@ -2848,7 +2898,6 @@ export function ImageCanvasEditorView() {
|
|||||||
if (options.sourceLayer) {
|
if (options.sourceLayer) {
|
||||||
fitLayers([options.sourceLayer, nextLayer]);
|
fitLayers([options.sourceLayer, nextLayer]);
|
||||||
}
|
}
|
||||||
createProjectResourceForLayer(nextLayer);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addQuickEditResultLayer = (
|
const addQuickEditResultLayer = (
|
||||||
@@ -2859,7 +2908,8 @@ export function ImageCanvasEditorView() {
|
|||||||
layerCounterRef.current += 1;
|
layerCounterRef.current += 1;
|
||||||
const generatedIndex = layerCounterRef.current;
|
const generatedIndex = layerCounterRef.current;
|
||||||
const originalWidth = generated.width || sourceLayer.originalWidth || 1024;
|
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(
|
const { width, height } = resolveLayerResolutionSize(
|
||||||
originalWidth,
|
originalWidth,
|
||||||
originalHeight,
|
originalHeight,
|
||||||
@@ -2894,13 +2944,12 @@ export function ImageCanvasEditorView() {
|
|||||||
generationInputs,
|
generationInputs,
|
||||||
};
|
};
|
||||||
|
|
||||||
setLayers((currentLayers) => [...currentLayers, nextLayer]);
|
appendCanvasLayersWithResources([nextLayer]);
|
||||||
selectSingleLayer(nextLayer.id);
|
selectSingleLayer(nextLayer.id);
|
||||||
setActiveSidebarPanel('layers');
|
setActiveSidebarPanel('layers');
|
||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
setActiveTool('select');
|
setActiveTool('select');
|
||||||
fitLayers([sourceLayer, nextLayer]);
|
fitLayers([sourceLayer, nextLayer]);
|
||||||
createProjectResourceForLayer(nextLayer);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addIconSpritesheetResultLayers = (
|
const addIconSpritesheetResultLayers = (
|
||||||
@@ -2970,14 +3019,13 @@ export function ImageCanvasEditorView() {
|
|||||||
if (!nextLayers.length) {
|
if (!nextLayers.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLayers((currentLayers) => [...currentLayers, ...nextLayers]);
|
appendCanvasLayersWithResources(nextLayers);
|
||||||
selectSingleLayer(nextLayers[0]?.id ?? null);
|
selectSingleLayer(nextLayers[0]?.id ?? null);
|
||||||
setActiveSidebarPanel('layers');
|
setActiveSidebarPanel('layers');
|
||||||
if (dialogId) {
|
if (dialogId) {
|
||||||
removeCanvasGenerationDialogById(dialogId);
|
removeCanvasGenerationDialogById(dialogId);
|
||||||
}
|
}
|
||||||
setActiveTool('select');
|
setActiveTool('select');
|
||||||
nextLayers.forEach((layer) => createProjectResourceForLayer(layer));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateIconDescription = (index: number, value: string) => {
|
const updateIconDescription = (index: number, value: string) => {
|
||||||
@@ -3101,8 +3149,9 @@ export function ImageCanvasEditorView() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const referenceImageSrc =
|
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||||
await resolveEditorImageReferenceDataUrl(quickEditSourceLayer.src);
|
quickEditSourceLayer.src,
|
||||||
|
);
|
||||||
const generated = await generateEditorImage({
|
const generated = await generateEditorImage({
|
||||||
prompt: normalizedPrompt,
|
prompt: normalizedPrompt,
|
||||||
size: quickEditPanel.size,
|
size: quickEditPanel.size,
|
||||||
@@ -3388,9 +3437,7 @@ export function ImageCanvasEditorView() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCanvasContextMenu = (
|
const handleCanvasContextMenu = (event: ReactMouseEvent<HTMLDivElement>) => {
|
||||||
event: ReactMouseEvent<HTMLDivElement>,
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const position = resolveContextMenuPosition(
|
const position = resolveContextMenuPosition(
|
||||||
@@ -4241,14 +4288,18 @@ export function ImageCanvasEditorView() {
|
|||||||
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
||||||
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
||||||
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
||||||
setIsPickingCharacterSpecFromCanvas={setIsPickingCharacterSpecFromCanvas}
|
setIsPickingCharacterSpecFromCanvas={
|
||||||
|
setIsPickingCharacterSpecFromCanvas
|
||||||
|
}
|
||||||
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
||||||
onOpenSpecDialog={openSpecDialog}
|
onOpenSpecDialog={openSpecDialog}
|
||||||
onRequestUpload={(target) => {
|
onRequestUpload={(target) => {
|
||||||
setUploadTarget(target);
|
setUploadTarget(target);
|
||||||
uploadInputRef.current?.click();
|
uploadInputRef.current?.click();
|
||||||
}}
|
}}
|
||||||
onSubmitImageGeneration={(dialog) => void submitImageGeneration(dialog)}
|
onSubmitImageGeneration={(dialog) =>
|
||||||
|
void submitImageGeneration(dialog)
|
||||||
|
}
|
||||||
onSubmitIconSpritesheetGeneration={(dialog) =>
|
onSubmitIconSpritesheetGeneration={(dialog) =>
|
||||||
void submitIconSpritesheetGeneration(dialog)
|
void submitIconSpritesheetGeneration(dialog)
|
||||||
}
|
}
|
||||||
@@ -4268,9 +4319,10 @@ export function ImageCanvasEditorView() {
|
|||||||
onUpdateSpecFormValue={updateSpecFormValue}
|
onUpdateSpecFormValue={updateSpecFormValue}
|
||||||
onUpdateIconDescription={updateIconDescription}
|
onUpdateIconDescription={updateIconDescription}
|
||||||
onAddIconDescription={addIconDescription}
|
onAddIconDescription={addIconDescription}
|
||||||
onUpdateCharacterAnimationDuration={updateCharacterAnimationDuration}
|
onUpdateCharacterAnimationDuration={
|
||||||
|
updateCharacterAnimationDuration
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</ImageCanvasStageView>
|
</ImageCanvasStageView>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -4311,7 +4363,11 @@ export function ImageCanvasEditorView() {
|
|||||||
key={`${reference.title}-${reference.label}-${reference.src}`}
|
key={`${reference.title}-${reference.label}-${reference.src}`}
|
||||||
className="image-canvas-editor__metadata-reference-card"
|
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-reference-copy">
|
||||||
<span className="image-canvas-editor__metadata-input-title">
|
<span className="image-canvas-editor__metadata-input-title">
|
||||||
{reference.title}
|
{reference.title}
|
||||||
|
|||||||
Reference in New Issue
Block a user