调整图片编辑器参考图选择交互
- 常规参考图入口改为先弹出来源菜单,支持从画布选择和上传图片。 - 角色规范、图标规范和常规参考图来源菜单统一向上弹出。 - 画布参考图选择拦截普通图层选中逻辑,保持生成面板不隐藏。 - 补充图片编辑器交互测试与技术文档说明。
This commit is contained in:
@@ -83,7 +83,7 @@
|
|||||||
- 默认选择模式;底部工具栏能切换工具;中键拖拽和 Space 临时抓手都能平移画布。
|
- 默认选择模式;底部工具栏能切换工具;中键拖拽和 Space 临时抓手都能平移画布。
|
||||||
- 拖拽图片接近其它图片边缘或中心时显示吸附线,并保存吸附后的最终布局。
|
- 拖拽图片接近其它图片边缘或中心时显示吸附线,并保存吸附后的最终布局。
|
||||||
- 生成工具点击后显示画布内 `Image Generator` 占位框和跟随占位框的生成输入框,生成失败保留占位和输入状态,生成成功后在占位位置创建真实图层,并让输入框继续跟随该生成图。
|
- 生成工具点击后显示画布内 `Image Generator` 占位框和跟随占位框的生成输入框,生成失败保留占位和输入状态,生成成功后在占位位置创建真实图层,并让输入框继续跟随该生成图。
|
||||||
- 生成类入口打开画布内面板时,底部 AI 工具栏必须保持可见;`生成规范`、角色 / 图标规范来源这类轻量菜单通过页面级 fixed portal 渲染,不能留在底部工具栏或参考图横向滚动容器内部,避免被局部 `overflow` 裁切。
|
- 生成类入口打开画布内面板时,底部 AI 工具栏必须保持可见;`生成规范`、角色 / 图标规范来源、角色常规参考图来源这类轻量菜单通过页面级 fixed portal 渲染,不能留在底部工具栏或参考图横向滚动容器内部,避免被局部 `overflow` 裁切。角色形象规范和常规参考图来源菜单必须向上弹出;常规参考图点击后先选择“从画布中选择”或“上传图片”,从画布取图时只绑定参考图,不触发普通画布图层选中、聚焦、面板隐藏或拖拽逻辑,绑定后退出画布选择状态。
|
||||||
- 点击生成、生成规范、生成角色形象或生成图标素材后创建的占位图可继续保留;点击画布空白区域让当前图片或占位图失焦时,关闭当前生成面板并移除图片选中样式,但不删除占位图本身。
|
- 点击生成、生成规范、生成角色形象或生成图标素材后创建的占位图可继续保留;点击画布空白区域让当前图片或占位图失焦时,关闭当前生成面板并移除图片选中样式,但不删除占位图本身。
|
||||||
- 生成资源显示元数据按钮,元数据窗口展示来源、生成输入快照、model、provider、task、Resolution 和 OSS 引用;生成输入快照只包含用户面板输入和参考图,不包含后端拼接 Prompt,不再展示独立 Size 字段。
|
- 生成资源显示元数据按钮,元数据窗口展示来源、生成输入快照、model、provider、task、Resolution 和 OSS 引用;生成输入快照只包含用户面板输入和参考图,不包含后端拼接 Prompt,不再展示独立 Size 字段。
|
||||||
- 修改生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图。
|
- 修改生成资源后,右侧出现新生成结果图层,并自动 fit 原图 + 新图。
|
||||||
|
|||||||
@@ -132,10 +132,14 @@ export type GenerateDialogState = {
|
|||||||
generatedLayerId?: string;
|
generatedLayerId?: string;
|
||||||
specType?: SpecGenerationType;
|
specType?: SpecGenerationType;
|
||||||
specValues?: SpecFormValues;
|
specValues?: SpecFormValues;
|
||||||
|
specReference?: CharacterReferenceImage | null;
|
||||||
characterSpecReference?: CharacterReferenceImage | null;
|
characterSpecReference?: CharacterReferenceImage | null;
|
||||||
characterReferences?: CharacterReferenceImage[];
|
characterReferences?: CharacterReferenceImage[];
|
||||||
iconSpecReference?: CharacterReferenceImage | null;
|
iconSpecReference?: CharacterReferenceImage | null;
|
||||||
iconDescriptions?: string[];
|
iconDescriptions?: string[];
|
||||||
|
imageModel?: string;
|
||||||
|
aspectRatio?: string;
|
||||||
|
imageSize?: string;
|
||||||
errorMessage?: string;
|
errorMessage?: string;
|
||||||
placeholder?: {
|
placeholder?: {
|
||||||
x: number;
|
x: number;
|
||||||
@@ -215,6 +219,7 @@ export type CharacterAnimationPanelState = {
|
|||||||
|
|
||||||
export type UploadTarget =
|
export type UploadTarget =
|
||||||
| 'asset'
|
| 'asset'
|
||||||
|
| 'spec-reference'
|
||||||
| 'character-spec'
|
| 'character-spec'
|
||||||
| 'character-reference'
|
| 'character-reference'
|
||||||
| 'icon-spec';
|
| 'icon-spec';
|
||||||
|
|||||||
@@ -2914,7 +2914,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
|
|
||||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||||
kind: 'spec',
|
kind: 'spec',
|
||||||
model: 'gpt-image-2',
|
model: 'gemini-3.1-flash-image-preview',
|
||||||
size: '2048x1152',
|
size: '2048x1152',
|
||||||
prompt: expect.stringContaining('玩法设计:平台跳跃玩法'),
|
prompt: expect.stringContaining('玩法设计:平台跳跃玩法'),
|
||||||
});
|
});
|
||||||
@@ -3009,23 +3009,29 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
name: '生成角色形象',
|
name: '生成角色形象',
|
||||||
});
|
});
|
||||||
expect(within(characterPanel).getByText('画面比例')).toBeTruthy();
|
expect(within(characterPanel).getByText('画面比例')).toBeTruthy();
|
||||||
|
expect(within(characterPanel).getByText('大小尺寸')).toBeTruthy();
|
||||||
expect(within(characterPanel).getByText('模型')).toBeTruthy();
|
expect(within(characterPanel).getByText('模型')).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(characterPanel).getByRole('button', { name: '1:1' }),
|
within(characterPanel).getByRole('button', { name: '1:1' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(characterPanel).getByRole('button', { name: 'GPT Image' }),
|
within(characterPanel).getByRole('button', { name: '1K' }),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
within(characterPanel).getByRole('button', { name: 'nanobanana2' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole('button', { name: '生成图标素材' }));
|
fireEvent.click(screen.getByRole('button', { name: '生成图标素材' }));
|
||||||
const iconPanel = screen.getByRole('dialog', { name: '生成图标素材' });
|
const iconPanel = screen.getByRole('dialog', { name: '生成图标素材' });
|
||||||
|
expect(within(iconPanel).getByText('画面比例')).toBeTruthy();
|
||||||
|
expect(within(iconPanel).getByText('大小尺寸')).toBeTruthy();
|
||||||
expect(within(iconPanel).getByText('模型')).toBeTruthy();
|
expect(within(iconPanel).getByText('模型')).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
within(iconPanel).getByRole('button', { name: 'nanobanana2' }),
|
within(iconPanel).getByRole('button', { name: 'nanobanana2' }),
|
||||||
).toBeTruthy();
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('submits character generation without legacy dimension options', async () => {
|
it('submits character generation with default model and dimension options', async () => {
|
||||||
generateEditorImageMock.mockResolvedValueOnce({
|
generateEditorImageMock.mockResolvedValueOnce({
|
||||||
imageSrc: 'data:image/png;base64,character-model-options',
|
imageSrc: 'data:image/png;base64,character-model-options',
|
||||||
width: 1024,
|
width: 1024,
|
||||||
@@ -3056,15 +3062,104 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
kind: 'character',
|
kind: 'character',
|
||||||
prompt: '高个子游侠',
|
prompt: '高个子游侠',
|
||||||
|
model: 'gemini-3.1-flash-image-preview',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
imageSize: '1K',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
expect(generateEditorImageMock.mock.calls[0]?.[0]).not.toEqual(
|
});
|
||||||
expect.objectContaining({
|
|
||||||
aspectRatio: expect.any(String),
|
it('remembers the last selected image model for character and icon generation', async () => {
|
||||||
imageSize: expect.any(String),
|
generateEditorImageMock.mockResolvedValueOnce({
|
||||||
}),
|
imageSrc: 'data:image/png;base64,character-gpt-model',
|
||||||
|
width: 1024,
|
||||||
|
height: 1024,
|
||||||
|
sourceType: 'generated',
|
||||||
|
prompt: '蓝衣剑士',
|
||||||
|
actualPrompt: '蓝衣剑士',
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
provider: 'VectorEngine',
|
||||||
|
taskId: 'character-gpt-model-1',
|
||||||
|
});
|
||||||
|
generateEditorIconSpritesheetMock.mockResolvedValueOnce({
|
||||||
|
spritesheetImageSrc: 'data:image/png;base64,sheet-gpt-model',
|
||||||
|
spritesheetWidth: 1024,
|
||||||
|
spritesheetHeight: 1024,
|
||||||
|
iconImageSrcs: [
|
||||||
|
{
|
||||||
|
name: '返回按钮',
|
||||||
|
imageSrc: 'data:image/png;base64,back',
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
prompt: '图标 prompt',
|
||||||
|
actualPrompt: '图标 prompt',
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
provider: 'VectorEngine',
|
||||||
|
taskId: 'icon-gpt-model-1',
|
||||||
|
});
|
||||||
|
render(<ImageCanvasEditorView />);
|
||||||
|
await screen.findByAltText('画布图片:拼图素材');
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '生成角色形象' }));
|
||||||
|
const characterPanel = screen.getByRole('dialog', {
|
||||||
|
name: '生成角色形象',
|
||||||
|
});
|
||||||
|
fireEvent.click(
|
||||||
|
within(characterPanel).getByRole('button', { name: 'gpt-image-2' }),
|
||||||
);
|
);
|
||||||
|
fireEvent.click(within(characterPanel).getByRole('button', { name: '2:3' }));
|
||||||
|
fireEvent.click(within(characterPanel).getByRole('button', { name: '2K' }));
|
||||||
|
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
||||||
|
target: { value: '蓝衣剑士' },
|
||||||
|
});
|
||||||
|
fireEvent.click(
|
||||||
|
within(characterPanel).getByRole('button', { name: '生成' }),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
kind: 'character',
|
||||||
|
prompt: '蓝衣剑士',
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
aspectRatio: '2:3',
|
||||||
|
imageSize: '2K',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole('button', { name: '生成图标素材' }));
|
||||||
|
const iconPanel = screen.getByRole('dialog', { name: '生成图标素材' });
|
||||||
|
expect(
|
||||||
|
within(iconPanel).getByRole('button', { name: 'gpt-image-2' }),
|
||||||
|
).toBeTruthy();
|
||||||
|
fireEvent.click(
|
||||||
|
within(iconPanel).getByRole('button', { name: '图标素材规范' }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
|
||||||
|
await userEvent.upload(
|
||||||
|
screen.getByLabelText('上传图片文件'),
|
||||||
|
new File(['icon-spec'], '图标规范.png', { type: 'image/png' }),
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
within(screen.getByRole('dialog', { name: '生成图标素材' })).getByRole(
|
||||||
|
'button',
|
||||||
|
{ name: '生成' },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
imageSize: '1K',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps the bottom AI toolbar visible while generation panels are open', () => {
|
it('keeps the bottom AI toolbar visible while generation panels are open', () => {
|
||||||
@@ -3239,6 +3334,18 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
const sourceMenu = screen.getByRole('menu', { name: '角色形象规范来源' });
|
const sourceMenu = screen.getByRole('menu', { name: '角色形象规范来源' });
|
||||||
|
|
||||||
expect(referenceRow?.contains(sourceMenu)).toBe(false);
|
expect(referenceRow?.contains(sourceMenu)).toBe(false);
|
||||||
|
expect(sourceMenu.className).toContain('platform-floating-menu--top-start');
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
|
||||||
|
);
|
||||||
|
const regularReferenceMenu = screen.getByRole('menu', {
|
||||||
|
name: '常规参考图来源',
|
||||||
|
});
|
||||||
|
expect(referenceRow?.contains(regularReferenceMenu)).toBe(false);
|
||||||
|
expect(regularReferenceMenu.className).toContain(
|
||||||
|
'platform-floating-menu--top-start',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses Lovart-style reference tiles in the character generation panel', () => {
|
it('uses Lovart-style reference tiles in the character generation panel', () => {
|
||||||
@@ -3395,7 +3502,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
|
|
||||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||||
kind: 'spec',
|
kind: 'spec',
|
||||||
model: 'gpt-image-2',
|
model: 'gemini-3.1-flash-image-preview',
|
||||||
size: '2048x1152',
|
size: '2048x1152',
|
||||||
prompt: expect.stringContaining('生成一张完整游戏UI规范汇总设定展板'),
|
prompt: expect.stringContaining('生成一张完整游戏UI规范汇总设定展板'),
|
||||||
});
|
});
|
||||||
@@ -3442,7 +3549,7 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
|
|
||||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||||
kind: 'spec',
|
kind: 'spec',
|
||||||
model: 'gpt-image-2',
|
model: 'gemini-3.1-flash-image-preview',
|
||||||
size: '2048x1152',
|
size: '2048x1152',
|
||||||
prompt: '生成一张武器图标规范展板',
|
prompt: '生成一张武器图标规范展板',
|
||||||
});
|
});
|
||||||
@@ -3504,15 +3611,51 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
screen.queryByText('请选择画布中的图片作为角色形象规范,按 Esc 退出'),
|
screen.queryByText('请选择画布中的图片作为角色形象规范,按 Esc 退出'),
|
||||||
).toBeNull();
|
).toBeNull();
|
||||||
|
|
||||||
|
const canvasReferenceLayer = screen
|
||||||
|
.getByAltText('画布图片:大鱼素材')
|
||||||
|
.closest('button')!;
|
||||||
|
expect(canvasReferenceLayer.className).not.toContain(
|
||||||
|
'image-canvas-editor__layer--selected',
|
||||||
|
);
|
||||||
fireEvent.click(
|
fireEvent.click(
|
||||||
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
|
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
|
||||||
);
|
);
|
||||||
|
const regularReferenceMenu = screen.getByRole('menu', {
|
||||||
|
name: '常规参考图来源',
|
||||||
|
});
|
||||||
|
fireEvent.click(
|
||||||
|
within(regularReferenceMenu).getByRole('menuitem', {
|
||||||
|
name: '从画布中选择',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
screen.getByText('请选择画布中的图片作为常规参考图,按 Esc 退出'),
|
||||||
|
).toBeTruthy();
|
||||||
|
fireEvent.pointerDown(canvasReferenceLayer, {
|
||||||
|
button: 0,
|
||||||
|
pointerId: 171,
|
||||||
|
clientX: 180,
|
||||||
|
clientY: 120,
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
screen.queryByText('请选择画布中的图片作为常规参考图,按 Esc 退出'),
|
||||||
|
).toBeNull();
|
||||||
|
expect(screen.getByRole('dialog', { name: '生成角色形象' })).toBeTruthy();
|
||||||
|
expect(canvasReferenceLayer.className).not.toContain(
|
||||||
|
'image-canvas-editor__layer--selected',
|
||||||
|
);
|
||||||
|
expect(within(characterPanel).getByText('1')).toBeTruthy();
|
||||||
|
|
||||||
|
fireEvent.click(
|
||||||
|
within(characterPanel).getByRole('button', { name: '上传常规参考图' }),
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
|
||||||
await userEvent.upload(
|
await userEvent.upload(
|
||||||
screen.getByLabelText('上传图片文件'),
|
screen.getByLabelText('上传图片文件'),
|
||||||
new File(['reference'], '常规参考.png', { type: 'image/png' }),
|
new File(['reference'], '常规参考.png', { type: 'image/png' }),
|
||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(within(characterPanel).getByText('1')).toBeTruthy();
|
expect(within(characterPanel).getByText('2')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
fireEvent.change(within(characterPanel).getByLabelText('角色设定'), {
|
||||||
@@ -3525,8 +3668,12 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
expect(generateEditorImageMock).toHaveBeenCalledWith({
|
||||||
kind: 'character',
|
kind: 'character',
|
||||||
prompt: '银发游侠,蓝色披风,弓箭手,适合像素风战棋。',
|
prompt: '银发游侠,蓝色披风,弓箭手,适合像素风战棋。',
|
||||||
|
model: 'gemini-3.1-flash-image-preview',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
imageSize: '1K',
|
||||||
referenceImageSrcs: [
|
referenceImageSrcs: [
|
||||||
'/creation-type-references/puzzle.webp',
|
'/creation-type-references/puzzle.webp',
|
||||||
|
'/creation-type-references/big-fish.webp',
|
||||||
expect.stringMatching(/^data:image\/png;base64,/u),
|
expect.stringMatching(/^data:image\/png;base64,/u),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -3554,6 +3701,8 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
expect(within(characterInfoPanel).getByText('角色形象规范')).toBeTruthy();
|
expect(within(characterInfoPanel).getByText('角色形象规范')).toBeTruthy();
|
||||||
expect(within(characterInfoPanel).getByText('拼图素材')).toBeTruthy();
|
expect(within(characterInfoPanel).getByText('拼图素材')).toBeTruthy();
|
||||||
expect(within(characterInfoPanel).getByText('常规参考图 1')).toBeTruthy();
|
expect(within(characterInfoPanel).getByText('常规参考图 1')).toBeTruthy();
|
||||||
|
expect(within(characterInfoPanel).getByText('大鱼素材')).toBeTruthy();
|
||||||
|
expect(within(characterInfoPanel).getByText('常规参考图 2')).toBeTruthy();
|
||||||
expect(within(characterInfoPanel).getByText('常规参考.png')).toBeTruthy();
|
expect(within(characterInfoPanel).getByText('常规参考.png')).toBeTruthy();
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
|
expect(saveEditorProjectLayoutMock).toHaveBeenCalledWith(
|
||||||
@@ -3752,6 +3901,9 @@ describe('ImageCanvasEditorView', () => {
|
|||||||
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith({
|
expect(generateEditorIconSpritesheetMock).toHaveBeenCalledWith({
|
||||||
referenceImageSrc: 'data:image/png;base64,icon-spec',
|
referenceImageSrc: 'data:image/png;base64,icon-spec',
|
||||||
iconDescriptions: ['返回按钮', '设置按钮'],
|
iconDescriptions: ['返回按钮', '设置按钮'],
|
||||||
|
model: 'gemini-3.1-flash-image-preview',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
imageSize: '1K',
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ export function ImageCanvasEditorView() {
|
|||||||
const resetCanvasInteractionStateRef = useRef<() => void>(() => {});
|
const resetCanvasInteractionStateRef = useRef<() => void>(() => {});
|
||||||
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||||
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||||
const selectedLayerIdRef = useRef<string | null>(null);
|
const selectedLayerIdRef = useRef<string | null>(null);
|
||||||
const selectedLayerIdsRef = useRef<string[]>([]);
|
const selectedLayerIdsRef = useRef<string[]>([]);
|
||||||
@@ -466,8 +467,12 @@ export function ImageCanvasEditorView() {
|
|||||||
setIsSpecMenuOpen,
|
setIsSpecMenuOpen,
|
||||||
isCharacterSpecMenuOpen,
|
isCharacterSpecMenuOpen,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
isCharacterReferenceMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
isIconSpecMenuOpen,
|
isIconSpecMenuOpen,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
@@ -480,6 +485,7 @@ export function ImageCanvasEditorView() {
|
|||||||
openEditDialog,
|
openEditDialog,
|
||||||
openQuickEditPanel,
|
openQuickEditPanel,
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
|
pickCharacterReferenceFromLayer,
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
submitIconSpritesheetGeneration,
|
submitIconSpritesheetGeneration,
|
||||||
submitQuickEdit,
|
submitQuickEdit,
|
||||||
@@ -488,6 +494,7 @@ export function ImageCanvasEditorView() {
|
|||||||
updateIconDescription,
|
updateIconDescription,
|
||||||
addIconDescription,
|
addIconDescription,
|
||||||
updateCharacterAnimationDuration,
|
updateCharacterAnimationDuration,
|
||||||
|
rememberImageModel,
|
||||||
submitCharacterAnimation,
|
submitCharacterAnimation,
|
||||||
hideGeneratedLayerPanelAfterBlur,
|
hideGeneratedLayerPanelAfterBlur,
|
||||||
closeGenerateComposer,
|
closeGenerateComposer,
|
||||||
@@ -605,9 +612,11 @@ export function ImageCanvasEditorView() {
|
|||||||
generateDialog,
|
generateDialog,
|
||||||
setGenerateDialog,
|
setGenerateDialog,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
clearCanvasFocus,
|
clearCanvasFocus,
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
|
pickCharacterReferenceFromLayer,
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
activateCanvasGenerationDialog,
|
activateCanvasGenerationDialog,
|
||||||
updateCanvasGenerationDialogById,
|
updateCanvasGenerationDialogById,
|
||||||
@@ -636,7 +645,9 @@ export function ImageCanvasEditorView() {
|
|||||||
closeEditorChromePanels,
|
closeEditorChromePanels,
|
||||||
setIsSpecMenuOpen,
|
setIsSpecMenuOpen,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
setIsPickingIconSpecFromCanvas,
|
setIsPickingIconSpecFromCanvas,
|
||||||
setIsSpacePanning,
|
setIsSpacePanning,
|
||||||
@@ -1064,11 +1075,16 @@ export function ImageCanvasEditorView() {
|
|||||||
<ImageCanvasGenerationComposerView
|
<ImageCanvasGenerationComposerView
|
||||||
specToolWrapRef={specToolWrapRef}
|
specToolWrapRef={specToolWrapRef}
|
||||||
characterSpecButtonRef={characterSpecButtonRef}
|
characterSpecButtonRef={characterSpecButtonRef}
|
||||||
|
characterReferenceButtonRef={characterReferenceButtonRef}
|
||||||
iconSpecButtonRef={iconSpecButtonRef}
|
iconSpecButtonRef={iconSpecButtonRef}
|
||||||
isSpecMenuOpen={isSpecMenuOpen}
|
isSpecMenuOpen={isSpecMenuOpen}
|
||||||
isCharacterSpecMenuOpen={isCharacterSpecMenuOpen}
|
isCharacterSpecMenuOpen={isCharacterSpecMenuOpen}
|
||||||
|
isCharacterReferenceMenuOpen={isCharacterReferenceMenuOpen}
|
||||||
isIconSpecMenuOpen={isIconSpecMenuOpen}
|
isIconSpecMenuOpen={isIconSpecMenuOpen}
|
||||||
isPickingCharacterSpecFromCanvas={isPickingCharacterSpecFromCanvas}
|
isPickingCharacterSpecFromCanvas={isPickingCharacterSpecFromCanvas}
|
||||||
|
isPickingCharacterReferenceFromCanvas={
|
||||||
|
isPickingCharacterReferenceFromCanvas
|
||||||
|
}
|
||||||
isPickingIconSpecFromCanvas={isPickingIconSpecFromCanvas}
|
isPickingIconSpecFromCanvas={isPickingIconSpecFromCanvas}
|
||||||
generateDialog={generateDialog}
|
generateDialog={generateDialog}
|
||||||
generationComposerStyle={generationComposerStyle}
|
generationComposerStyle={generationComposerStyle}
|
||||||
@@ -1086,10 +1102,14 @@ export function ImageCanvasEditorView() {
|
|||||||
setQuickEditPanel={setQuickEditPanel}
|
setQuickEditPanel={setQuickEditPanel}
|
||||||
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
||||||
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
||||||
|
setIsCharacterReferenceMenuOpen={setIsCharacterReferenceMenuOpen}
|
||||||
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
||||||
setIsPickingCharacterSpecFromCanvas={
|
setIsPickingCharacterSpecFromCanvas={
|
||||||
setIsPickingCharacterSpecFromCanvas
|
setIsPickingCharacterSpecFromCanvas
|
||||||
}
|
}
|
||||||
|
setIsPickingCharacterReferenceFromCanvas={
|
||||||
|
setIsPickingCharacterReferenceFromCanvas
|
||||||
|
}
|
||||||
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
||||||
onOpenSpecDialog={openSpecDialog}
|
onOpenSpecDialog={openSpecDialog}
|
||||||
onRequestUpload={requestUpload}
|
onRequestUpload={requestUpload}
|
||||||
@@ -1118,6 +1138,7 @@ export function ImageCanvasEditorView() {
|
|||||||
onUpdateCharacterAnimationDuration={
|
onUpdateCharacterAnimationDuration={
|
||||||
updateCharacterAnimationDuration
|
updateCharacterAnimationDuration
|
||||||
}
|
}
|
||||||
|
onRememberImageModel={rememberImageModel}
|
||||||
/>
|
/>
|
||||||
</ImageCanvasStageView>
|
</ImageCanvasStageView>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ import {
|
|||||||
CHARACTER_ANIMATION_RATIO_OPTIONS,
|
CHARACTER_ANIMATION_RATIO_OPTIONS,
|
||||||
CHARACTER_SPEC_VIEW_OPTIONS,
|
CHARACTER_SPEC_VIEW_OPTIONS,
|
||||||
DEFAULT_ICON_DESCRIPTIONS,
|
DEFAULT_ICON_DESCRIPTIONS,
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||||
|
EDITOR_IMAGE_MODEL_OPTIONS,
|
||||||
ICON_DESCRIPTION_LIMIT,
|
ICON_DESCRIPTION_LIMIT,
|
||||||
|
IMAGE_MODEL_NANOBANANA2,
|
||||||
SPEC_GENERATION_COST,
|
SPEC_GENERATION_COST,
|
||||||
SPEC_TYPE_LABEL,
|
SPEC_TYPE_LABEL,
|
||||||
} from './ImageCanvasGenerationModel';
|
} from './ImageCanvasGenerationModel';
|
||||||
@@ -52,11 +55,14 @@ import type {
|
|||||||
type ImageCanvasGenerationComposerViewProps = {
|
type ImageCanvasGenerationComposerViewProps = {
|
||||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||||
|
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||||
isSpecMenuOpen: boolean;
|
isSpecMenuOpen: boolean;
|
||||||
isCharacterSpecMenuOpen: boolean;
|
isCharacterSpecMenuOpen: boolean;
|
||||||
|
isCharacterReferenceMenuOpen: boolean;
|
||||||
isIconSpecMenuOpen: boolean;
|
isIconSpecMenuOpen: boolean;
|
||||||
isPickingCharacterSpecFromCanvas: boolean;
|
isPickingCharacterSpecFromCanvas: boolean;
|
||||||
|
isPickingCharacterReferenceFromCanvas: boolean;
|
||||||
isPickingIconSpecFromCanvas: boolean;
|
isPickingIconSpecFromCanvas: boolean;
|
||||||
generateDialog: GenerateDialogState | null;
|
generateDialog: GenerateDialogState | null;
|
||||||
generationComposerStyle: CSSProperties | null;
|
generationComposerStyle: CSSProperties | null;
|
||||||
@@ -76,8 +82,10 @@ type ImageCanvasGenerationComposerViewProps = {
|
|||||||
SetStateAction<CharacterAnimationPanelState | null>
|
SetStateAction<CharacterAnimationPanelState | null>
|
||||||
>;
|
>;
|
||||||
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||||
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||||
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
||||||
onRequestUpload: (target: UploadTarget) => void;
|
onRequestUpload: (target: UploadTarget) => void;
|
||||||
@@ -90,6 +98,7 @@ type ImageCanvasGenerationComposerViewProps = {
|
|||||||
onUpdateIconDescription: (index: number, value: string) => void;
|
onUpdateIconDescription: (index: number, value: string) => void;
|
||||||
onAddIconDescription: () => void;
|
onAddIconDescription: () => void;
|
||||||
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
||||||
|
onRememberImageModel: (model: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
function triggerPlaceholderAction(label: string) {
|
function triggerPlaceholderAction(label: string) {
|
||||||
@@ -152,14 +161,161 @@ function resetFailedPanelStatus<T extends { status: string; errorMessage?: strin
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getImageDimensionOptions(model: string | null | undefined) {
|
||||||
|
return (
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||||
|
(model ?? IMAGE_MODEL_NANOBANANA2) as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||||
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[IMAGE_MODEL_NANOBANANA2]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeImageDialogSelection(dialog: GenerateDialogState) {
|
||||||
|
const model = dialog.imageModel ?? IMAGE_MODEL_NANOBANANA2;
|
||||||
|
const options = getImageDimensionOptions(model);
|
||||||
|
const aspectRatios = options.aspectRatios as readonly string[];
|
||||||
|
const imageSizes = options.imageSizes as readonly string[];
|
||||||
|
return {
|
||||||
|
model,
|
||||||
|
aspectRatio:
|
||||||
|
dialog.aspectRatio && aspectRatios.includes(dialog.aspectRatio)
|
||||||
|
? dialog.aspectRatio
|
||||||
|
: options.aspectRatios[0],
|
||||||
|
imageSize:
|
||||||
|
dialog.imageSize && imageSizes.includes(dialog.imageSize)
|
||||||
|
? dialog.imageSize
|
||||||
|
: (options.imageSizes.find((size) => size === '1K') ?? options.imageSizes[0]),
|
||||||
|
options,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderImageOptionButtons({
|
||||||
|
dialog,
|
||||||
|
setGenerateDialog,
|
||||||
|
includeDimensions,
|
||||||
|
onRememberImageModel,
|
||||||
|
}: {
|
||||||
|
dialog: GenerateDialogState;
|
||||||
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||||
|
includeDimensions: boolean;
|
||||||
|
onRememberImageModel: (model: string) => void;
|
||||||
|
}) {
|
||||||
|
const selection = normalizeImageDialogSelection(dialog);
|
||||||
|
const updateDialog = (patch: Partial<GenerateDialogState>) => {
|
||||||
|
setGenerateDialog((currentDialog) =>
|
||||||
|
currentDialog && currentDialog.mode === dialog.mode
|
||||||
|
? {
|
||||||
|
...resetFailedDialogStatus(currentDialog),
|
||||||
|
...patch,
|
||||||
|
}
|
||||||
|
: currentDialog,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{includeDimensions ? (
|
||||||
|
<>
|
||||||
|
<div className="image-canvas-editor__option-field">
|
||||||
|
<PlatformFieldLabel
|
||||||
|
variant="field"
|
||||||
|
className="image-canvas-editor__field-title"
|
||||||
|
>
|
||||||
|
画面比例
|
||||||
|
</PlatformFieldLabel>
|
||||||
|
<div className="image-canvas-editor__inline-option-group">
|
||||||
|
{selection.options.aspectRatios.map((aspectRatio) => (
|
||||||
|
<PlatformInlineOptionButton
|
||||||
|
key={aspectRatio}
|
||||||
|
className="image-canvas-editor__generation-ratio"
|
||||||
|
disabled={dialog.status === 'generating'}
|
||||||
|
aria-pressed={selection.aspectRatio === aspectRatio}
|
||||||
|
onClick={() => updateDialog({ aspectRatio })}
|
||||||
|
>
|
||||||
|
{aspectRatio}
|
||||||
|
</PlatformInlineOptionButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="image-canvas-editor__option-field">
|
||||||
|
<PlatformFieldLabel
|
||||||
|
variant="field"
|
||||||
|
className="image-canvas-editor__field-title"
|
||||||
|
>
|
||||||
|
大小尺寸
|
||||||
|
</PlatformFieldLabel>
|
||||||
|
<div className="image-canvas-editor__inline-option-group">
|
||||||
|
{selection.options.imageSizes.map((imageSize) => (
|
||||||
|
<PlatformInlineOptionButton
|
||||||
|
key={imageSize}
|
||||||
|
className="image-canvas-editor__generation-ratio"
|
||||||
|
disabled={dialog.status === 'generating'}
|
||||||
|
aria-pressed={selection.imageSize === imageSize}
|
||||||
|
onClick={() => updateDialog({ imageSize })}
|
||||||
|
>
|
||||||
|
{imageSize}
|
||||||
|
</PlatformInlineOptionButton>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<div className="image-canvas-editor__option-field">
|
||||||
|
<PlatformFieldLabel
|
||||||
|
variant="field"
|
||||||
|
className="image-canvas-editor__field-title"
|
||||||
|
>
|
||||||
|
模型
|
||||||
|
</PlatformFieldLabel>
|
||||||
|
<div className="image-canvas-editor__inline-option-group">
|
||||||
|
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => {
|
||||||
|
const nextOptions = getImageDimensionOptions(option.value);
|
||||||
|
const nextAspectRatios = nextOptions.aspectRatios as readonly string[];
|
||||||
|
const nextImageSizes = nextOptions.imageSizes as readonly string[];
|
||||||
|
return (
|
||||||
|
<PlatformInlineOptionButton
|
||||||
|
key={option.value}
|
||||||
|
className="image-canvas-editor__generation-model"
|
||||||
|
disabled={dialog.status === 'generating'}
|
||||||
|
aria-pressed={selection.model === option.value}
|
||||||
|
onClick={() => {
|
||||||
|
onRememberImageModel(option.value);
|
||||||
|
updateDialog({
|
||||||
|
imageModel: option.value,
|
||||||
|
aspectRatio:
|
||||||
|
dialog.aspectRatio &&
|
||||||
|
nextAspectRatios.includes(dialog.aspectRatio)
|
||||||
|
? dialog.aspectRatio
|
||||||
|
: nextOptions.aspectRatios[0],
|
||||||
|
imageSize:
|
||||||
|
dialog.imageSize &&
|
||||||
|
nextImageSizes.includes(dialog.imageSize)
|
||||||
|
? dialog.imageSize
|
||||||
|
: (nextOptions.imageSizes.find((size) => size === '1K') ??
|
||||||
|
nextOptions.imageSizes[0]),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</PlatformInlineOptionButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function ImageCanvasGenerationComposerView({
|
export function ImageCanvasGenerationComposerView({
|
||||||
specToolWrapRef,
|
specToolWrapRef,
|
||||||
characterSpecButtonRef,
|
characterSpecButtonRef,
|
||||||
|
characterReferenceButtonRef,
|
||||||
iconSpecButtonRef,
|
iconSpecButtonRef,
|
||||||
isSpecMenuOpen,
|
isSpecMenuOpen,
|
||||||
isCharacterSpecMenuOpen,
|
isCharacterSpecMenuOpen,
|
||||||
|
isCharacterReferenceMenuOpen,
|
||||||
isIconSpecMenuOpen,
|
isIconSpecMenuOpen,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
generateDialog,
|
generateDialog,
|
||||||
generationComposerStyle,
|
generationComposerStyle,
|
||||||
@@ -177,8 +333,10 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
setQuickEditPanel,
|
setQuickEditPanel,
|
||||||
setCharacterAnimationPanel,
|
setCharacterAnimationPanel,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
setIsPickingIconSpecFromCanvas,
|
setIsPickingIconSpecFromCanvas,
|
||||||
onOpenSpecDialog,
|
onOpenSpecDialog,
|
||||||
onRequestUpload,
|
onRequestUpload,
|
||||||
@@ -191,6 +349,7 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
onUpdateIconDescription,
|
onUpdateIconDescription,
|
||||||
onAddIconDescription,
|
onAddIconDescription,
|
||||||
onUpdateCharacterAnimationDuration,
|
onUpdateCharacterAnimationDuration,
|
||||||
|
onRememberImageModel,
|
||||||
}: ImageCanvasGenerationComposerViewProps) {
|
}: ImageCanvasGenerationComposerViewProps) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -544,10 +703,10 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
<PlatformFloatingMenu
|
<PlatformFloatingMenu
|
||||||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||||||
label="角色形象规范来源"
|
label="角色形象规范来源"
|
||||||
placement="bottom-start"
|
placement="top-start"
|
||||||
style={buildPortalMenuStyle(
|
style={buildPortalMenuStyle(
|
||||||
characterSpecButtonRef.current,
|
characterSpecButtonRef.current,
|
||||||
'below',
|
'above',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PlatformFloatingMenuItem
|
<PlatformFloatingMenuItem
|
||||||
@@ -603,10 +762,13 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
|
ref={characterReferenceButtonRef}
|
||||||
type="button"
|
type="button"
|
||||||
className="image-canvas-editor__character-reference-add image-canvas-editor__reference-tile image-canvas-editor__reference-tile--upload"
|
className="image-canvas-editor__character-reference-add image-canvas-editor__reference-tile image-canvas-editor__reference-tile--upload"
|
||||||
disabled={generateDialog.status === 'generating'}
|
disabled={generateDialog.status === 'generating'}
|
||||||
onClick={() => onRequestUpload('character-reference')}
|
onClick={() =>
|
||||||
|
setIsCharacterReferenceMenuOpen((open) => !open)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<span className="image-canvas-editor__reference-tile-visual">
|
<span className="image-canvas-editor__reference-tile-visual">
|
||||||
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
||||||
@@ -615,6 +777,38 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
上传常规参考图
|
上传常规参考图
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
{isCharacterReferenceMenuOpen
|
||||||
|
? renderEditorPortal(
|
||||||
|
<PlatformFloatingMenu
|
||||||
|
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||||||
|
label="常规参考图来源"
|
||||||
|
placement="top-start"
|
||||||
|
style={buildPortalMenuStyle(
|
||||||
|
characterReferenceButtonRef.current,
|
||||||
|
'above',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<PlatformFloatingMenuItem
|
||||||
|
className="image-canvas-editor__context-menu-item"
|
||||||
|
onClick={() => {
|
||||||
|
setIsPickingCharacterReferenceFromCanvas(true);
|
||||||
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
从画布中选择
|
||||||
|
</PlatformFloatingMenuItem>
|
||||||
|
<PlatformFloatingMenuItem
|
||||||
|
className="image-canvas-editor__context-menu-item"
|
||||||
|
onClick={() => {
|
||||||
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
|
onRequestUpload('character-reference');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
上传图片
|
||||||
|
</PlatformFloatingMenuItem>
|
||||||
|
</PlatformFloatingMenu>,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -657,36 +851,12 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
</PlatformStatusMessage>
|
</PlatformStatusMessage>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="image-canvas-editor__generation-composer-footer">
|
<div className="image-canvas-editor__generation-composer-footer">
|
||||||
<div className="image-canvas-editor__option-field">
|
{renderImageOptionButtons({
|
||||||
<PlatformFieldLabel
|
dialog: generateDialog,
|
||||||
variant="field"
|
setGenerateDialog,
|
||||||
className="image-canvas-editor__field-title"
|
includeDimensions: true,
|
||||||
>
|
onRememberImageModel,
|
||||||
画面比例
|
})}
|
||||||
</PlatformFieldLabel>
|
|
||||||
<PlatformInlineOptionButton
|
|
||||||
className="image-canvas-editor__generation-ratio"
|
|
||||||
disabled={generateDialog.status === 'generating'}
|
|
||||||
onClick={() => triggerPlaceholderAction('角色比例')}
|
|
||||||
>
|
|
||||||
1:1
|
|
||||||
</PlatformInlineOptionButton>
|
|
||||||
</div>
|
|
||||||
<div className="image-canvas-editor__option-field">
|
|
||||||
<PlatformFieldLabel
|
|
||||||
variant="field"
|
|
||||||
className="image-canvas-editor__field-title"
|
|
||||||
>
|
|
||||||
模型
|
|
||||||
</PlatformFieldLabel>
|
|
||||||
<PlatformInlineOptionButton
|
|
||||||
className="image-canvas-editor__generation-model"
|
|
||||||
disabled={generateDialog.status === 'generating'}
|
|
||||||
onClick={() => triggerPlaceholderAction('角色模型')}
|
|
||||||
>
|
|
||||||
GPT Image
|
|
||||||
</PlatformInlineOptionButton>
|
|
||||||
</div>
|
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
type="submit"
|
type="submit"
|
||||||
tone="secondary"
|
tone="secondary"
|
||||||
@@ -764,10 +934,10 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
<PlatformFloatingMenu
|
<PlatformFloatingMenu
|
||||||
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
||||||
label="图标素材规范来源"
|
label="图标素材规范来源"
|
||||||
placement="bottom-start"
|
placement="top-start"
|
||||||
style={buildPortalMenuStyle(
|
style={buildPortalMenuStyle(
|
||||||
iconSpecButtonRef.current,
|
iconSpecButtonRef.current,
|
||||||
'below',
|
'above',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<PlatformFloatingMenuItem
|
<PlatformFloatingMenuItem
|
||||||
@@ -897,21 +1067,12 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
>
|
>
|
||||||
添加素材描述
|
添加素材描述
|
||||||
</button>
|
</button>
|
||||||
<div className="image-canvas-editor__option-field">
|
{renderImageOptionButtons({
|
||||||
<PlatformFieldLabel
|
dialog: generateDialog,
|
||||||
variant="field"
|
setGenerateDialog,
|
||||||
className="image-canvas-editor__field-title"
|
includeDimensions: true,
|
||||||
>
|
onRememberImageModel,
|
||||||
模型
|
})}
|
||||||
</PlatformFieldLabel>
|
|
||||||
<PlatformInlineOptionButton
|
|
||||||
className="image-canvas-editor__generation-model"
|
|
||||||
disabled={generateDialog.status === 'generating'}
|
|
||||||
onClick={() => triggerPlaceholderAction('图标模型')}
|
|
||||||
>
|
|
||||||
nanobanana2
|
|
||||||
</PlatformInlineOptionButton>
|
|
||||||
</div>
|
|
||||||
<PlatformActionButton
|
<PlatformActionButton
|
||||||
type="submit"
|
type="submit"
|
||||||
tone="secondary"
|
tone="secondary"
|
||||||
@@ -932,6 +1093,11 @@ export function ImageCanvasGenerationComposerView({
|
|||||||
请选择画布中的图片作为角色形象规范,按 Esc 退出
|
请选择画布中的图片作为角色形象规范,按 Esc 退出
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isPickingCharacterReferenceFromCanvas ? (
|
||||||
|
<div className="image-canvas-editor__canvas-pick-hint">
|
||||||
|
请选择画布中的图片作为常规参考图,按 Esc 退出
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{isPickingIconSpecFromCanvas ? (
|
{isPickingIconSpecFromCanvas ? (
|
||||||
<div className="image-canvas-editor__canvas-pick-hint">
|
<div className="image-canvas-editor__canvas-pick-hint">
|
||||||
请选择画布中的图标素材规范,按 Esc 退出
|
请选择画布中的图标素材规范,按 Esc 退出
|
||||||
|
|||||||
@@ -104,10 +104,46 @@ describe('ImageCanvasGenerationModel', () => {
|
|||||||
.toBe('自定义');
|
.toBe('自定义');
|
||||||
expect(buildQuickEditModelOptions('nano-banana')).toEqual([
|
expect(buildQuickEditModelOptions('nano-banana')).toEqual([
|
||||||
{ label: 'nano-banana', value: 'nano-banana' },
|
{ label: 'nano-banana', value: 'nano-banana' },
|
||||||
{ label: 'GPT Image', value: DEFAULT_IMAGE_MODEL },
|
{ label: 'nanobanana2', value: DEFAULT_IMAGE_MODEL },
|
||||||
|
{ label: 'gpt-image-2', value: 'gpt-image-2' },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds reference image semantics and snapshots for spec generation references', () => {
|
||||||
|
const prompt = buildSpecPrompt(
|
||||||
|
'ui',
|
||||||
|
{
|
||||||
|
playSetting: '消除玩法',
|
||||||
|
artStyle: '清爽卡通',
|
||||||
|
bodyRatio: '3',
|
||||||
|
characterView: '',
|
||||||
|
customPrompt: '',
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(prompt).toContain('参考图生成规范');
|
||||||
|
expect(prompt).toContain('参考图1');
|
||||||
|
expect(prompt).toContain('生成一张完整游戏UI规范汇总设定展板');
|
||||||
|
expect(
|
||||||
|
buildSpecPrompt(
|
||||||
|
'custom',
|
||||||
|
{ ...blankSpecValues, customPrompt: '生成一张怪兽规范图' },
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
).toContain('生成一张怪兽规范图');
|
||||||
|
expect(
|
||||||
|
buildSpecGenerationInputs(
|
||||||
|
'custom',
|
||||||
|
{ ...blankSpecValues, customPrompt: '生成一张怪兽规范图' },
|
||||||
|
{ id: 'spec-ref', label: '参考.png', src: '/ref.png' },
|
||||||
|
),
|
||||||
|
).toEqual({
|
||||||
|
fields: [{ title: '自定义规范提示词', value: '生成一张怪兽规范图' }],
|
||||||
|
references: [{ title: '参考图', label: '参考.png', src: '/ref.png' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('uses objectKey for character animation references before falling back to src', () => {
|
it('uses objectKey for character animation references before falling back to src', () => {
|
||||||
expect(
|
expect(
|
||||||
resolveCharacterAnimationSourceImageSrc({
|
resolveCharacterAnimationSourceImageSrc({
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ export const CHARACTER_FRAME_ORIGINAL_SIZE = { width: 2048, height: 2048 };
|
|||||||
export const CHARACTER_FRAME_DISPLAY_SIZE = { width: 420, height: 420 };
|
export const CHARACTER_FRAME_DISPLAY_SIZE = { width: 420, height: 420 };
|
||||||
export const ICON_FRAME_ORIGINAL_SIZE = { width: 512, height: 512 };
|
export const ICON_FRAME_ORIGINAL_SIZE = { width: 512, height: 512 };
|
||||||
export const ICON_FRAME_DISPLAY_SIZE = { width: 360, height: 360 };
|
export const ICON_FRAME_DISPLAY_SIZE = { width: 360, height: 360 };
|
||||||
export const DEFAULT_IMAGE_MODEL = 'gpt-image-2';
|
export const IMAGE_MODEL_GPT_IMAGE_2 = 'gpt-image-2';
|
||||||
|
export const IMAGE_MODEL_NANOBANANA2 = 'gemini-3.1-flash-image-preview';
|
||||||
|
export const DEFAULT_IMAGE_MODEL = IMAGE_MODEL_NANOBANANA2;
|
||||||
export const ICON_DESCRIPTION_LIMIT = 100;
|
export const ICON_DESCRIPTION_LIMIT = 100;
|
||||||
// 图标素材面板按描述项扩宽,避免在画布子面板里做滑动列表。
|
// 图标素材面板按描述项扩宽,避免在画布子面板里做滑动列表。
|
||||||
export const ICON_DESCRIPTION_CARD_WIDTH_REM = 8.4;
|
export const ICON_DESCRIPTION_CARD_WIDTH_REM = 8.4;
|
||||||
@@ -44,8 +46,20 @@ export const QUICK_EDIT_SIZE_PRESETS = [
|
|||||||
'1024x1536',
|
'1024x1536',
|
||||||
] as const;
|
] as const;
|
||||||
export const QUICK_EDIT_MODEL_OPTIONS = [
|
export const QUICK_EDIT_MODEL_OPTIONS = [
|
||||||
{ label: 'GPT Image', value: DEFAULT_IMAGE_MODEL },
|
{ label: 'nanobanana2', value: IMAGE_MODEL_NANOBANANA2 },
|
||||||
|
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
|
||||||
] as const;
|
] as const;
|
||||||
|
export const EDITOR_IMAGE_MODEL_OPTIONS = QUICK_EDIT_MODEL_OPTIONS;
|
||||||
|
export const EDITOR_IMAGE_DIMENSION_OPTIONS = {
|
||||||
|
[IMAGE_MODEL_NANOBANANA2]: {
|
||||||
|
aspectRatios: ['1:1', '2:3', '3:2', '9:16', '16:9'],
|
||||||
|
imageSizes: ['0.5K', '1K', '2K'],
|
||||||
|
},
|
||||||
|
[IMAGE_MODEL_GPT_IMAGE_2]: {
|
||||||
|
aspectRatios: ['1:1', '2:3', '3:2', '9:16', '16:9'],
|
||||||
|
imageSizes: ['1K', '2K'],
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
export const CHARACTER_ANIMATION_MODEL = 'seedance2.0';
|
export const CHARACTER_ANIMATION_MODEL = 'seedance2.0';
|
||||||
export const CHARACTER_ANIMATION_ACTION_PROMPTS = [
|
export const CHARACTER_ANIMATION_ACTION_PROMPTS = [
|
||||||
{ label: '待机', text: '待机动作,轻微呼吸起伏。' },
|
{ label: '待机', text: '待机动作,轻微呼吸起伏。' },
|
||||||
@@ -169,17 +183,23 @@ export function buildIconSpecPrompt(values: SpecFormValues) {
|
|||||||
export function buildSpecPrompt(
|
export function buildSpecPrompt(
|
||||||
type: SpecGenerationType,
|
type: SpecGenerationType,
|
||||||
values: SpecFormValues,
|
values: SpecFormValues,
|
||||||
|
hasReferenceImage = false,
|
||||||
) {
|
) {
|
||||||
if (type === 'character') {
|
const prompt =
|
||||||
return buildCharacterSpecPrompt(values);
|
type === 'character'
|
||||||
|
? buildCharacterSpecPrompt(values)
|
||||||
|
: type === 'ui'
|
||||||
|
? buildUiSpecPrompt(values)
|
||||||
|
: type === 'icon'
|
||||||
|
? buildIconSpecPrompt(values)
|
||||||
|
: values.customPrompt.trim();
|
||||||
|
if (!hasReferenceImage) {
|
||||||
|
return prompt;
|
||||||
}
|
}
|
||||||
if (type === 'ui') {
|
return [
|
||||||
return buildUiSpecPrompt(values);
|
'参考图生成规范:严格参考图1的构图、风格、材质、色彩、形状语言和视觉层级生成本次规范图;参考图只作为美术方向和规范语义依据,不要直接复制参考图中的文字、水印或无关背景。',
|
||||||
}
|
prompt,
|
||||||
if (type === 'icon') {
|
].join('\n');
|
||||||
return buildIconSpecPrompt(values);
|
|
||||||
}
|
|
||||||
return values.customPrompt.trim();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLayerKindLabel(layer: CanvasLayer) {
|
export function getLayerKindLabel(layer: CanvasLayer) {
|
||||||
@@ -254,11 +274,15 @@ export function buildImageGenerationInputs(prompt: string): CanvasGenerationInpu
|
|||||||
export function buildSpecGenerationInputs(
|
export function buildSpecGenerationInputs(
|
||||||
specType: SpecGenerationType,
|
specType: SpecGenerationType,
|
||||||
values: SpecFormValues,
|
values: SpecFormValues,
|
||||||
|
reference?: CharacterReferenceImage | null,
|
||||||
): CanvasGenerationInputs {
|
): CanvasGenerationInputs {
|
||||||
|
const references = reference
|
||||||
|
? [{ title: '参考图', label: reference.label, src: reference.src }]
|
||||||
|
: [];
|
||||||
if (specType === 'custom') {
|
if (specType === 'custom') {
|
||||||
return {
|
return {
|
||||||
fields: createGenerationInputField('自定义规范提示词', values.customPrompt),
|
fields: createGenerationInputField('自定义规范提示词', values.customPrompt),
|
||||||
references: [],
|
references,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,7 +298,7 @@ export function buildSpecGenerationInputs(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
fields: baseFields,
|
fields: baseFields,
|
||||||
references: [],
|
references,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
DEFAULT_ICON_DESCRIPTIONS,
|
DEFAULT_ICON_DESCRIPTIONS,
|
||||||
DEFAULT_IMAGE_MODEL,
|
DEFAULT_IMAGE_MODEL,
|
||||||
DEFAULT_SPEC_FORM_VALUES,
|
DEFAULT_SPEC_FORM_VALUES,
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||||
ICON_DESCRIPTION_LIMIT,
|
ICON_DESCRIPTION_LIMIT,
|
||||||
ICON_FRAME_DISPLAY_SIZE,
|
ICON_FRAME_DISPLAY_SIZE,
|
||||||
ICON_FRAME_ORIGINAL_SIZE,
|
ICON_FRAME_ORIGINAL_SIZE,
|
||||||
@@ -154,10 +155,16 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
||||||
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] =
|
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
||||||
|
useState(false);
|
||||||
const [
|
const [
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
] = useState(false);
|
] = useState(false);
|
||||||
|
const [
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
|
] = useState(false);
|
||||||
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
|
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
|
||||||
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
||||||
useState(false);
|
useState(false);
|
||||||
@@ -165,6 +172,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
useState<QuickEditPanelState | null>(null);
|
useState<QuickEditPanelState | null>(null);
|
||||||
const [characterAnimationPanel, setCharacterAnimationPanel] =
|
const [characterAnimationPanel, setCharacterAnimationPanel] =
|
||||||
useState<CharacterAnimationPanelState | null>(null);
|
useState<CharacterAnimationPanelState | null>(null);
|
||||||
|
const [lastImageModel, setLastImageModel] = useState(DEFAULT_IMAGE_MODEL);
|
||||||
|
|
||||||
const quickEditSourceLayer = quickEditPanel
|
const quickEditSourceLayer = quickEditPanel
|
||||||
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
|
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
|
||||||
@@ -278,7 +286,13 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const openCharacterGenerationDialog = useCallback(() => {
|
const openCharacterGenerationDialog = useCallback(() => {
|
||||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||||
setIsSpecMenuOpen(false);
|
setIsSpecMenuOpen(false);
|
||||||
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
|
const dimensionOptions =
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||||
|
lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||||
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
||||||
openCanvasGenerationDialog({
|
openCanvasGenerationDialog({
|
||||||
mode: 'character',
|
mode: 'character',
|
||||||
prompt: '',
|
prompt: '',
|
||||||
@@ -286,6 +300,11 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
composerOpen: true,
|
composerOpen: true,
|
||||||
characterSpecReference: null,
|
characterSpecReference: null,
|
||||||
characterReferences: [],
|
characterReferences: [],
|
||||||
|
imageModel: lastImageModel,
|
||||||
|
aspectRatio: dimensionOptions.aspectRatios[0],
|
||||||
|
imageSize:
|
||||||
|
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
||||||
|
dimensionOptions.imageSizes[0],
|
||||||
placeholder: {
|
placeholder: {
|
||||||
x: worldCenter.x - CHARACTER_FRAME_DISPLAY_SIZE.width / 2,
|
x: worldCenter.x - CHARACTER_FRAME_DISPLAY_SIZE.width / 2,
|
||||||
y: worldCenter.y - CHARACTER_FRAME_DISPLAY_SIZE.height / 2,
|
y: worldCenter.y - CHARACTER_FRAME_DISPLAY_SIZE.height / 2,
|
||||||
@@ -300,6 +319,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
}, [
|
}, [
|
||||||
canvasSize,
|
canvasSize,
|
||||||
|
lastImageModel,
|
||||||
openCanvasGenerationDialog,
|
openCanvasGenerationDialog,
|
||||||
selectSingleLayer,
|
selectSingleLayer,
|
||||||
setActiveTool,
|
setActiveTool,
|
||||||
@@ -309,8 +329,14 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const openIconGenerationDialog = useCallback(() => {
|
const openIconGenerationDialog = useCallback(() => {
|
||||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||||
setIsSpecMenuOpen(false);
|
setIsSpecMenuOpen(false);
|
||||||
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
|
const dimensionOptions =
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||||
|
lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||||
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
||||||
openCanvasGenerationDialog({
|
openCanvasGenerationDialog({
|
||||||
mode: 'icon',
|
mode: 'icon',
|
||||||
prompt: '',
|
prompt: '',
|
||||||
@@ -318,6 +344,11 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
composerOpen: true,
|
composerOpen: true,
|
||||||
iconSpecReference: null,
|
iconSpecReference: null,
|
||||||
iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS],
|
iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS],
|
||||||
|
imageModel: lastImageModel,
|
||||||
|
aspectRatio: dimensionOptions.aspectRatios[0],
|
||||||
|
imageSize:
|
||||||
|
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
||||||
|
dimensionOptions.imageSizes[0],
|
||||||
placeholder: {
|
placeholder: {
|
||||||
x: worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2,
|
x: worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2,
|
||||||
y: worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2,
|
y: worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2,
|
||||||
@@ -333,6 +364,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
setCharacterAnimationPanel(null);
|
setCharacterAnimationPanel(null);
|
||||||
}, [
|
}, [
|
||||||
canvasSize,
|
canvasSize,
|
||||||
|
lastImageModel,
|
||||||
openCanvasGenerationDialog,
|
openCanvasGenerationDialog,
|
||||||
selectSingleLayer,
|
selectSingleLayer,
|
||||||
setActiveTool,
|
setActiveTool,
|
||||||
@@ -543,6 +575,26 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
[setGenerateDialog, setImageContextMenu],
|
[setGenerateDialog, setImageContextMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pickCharacterReferenceFromLayer = useCallback(
|
||||||
|
(layer: CanvasLayer) => {
|
||||||
|
setGenerateDialog((currentDialog) =>
|
||||||
|
currentDialog?.mode === 'character'
|
||||||
|
? {
|
||||||
|
...setFailedCharacterGenerationIdle(currentDialog),
|
||||||
|
characterReferences: [
|
||||||
|
...(currentDialog.characterReferences ?? []),
|
||||||
|
createCanvasLayerReference(layer),
|
||||||
|
],
|
||||||
|
composerOpen: true,
|
||||||
|
}
|
||||||
|
: currentDialog,
|
||||||
|
);
|
||||||
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
|
setImageContextMenu(null);
|
||||||
|
},
|
||||||
|
[setGenerateDialog, setImageContextMenu],
|
||||||
|
);
|
||||||
|
|
||||||
const pickIconSpecFromLayer = useCallback(
|
const pickIconSpecFromLayer = useCallback(
|
||||||
(layer: CanvasLayer) => {
|
(layer: CanvasLayer) => {
|
||||||
if (layer.assetKind !== 'icon-spec') {
|
if (layer.assetKind !== 'icon-spec') {
|
||||||
@@ -654,7 +706,11 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const generated = await generateEditorIconSpritesheet({
|
const generated = await generateEditorIconSpritesheet({
|
||||||
referenceImageSrc: dialog.iconSpecReference.src,
|
referenceImageSrc: dialog.iconSpecReference.src,
|
||||||
iconDescriptions,
|
iconDescriptions,
|
||||||
|
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL,
|
||||||
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||||
|
imageSize: dialog.imageSize ?? '1K',
|
||||||
});
|
});
|
||||||
|
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
|
||||||
addIconSpritesheetResultLayers(
|
addIconSpritesheetResultLayers(
|
||||||
generated,
|
generated,
|
||||||
generated.iconImageSrcs,
|
generated.iconImageSrcs,
|
||||||
@@ -795,8 +851,12 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const generated = await generateEditorImage({
|
const generated = await generateEditorImage({
|
||||||
prompt: normalizedPrompt,
|
prompt: normalizedPrompt,
|
||||||
kind: 'character',
|
kind: 'character',
|
||||||
|
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL,
|
||||||
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||||
|
imageSize: dialog.imageSize ?? '1K',
|
||||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||||
});
|
});
|
||||||
|
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
|
||||||
addGeneratedResultLayer(generated, {
|
addGeneratedResultLayer(generated, {
|
||||||
frame: getGeneratingDialogPlaceholder(dialog),
|
frame: getGeneratingDialogPlaceholder(dialog),
|
||||||
assetKind: 'character',
|
assetKind: 'character',
|
||||||
@@ -1021,8 +1081,12 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
setIsSpecMenuOpen,
|
setIsSpecMenuOpen,
|
||||||
isCharacterSpecMenuOpen,
|
isCharacterSpecMenuOpen,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
isCharacterReferenceMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
isIconSpecMenuOpen,
|
isIconSpecMenuOpen,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
@@ -1035,6 +1099,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
openEditDialog,
|
openEditDialog,
|
||||||
openQuickEditPanel,
|
openQuickEditPanel,
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
|
pickCharacterReferenceFromLayer,
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
submitIconSpritesheetGeneration,
|
submitIconSpritesheetGeneration,
|
||||||
submitQuickEdit,
|
submitQuickEdit,
|
||||||
@@ -1043,6 +1108,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
updateIconDescription,
|
updateIconDescription,
|
||||||
addIconDescription,
|
addIconDescription,
|
||||||
updateCharacterAnimationDuration,
|
updateCharacterAnimationDuration,
|
||||||
|
rememberImageModel: setLastImageModel,
|
||||||
submitCharacterAnimation,
|
submitCharacterAnimation,
|
||||||
hideGeneratedLayerPanelAfterBlur,
|
hideGeneratedLayerPanelAfterBlur,
|
||||||
closeGenerateComposer,
|
closeGenerateComposer,
|
||||||
@@ -1057,8 +1123,10 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
closeGenerateComposer,
|
closeGenerateComposer,
|
||||||
hideGeneratedLayerPanelAfterBlur,
|
hideGeneratedLayerPanelAfterBlur,
|
||||||
iconDescriptionValues,
|
iconDescriptionValues,
|
||||||
|
isCharacterReferenceMenuOpen,
|
||||||
isCharacterSpecMenuOpen,
|
isCharacterSpecMenuOpen,
|
||||||
isIconSpecMenuOpen,
|
isIconSpecMenuOpen,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
isSpecMenuOpen,
|
isSpecMenuOpen,
|
||||||
@@ -1069,6 +1137,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
openIconGenerationDialog,
|
openIconGenerationDialog,
|
||||||
openQuickEditPanel,
|
openQuickEditPanel,
|
||||||
openSpecDialog,
|
openSpecDialog,
|
||||||
|
pickCharacterReferenceFromLayer,
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
quickEditModelOptions,
|
quickEditModelOptions,
|
||||||
|
|||||||
@@ -79,8 +79,14 @@ function KeyboardShortcutsHarness({
|
|||||||
const [contextMenuOpen, setContextMenuOpen] = useState(true);
|
const [contextMenuOpen, setContextMenuOpen] = useState(true);
|
||||||
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(true);
|
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(true);
|
||||||
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(true);
|
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(true);
|
||||||
|
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
||||||
|
useState(true);
|
||||||
const [isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas] =
|
const [isPickingCharacterSpecFromCanvas, setIsPickingCharacterSpecFromCanvas] =
|
||||||
useState(true);
|
useState(true);
|
||||||
|
const [
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
|
] = useState(true);
|
||||||
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(true);
|
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(true);
|
||||||
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
||||||
useState(true);
|
useState(true);
|
||||||
@@ -111,7 +117,9 @@ function KeyboardShortcutsHarness({
|
|||||||
closeEditorChromePanels,
|
closeEditorChromePanels,
|
||||||
setIsSpecMenuOpen,
|
setIsSpecMenuOpen,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
setIsPickingIconSpecFromCanvas,
|
setIsPickingIconSpecFromCanvas,
|
||||||
setIsSpacePanning,
|
setIsSpacePanning,
|
||||||
@@ -138,9 +146,15 @@ function KeyboardShortcutsHarness({
|
|||||||
<span data-testid="character-menu">
|
<span data-testid="character-menu">
|
||||||
{String(isCharacterSpecMenuOpen)}
|
{String(isCharacterSpecMenuOpen)}
|
||||||
</span>
|
</span>
|
||||||
|
<span data-testid="character-reference-menu">
|
||||||
|
{String(isCharacterReferenceMenuOpen)}
|
||||||
|
</span>
|
||||||
<span data-testid="character-picking">
|
<span data-testid="character-picking">
|
||||||
{String(isPickingCharacterSpecFromCanvas)}
|
{String(isPickingCharacterSpecFromCanvas)}
|
||||||
</span>
|
</span>
|
||||||
|
<span data-testid="character-reference-picking">
|
||||||
|
{String(isPickingCharacterReferenceFromCanvas)}
|
||||||
|
</span>
|
||||||
<span data-testid="icon-menu">{String(isIconSpecMenuOpen)}</span>
|
<span data-testid="icon-menu">{String(isIconSpecMenuOpen)}</span>
|
||||||
<span data-testid="icon-picking">
|
<span data-testid="icon-picking">
|
||||||
{String(isPickingIconSpecFromCanvas)}
|
{String(isPickingIconSpecFromCanvas)}
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ type UseImageCanvasKeyboardShortcutsOptions = {
|
|||||||
closeEditorChromePanels: () => void;
|
closeEditorChromePanels: () => void;
|
||||||
setIsSpecMenuOpen: (open: boolean) => void;
|
setIsSpecMenuOpen: (open: boolean) => void;
|
||||||
setIsCharacterSpecMenuOpen: (open: boolean) => void;
|
setIsCharacterSpecMenuOpen: (open: boolean) => void;
|
||||||
|
setIsCharacterReferenceMenuOpen: (open: boolean) => void;
|
||||||
setIsPickingCharacterSpecFromCanvas: (picking: boolean) => void;
|
setIsPickingCharacterSpecFromCanvas: (picking: boolean) => void;
|
||||||
|
setIsPickingCharacterReferenceFromCanvas: (picking: boolean) => void;
|
||||||
setIsIconSpecMenuOpen: (open: boolean) => void;
|
setIsIconSpecMenuOpen: (open: boolean) => void;
|
||||||
setIsPickingIconSpecFromCanvas: (picking: boolean) => void;
|
setIsPickingIconSpecFromCanvas: (picking: boolean) => void;
|
||||||
setIsSpacePanning: (panning: boolean) => void;
|
setIsSpacePanning: (panning: boolean) => void;
|
||||||
@@ -77,7 +79,9 @@ export function useImageCanvasKeyboardShortcuts({
|
|||||||
closeEditorChromePanels,
|
closeEditorChromePanels,
|
||||||
setIsSpecMenuOpen,
|
setIsSpecMenuOpen,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
setIsPickingIconSpecFromCanvas,
|
setIsPickingIconSpecFromCanvas,
|
||||||
setIsSpacePanning,
|
setIsSpacePanning,
|
||||||
@@ -93,7 +97,9 @@ export function useImageCanvasKeyboardShortcuts({
|
|||||||
currentPanel?.status === 'generating' ? currentPanel : null,
|
currentPanel?.status === 'generating' ? currentPanel : null,
|
||||||
);
|
);
|
||||||
setIsCharacterSpecMenuOpen(false);
|
setIsCharacterSpecMenuOpen(false);
|
||||||
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
setIsIconSpecMenuOpen(false);
|
setIsIconSpecMenuOpen(false);
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
setGenerateDialog((currentDialog) => {
|
setGenerateDialog((currentDialog) => {
|
||||||
@@ -149,7 +155,9 @@ export function useImageCanvasKeyboardShortcuts({
|
|||||||
setGenerateDialog(null);
|
setGenerateDialog(null);
|
||||||
setActiveTool('select');
|
setActiveTool('select');
|
||||||
setIsCharacterSpecMenuOpen(false);
|
setIsCharacterSpecMenuOpen(false);
|
||||||
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
setIsIconSpecMenuOpen(false);
|
setIsIconSpecMenuOpen(false);
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
return;
|
return;
|
||||||
@@ -193,7 +201,9 @@ export function useImageCanvasKeyboardShortcuts({
|
|||||||
setGenerateDialog,
|
setGenerateDialog,
|
||||||
setImageContextMenu,
|
setImageContextMenu,
|
||||||
setIsCharacterSpecMenuOpen,
|
setIsCharacterSpecMenuOpen,
|
||||||
|
setIsCharacterReferenceMenuOpen,
|
||||||
setIsIconSpecMenuOpen,
|
setIsIconSpecMenuOpen,
|
||||||
|
setIsPickingCharacterReferenceFromCanvas,
|
||||||
setIsPickingCharacterSpecFromCanvas,
|
setIsPickingCharacterSpecFromCanvas,
|
||||||
setIsPickingIconSpecFromCanvas,
|
setIsPickingIconSpecFromCanvas,
|
||||||
setIsSpacePanning,
|
setIsSpacePanning,
|
||||||
|
|||||||
@@ -175,6 +175,7 @@ function StageInteractionsHarness({
|
|||||||
generateDialog,
|
generateDialog,
|
||||||
setGenerateDialog,
|
setGenerateDialog,
|
||||||
isPickingCharacterSpecFromCanvas: false,
|
isPickingCharacterSpecFromCanvas: false,
|
||||||
|
isPickingCharacterReferenceFromCanvas: false,
|
||||||
isPickingIconSpecFromCanvas: false,
|
isPickingIconSpecFromCanvas: false,
|
||||||
clearCanvasFocus: () => {
|
clearCanvasFocus: () => {
|
||||||
setSelectedLayerId(null);
|
setSelectedLayerId(null);
|
||||||
@@ -182,6 +183,7 @@ function StageInteractionsHarness({
|
|||||||
setClearCount((currentCount) => currentCount + 1);
|
setClearCount((currentCount) => currentCount + 1);
|
||||||
},
|
},
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
|
pickCharacterReferenceFromLayer: vi.fn(),
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
activateCanvasGenerationDialog,
|
activateCanvasGenerationDialog,
|
||||||
updateCanvasGenerationDialogById: (dialogId, updater) => {
|
updateCanvasGenerationDialogById: (dialogId, updater) => {
|
||||||
|
|||||||
@@ -45,9 +45,11 @@ type UseImageCanvasStageInteractionsOptions = {
|
|||||||
generateDialog: GenerateDialogState | null;
|
generateDialog: GenerateDialogState | null;
|
||||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||||
isPickingCharacterSpecFromCanvas: boolean;
|
isPickingCharacterSpecFromCanvas: boolean;
|
||||||
|
isPickingCharacterReferenceFromCanvas: boolean;
|
||||||
isPickingIconSpecFromCanvas: boolean;
|
isPickingIconSpecFromCanvas: boolean;
|
||||||
clearCanvasFocus: () => void;
|
clearCanvasFocus: () => void;
|
||||||
pickCharacterSpecFromLayer: (layer: CanvasLayer) => void;
|
pickCharacterSpecFromLayer: (layer: CanvasLayer) => void;
|
||||||
|
pickCharacterReferenceFromLayer: (layer: CanvasLayer) => void;
|
||||||
pickIconSpecFromLayer: (layer: CanvasLayer) => void;
|
pickIconSpecFromLayer: (layer: CanvasLayer) => void;
|
||||||
activateCanvasGenerationDialog: (
|
activateCanvasGenerationDialog: (
|
||||||
dialog: CanvasGenerationDialogState,
|
dialog: CanvasGenerationDialogState,
|
||||||
@@ -126,9 +128,11 @@ export function useImageCanvasStageInteractions({
|
|||||||
generateDialog,
|
generateDialog,
|
||||||
setGenerateDialog,
|
setGenerateDialog,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
clearCanvasFocus,
|
clearCanvasFocus,
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
|
pickCharacterReferenceFromLayer,
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
activateCanvasGenerationDialog,
|
activateCanvasGenerationDialog,
|
||||||
updateCanvasGenerationDialogById,
|
updateCanvasGenerationDialogById,
|
||||||
@@ -139,6 +143,7 @@ export function useImageCanvasStageInteractions({
|
|||||||
}: UseImageCanvasStageInteractionsOptions) {
|
}: UseImageCanvasStageInteractionsOptions) {
|
||||||
const dragStateRef = useRef<DragState | null>(null);
|
const dragStateRef = useRef<DragState | null>(null);
|
||||||
const isShiftPressedRef = useRef(false);
|
const isShiftPressedRef = useRef(false);
|
||||||
|
const suppressNextLayerClickRef = useRef(false);
|
||||||
const [canvasMarquee, setCanvasMarquee] = useState<CanvasMarqueeState | null>(
|
const [canvasMarquee, setCanvasMarquee] = useState<CanvasMarqueeState | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
@@ -230,12 +235,24 @@ export function useImageCanvasStageInteractions({
|
|||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
suppressNextLayerClickRef.current = true;
|
||||||
pickCharacterSpecFromLayer(layer);
|
pickCharacterSpecFromLayer(layer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
isPickingCharacterReferenceFromCanvas &&
|
||||||
|
generateDialog?.mode === 'character'
|
||||||
|
) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
suppressNextLayerClickRef.current = true;
|
||||||
|
pickCharacterReferenceFromLayer(layer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isPickingIconSpecFromCanvas && generateDialog?.mode === 'icon') {
|
if (isPickingIconSpecFromCanvas && generateDialog?.mode === 'icon') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
suppressNextLayerClickRef.current = true;
|
||||||
pickIconSpecFromLayer(layer);
|
pickIconSpecFromLayer(layer);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -302,8 +319,10 @@ export function useImageCanvasStageInteractions({
|
|||||||
effectiveTool,
|
effectiveTool,
|
||||||
generateDialog?.mode,
|
generateDialog?.mode,
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
layers,
|
layers,
|
||||||
|
pickCharacterReferenceFromLayer,
|
||||||
pickCharacterSpecFromLayer,
|
pickCharacterSpecFromLayer,
|
||||||
pickIconSpecFromLayer,
|
pickIconSpecFromLayer,
|
||||||
selectedLayerIds,
|
selectedLayerIds,
|
||||||
@@ -320,9 +339,16 @@ export function useImageCanvasStageInteractions({
|
|||||||
// 测试环境和辅助技术可能只触发 click;
|
// 测试环境和辅助技术可能只触发 click;
|
||||||
// 用 click 兜底选中,真实拖拽仍由 pointerDown 负责。
|
// 用 click 兜底选中,真实拖拽仍由 pointerDown 负责。
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
if (suppressNextLayerClickRef.current) {
|
||||||
|
suppressNextLayerClickRef.current = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isPickingCharacterSpecFromCanvas) {
|
if (isPickingCharacterSpecFromCanvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isPickingCharacterReferenceFromCanvas) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isPickingIconSpecFromCanvas) {
|
if (isPickingIconSpecFromCanvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -346,6 +372,7 @@ export function useImageCanvasStageInteractions({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
isPickingCharacterSpecFromCanvas,
|
isPickingCharacterSpecFromCanvas,
|
||||||
|
isPickingCharacterReferenceFromCanvas,
|
||||||
isPickingIconSpecFromCanvas,
|
isPickingIconSpecFromCanvas,
|
||||||
onCloseImageContextMenu,
|
onCloseImageContextMenu,
|
||||||
setGenerateDialog,
|
setGenerateDialog,
|
||||||
|
|||||||
Reference in New Issue
Block a user