diff --git a/TRACKING.md b/TRACKING.md index 22b29566..3bdc31d1 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -154,3 +154,4 @@ - 2026-06-17 前端拆分第三十五阶段:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationSubmissionModel`,把普通生图、修改图片、规范生成和角色形象生成的请求 payload、标准化 prompt、结果图层标题 / assetKind 和 generationInputs 快照构建从 workflow hook 中抽成纯模型;workflow hook 保留对话状态、真实 API 调用、图片引用解析、结果落图、选中和 fit 副作用,避免拆散生成生命周期。新增模型单测覆盖普通生图、修改图、带参考图的规范生成、带规范 / 常规参考图的角色生成和缺失源图异常;`useImageCanvasGenerationWorkflow` 从 1167 行降至 1104 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 - 2026-06-17 前端拆分第三十六阶段:继续收口 `useImageCanvasUploadWorkflow`,新增 `ImageCanvasUploadModel`,把上传目标文件夹解析、上传中素材占位卡、上传到画布的临时图层、无效 drop 坐标兜底和图片真实尺寸回填坐标计算从 hook 中抽成纯模型;upload workflow hook 保留登录恢复、文件读取、真实素材创建 API、上传进度状态和生成面板参考图写入副作用。新增模型单测覆盖文件夹兜底、占位素材、画布落点、非法坐标兜底和真实尺寸修正;`useImageCanvasUploadWorkflow` 从 546 行降至 510 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框、`上传到项目素材` 入口和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 - 2026-06-17 前端拆分第三十七阶段:继续收口 `useImageCanvasAssetLibrary`,新增 `ImageCanvasAssetLibraryModel`,把素材分组、可选择素材筛选、全选状态、素材 / 文件夹重命名、文件夹折叠、本地新建文件夹占位、持久化文件夹替换、本地删除素材、删除文件夹回默认文件夹、选择集合切换、批量删除和本地移动素材到文件夹从 hook 中抽成纯模型;asset library hook 继续保留加载账号素材库、后端 CRUD 调用、登录弹窗、DOM 框选和素材拖拽命中生命周期。新增模型单测覆盖分组 / 选择、重命名 / 折叠 / 本地文件夹、本地文件夹持久化替换、删除文件夹回默认文件夹、全选 / 批量删除和本地移动;`useImageCanvasAssetLibrary` 从 609 行降至 573 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;切回 `打开素材` 后侧栏显示 `素材` 且 `上传到项目素材` 入口可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 +- 2026-06-17 前端拆分第三十八阶段:继续收口 `useImageCanvasGenerationWorkflow`,扩展 `ImageCanvasGenerationSubmissionModel`,把图标素材批量生成的规范校验 / 描述清洗 / 请求 payload / generationInputs,以及角色动画生成的 prompt 清洗 / objectKey 优先源图 / 尺寸 / 价格 / 模型参数从 workflow hook 中抽成纯模型;workflow hook 继续保留对话状态、真实 API 调用、生成结果落图、失败恢复和角色动画面板生命周期。新增模型单测覆盖图标缺少规范、图标空描述、图标描述 trim / 参考快照,以及角色动画 trim、objectKey 源图和价格计算;`useImageCanvasGenerationWorkflow` 从 1104 行降至 1075 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;点击 `生成图标素材` 后 `Icon Generator` 占位和 `生成图标素材` 面板可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts index 01f56579..56b4d1f1 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts @@ -1,7 +1,11 @@ import { describe, expect, it } from 'vitest'; import type { CanvasLayer } from './ImageCanvasEditorTypes'; -import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel'; +import { + buildCharacterAnimationSubmissionPlan, + buildIconSpritesheetGenerationSubmissionPlan, + buildImageGenerationSubmissionPlan, +} from './ImageCanvasGenerationSubmissionModel'; function createLayer(overrides: Partial = {}): CanvasLayer { return { @@ -209,4 +213,121 @@ describe('ImageCanvasGenerationSubmissionModel', () => { }), ).toThrow('未找到要修改的图片'); }); + + it('returns an icon spritesheet error when the spec reference is missing', () => { + const plan = buildIconSpritesheetGenerationSubmissionPlan({ + mode: 'icon', + prompt: '', + status: 'idle', + iconDescriptions: ['返回按钮'], + }); + + expect(plan).toEqual({ + ok: false, + errorMessage: '请选择图标素材规范', + }); + }); + + it('returns an icon spritesheet error when descriptions are empty', () => { + const plan = buildIconSpritesheetGenerationSubmissionPlan({ + mode: 'icon', + prompt: '', + status: 'idle', + iconSpecReference: { + id: 'icon-spec', + label: '图标规范', + src: 'data:image/png;base64,spec', + }, + iconDescriptions: [' ', '\n'], + }); + + expect(plan).toEqual({ + ok: false, + errorMessage: '请填写素材描述', + }); + }); + + it('builds icon spritesheet plans with trimmed descriptions and references', () => { + const plan = buildIconSpritesheetGenerationSubmissionPlan({ + mode: 'icon', + prompt: '', + status: 'idle', + imageModel: 'gpt-image-2', + aspectRatio: '3:2', + imageSize: '2K', + iconSpecReference: { + id: 'icon-spec', + label: '图标规范', + src: 'data:image/png;base64,spec', + }, + iconDescriptions: [' 返回按钮 ', '', '设置按钮'], + }); + + expect(plan).toEqual({ + ok: true, + iconDescriptions: ['返回按钮', '设置按钮'], + input: { + referenceImageSrc: 'data:image/png;base64,spec', + iconDescriptions: ['返回按钮', '设置按钮'], + model: 'gpt-image-2', + aspectRatio: '3:2', + imageSize: '2K', + }, + generationInputs: { + fields: [ + { title: '素材描述 1', value: '返回按钮' }, + { title: '素材描述 2', value: '设置按钮' }, + ], + references: [ + { + title: '图标素材规范', + label: '图标规范', + src: 'data:image/png;base64,spec', + }, + ], + }, + rememberImageModel: 'gpt-image-2', + }); + }); + + it('builds character animation plans with trimmed prompt and object key source', () => { + const sourceLayer = createLayer({ + id: 'character-layer', + title: '角色图', + objectKey: 'generated/character.png', + assetKind: 'character', + originalWidth: 960, + originalHeight: 1280, + }); + + const plan = buildCharacterAnimationSubmissionPlan({ + panel: { + sourceLayerId: 'character-layer', + promptText: ' 循环奔跑动作 ', + resolution: '720p', + ratio: 'same', + frameCount: 48, + durationSeconds: 6, + status: 'idle', + }, + sourceLayer, + }); + + expect(plan).toEqual({ + promptText: '循环奔跑动作', + input: { + sourceLayerId: 'character-layer', + sourceImageSrc: 'generated/character.png', + sourceWidth: 960, + sourceHeight: 1280, + promptText: '循环奔跑动作', + resolution: '720p', + ratio: 'same', + frameCount: 48, + durationSeconds: 6, + priceMudPoints: 120, + model: 'seedance2.0', + }, + }); + }); }); diff --git a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts index e065c977..c92bc16f 100644 --- a/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts @@ -1,21 +1,29 @@ import type { + EditorCharacterAnimationGenerationInput, + EditorIconSpritesheetGenerationInput, EditorImageGenerationInput, } from '../../services/image-editor/editorProjectClient'; import type { CanvasGenerationInputs, CanvasLayer, + CharacterAnimationPanelState, GenerateDialogState, } from './ImageCanvasEditorTypes'; import { + CHARACTER_ANIMATION_MODEL, DEFAULT_IMAGE_MODEL, + DEFAULT_ICON_DESCRIPTIONS, DEFAULT_SPEC_FORM_VALUES, SPEC_GENERATION_SIZE, SPEC_TYPE_LABEL, + buildIconGenerationInputs, buildCharacterGenerationInputs, buildEditGenerationInputs, buildImageGenerationInputs, buildSpecGenerationInputs, buildSpecPrompt, + calculateCharacterAnimationPrice, + resolveCharacterAnimationSourceImageSrc, } from './ImageCanvasGenerationModel'; type ImageGenerationSubmissionOptions = { @@ -43,6 +51,24 @@ export type ImageGenerationSubmissionPlan = rememberImageModel?: string; }; +export type IconSpritesheetGenerationSubmissionPlan = + | { + ok: false; + errorMessage: string; + } + | { + ok: true; + iconDescriptions: string[]; + input: EditorIconSpritesheetGenerationInput; + generationInputs: CanvasGenerationInputs; + rememberImageModel: string; + }; + +export type CharacterAnimationSubmissionPlan = { + promptText: string; + input: EditorCharacterAnimationGenerationInput; +}; + export function buildImageGenerationSubmissionPlan({ dialog, layers, @@ -141,3 +167,72 @@ export function buildImageGenerationSubmissionPlan({ }, }; } + +export function buildIconSpritesheetGenerationSubmissionPlan( + dialog: GenerateDialogState, +): IconSpritesheetGenerationSubmissionPlan { + const iconDescriptions = (dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS) + .map((description) => description.trim()) + .filter(Boolean); + + if (!dialog.iconSpecReference) { + return { + ok: false, + errorMessage: '请选择图标素材规范', + }; + } + + if (!iconDescriptions.length) { + return { + ok: false, + errorMessage: '请填写素材描述', + }; + } + + const rememberImageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL; + return { + ok: true, + iconDescriptions, + input: { + referenceImageSrc: dialog.iconSpecReference.src, + iconDescriptions, + model: rememberImageModel, + aspectRatio: dialog.aspectRatio ?? '1:1', + imageSize: dialog.imageSize ?? '1K', + }, + generationInputs: buildIconGenerationInputs( + iconDescriptions, + dialog.iconSpecReference, + ), + rememberImageModel, + }; +} + +export function buildCharacterAnimationSubmissionPlan({ + panel, + sourceLayer, +}: { + panel: CharacterAnimationPanelState; + sourceLayer: CanvasLayer; +}): CharacterAnimationSubmissionPlan { + const promptText = panel.promptText.trim(); + return { + promptText, + input: { + sourceLayerId: sourceLayer.id, + sourceImageSrc: resolveCharacterAnimationSourceImageSrc(sourceLayer), + sourceWidth: sourceLayer.originalWidth, + sourceHeight: sourceLayer.originalHeight, + promptText, + resolution: panel.resolution, + ratio: panel.ratio, + frameCount: panel.frameCount, + durationSeconds: panel.durationSeconds, + priceMudPoints: calculateCharacterAnimationPrice( + panel.resolution, + panel.durationSeconds, + ), + model: CHARACTER_ANIMATION_MODEL, + }, + }; +} diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index 42f35a98..724ea314 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -24,7 +24,6 @@ import { } from './ImageCanvasGenerationLayerModel'; import { CHARACTER_ANIMATION_DURATION_OPTIONS, - CHARACTER_ANIMATION_MODEL, CHARACTER_FRAME_DISPLAY_SIZE, CHARACTER_FRAME_ORIGINAL_SIZE, DEFAULT_ICON_DESCRIPTIONS, @@ -37,16 +36,18 @@ import { SPEC_FRAME_DISPLAY_SIZE, SPEC_FRAME_ORIGINAL_SIZE, buildEditGenerationInputs, - buildIconGenerationInputs, buildQuickEditModelOptions, buildQuickEditSizeOptions, calculateCharacterAnimationPrice, createCanvasLayerReference, isCanvasGenerationDialog, - resolveCharacterAnimationSourceImageSrc, resolveImageGenerationErrorMessage, } from './ImageCanvasGenerationModel'; -import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel'; +import { + buildCharacterAnimationSubmissionPlan, + buildIconSpritesheetGenerationSubmissionPlan, + buildImageGenerationSubmissionPlan, +} from './ImageCanvasGenerationSubmissionModel'; import { formatImageSizeValue } from './ImageCanvasEditorModel'; import type { CanvasGenerationDialogState, @@ -657,29 +658,15 @@ export function useImageCanvasGenerationWorkflow({ ) => { updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog); }; - const iconDescriptions = ( - dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS - ) - .map((description) => description.trim()) - .filter(Boolean); - if (!dialog.iconSpecReference) { + const submissionPlan = + buildIconSpritesheetGenerationSubmissionPlan(dialog); + if (!submissionPlan.ok) { if (canvasDialog) { setSubmittingIconDialog({ ...canvasDialog, status: 'failed', composerOpen: true, - errorMessage: '请选择图标素材规范', - }); - } - return; - } - if (!iconDescriptions.length) { - if (canvasDialog) { - setSubmittingIconDialog({ - ...canvasDialog, - status: 'failed', - composerOpen: true, - errorMessage: '请填写素材描述', + errorMessage: submissionPlan.errorMessage, }); } return; @@ -691,32 +678,28 @@ export function useImageCanvasGenerationWorkflow({ setSubmittingIconDialog({ ...canvasDialog, - iconDescriptions, + iconDescriptions: submissionPlan.iconDescriptions, status: 'generating', composerOpen: false, errorMessage: undefined, }); try { - const generated = await generateEditorIconSpritesheet({ - referenceImageSrc: dialog.iconSpecReference.src, - iconDescriptions, - model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL, - aspectRatio: dialog.aspectRatio ?? '1:1', - imageSize: dialog.imageSize ?? '1K', - }); - setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL); + const generated = await generateEditorIconSpritesheet( + submissionPlan.input, + ); + setLastImageModel(submissionPlan.rememberImageModel); addIconSpritesheetResultLayers( generated, generated.iconImageSrcs, - buildIconGenerationInputs(iconDescriptions, dialog.iconSpecReference), + submissionPlan.generationInputs, getGeneratingDialogPlaceholder(dialog), canvasDialog.id, ); } catch (error) { setSubmittingIconDialog({ ...canvasDialog, - iconDescriptions, + iconDescriptions: submissionPlan.iconDescriptions, status: 'failed', composerOpen: true, errorMessage: resolveImageGenerationErrorMessage(error), @@ -913,10 +896,13 @@ export function useImageCanvasGenerationWorkflow({ if (!characterAnimationPanel || !characterAnimationSourceLayer) { return; } - const promptText = characterAnimationPanel.promptText.trim(); + const submissionPlan = buildCharacterAnimationSubmissionPlan({ + panel: characterAnimationPanel, + sourceLayer: characterAnimationSourceLayer, + }); const nextPanel = { ...characterAnimationPanel, - promptText, + promptText: submissionPlan.promptText, status: 'generating' as const, errorMessage: undefined, result: undefined, @@ -924,24 +910,9 @@ export function useImageCanvasGenerationWorkflow({ setCharacterAnimationPanel(nextPanel); try { - const result = await generateEditorCharacterAnimation({ - sourceLayerId: characterAnimationSourceLayer.id, - sourceImageSrc: resolveCharacterAnimationSourceImageSrc( - characterAnimationSourceLayer, - ), - sourceWidth: characterAnimationSourceLayer.originalWidth, - sourceHeight: characterAnimationSourceLayer.originalHeight, - promptText, - resolution: nextPanel.resolution, - ratio: nextPanel.ratio, - frameCount: nextPanel.frameCount, - durationSeconds: nextPanel.durationSeconds, - priceMudPoints: calculateCharacterAnimationPrice( - nextPanel.resolution, - nextPanel.durationSeconds, - ), - model: CHARACTER_ANIMATION_MODEL, - }); + const result = await generateEditorCharacterAnimation( + submissionPlan.input, + ); setCharacterAnimationPanel((currentPanel) => currentPanel ? {