From b5707ac2b9f513ba4c916a100b1cf022f12192be Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 17 Jun 2026 18:06:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E7=BC=96=E8=BE=91=E5=99=A8?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 抽出上传文件夹解析和画布落点计算 补充上传模型单测 更新 TRACKING.md 记录第三十六阶段验证 --- TRACKING.md | 1 + .../ImageCanvasUploadModel.test.ts | 151 ++++++++++++++++++ .../image-editor/ImageCanvasUploadModel.ts | 148 +++++++++++++++++ .../useImageCanvasUploadWorkflow.ts | 106 ++++-------- 4 files changed, 335 insertions(+), 71 deletions(-) create mode 100644 src/components/image-editor/ImageCanvasUploadModel.test.ts create mode 100644 src/components/image-editor/ImageCanvasUploadModel.ts diff --git a/TRACKING.md b/TRACKING.md index c1846f84..165fe9d8 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -152,3 +152,4 @@ - 2026-06-17 前端拆分第三十三阶段:继续收口 `ImageCanvasAssetLibraryPanelView`,新增 `ImageCanvasAssetFolderSectionView` 和 `ImageCanvasAssetRowView`,把素材库文件夹头 / 文件夹 drop 区域、素材卡片 / 上传进度 / 重命名 / 选择模式从素材库父面板中拆成两个完整 surface;素材库父面板降至 279 行,只保留素材列表容器、新建文件夹表单、批量操作栏和框选遮罩。新增素材行单测覆盖普通点击加入画布、选择模式改为选中、重命名 Enter 提交和上传中禁用 / 进度显示。统一验证命令:`npm run test -- 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/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 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;默认素材栏显示 `项目素材` 文件夹、上传入口和底部 `AI画布工具栏`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。 - 2026-06-17 前端拆分第三十四阶段:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasWorldView`,把画布世界表面、吸附参考线、可见图层排序 / 悬浮 / 选中 / 锁定 / 生成态、元数据角标、框选矩形、生成占位框和浮动生成状态从 StageView 内联 JSX 中抽出;StageView 降至 324 行,继续保留 viewport 宿主、drop overlay、左下 dock、底部工具栏、右键菜单和选中图片工具栏装配。新增 world view 单测覆盖隐藏图层过滤、悬浮尺寸、生成态、元数据按钮、吸附线、框选矩形和生成占位框事件。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx`、`npm run test -- 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`,`AI画布工具栏` 保持可见;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 - 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。 diff --git a/src/components/image-editor/ImageCanvasUploadModel.test.ts b/src/components/image-editor/ImageCanvasUploadModel.test.ts new file mode 100644 index 00000000..2bdef324 --- /dev/null +++ b/src/components/image-editor/ImageCanvasUploadModel.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from 'vitest'; + +import type { + CanvasLayer, + EditorAssetFolder, +} from './ImageCanvasEditorTypes'; +import { + createUploadCanvasLayer, + createUploadingAssetPlaceholder, + resizeUploadCanvasLayerToImage, + resolveUploadFolderId, +} from './ImageCanvasUploadModel'; + +function createFolder( + overrides: Partial = {}, +): EditorAssetFolder { + return { + id: 'project', + label: '项目素材', + collapsed: false, + systemDefault: true, + persisted: true, + ...overrides, + }; +} + +function createLayer(overrides: Partial = {}): CanvasLayer { + return { + id: 'layer-upload-1', + resourceId: 'local-resource-upload-1', + title: '上传图片', + src: 'data:image/png;base64,upload', + x: -160, + y: -107.5, + width: 420, + height: 315, + originalWidth: 420, + originalHeight: 315, + zIndex: 11, + sourceType: 'uploaded', + sourceAssetId: 'upload-1', + ...overrides, + }; +} + +describe('ImageCanvasUploadModel', () => { + it('resolves upload folder ids with project fallback', () => { + const folders = [ + createFolder(), + createFolder({ id: 'characters', label: '角色素材' }), + ]; + + expect( + resolveUploadFolderId({ + assetFolders: folders, + requestedFolderId: 'characters', + activeUploadFolderId: 'project', + }), + ).toBe('characters'); + expect( + resolveUploadFolderId({ + assetFolders: folders, + requestedFolderId: 'missing', + activeUploadFolderId: 'characters', + }), + ).toBe('project'); + }); + + it('creates uploading asset placeholders', () => { + expect( + createUploadingAssetPlaceholder({ + uploadIndex: 7, + fileName: 'hero.png', + folderId: 'characters', + }), + ).toEqual({ + id: 'upload-7', + label: 'hero.png', + src: '', + width: 420, + height: 315, + folderId: 'characters', + sourceKind: 'uploaded', + sourceType: 'uploaded', + persisted: false, + uploadStatus: 'uploading', + uploadProgress: 8, + uploadMessage: '准备上传', + }); + }); + + it('creates upload canvas layers centered on the target canvas point', () => { + const layer = createUploadCanvasLayer({ + uploadIndex: 1, + fileName: '画布素材.png', + imageSrc: 'data:image/png;base64,canvas', + canvasPoint: { x: 110, y: 120 }, + canvasSize: { width: 900, height: 640 }, + viewport: { x: 10, y: 20, scale: 2 }, + }); + + expect(layer).toMatchObject({ + id: 'layer-upload-1', + resourceId: 'local-resource-upload-1', + title: '画布素材.png', + x: -160, + y: -107.5, + width: 420, + height: 315, + originalWidth: 420, + originalHeight: 315, + zIndex: 11, + sourceAssetId: 'upload-1', + }); + }); + + it('uses viewport center fallback for invalid upload points', () => { + const layer = createUploadCanvasLayer({ + uploadIndex: 2, + fileName: '', + imageSrc: 'data:image/png;base64,canvas', + canvasPoint: { x: Number.NaN, y: Number.POSITIVE_INFINITY }, + canvasSize: { width: 900, height: 640 }, + viewport: { x: 10, y: 20, scale: 2 }, + }); + + expect(layer.title).toBe('上传图片'); + expect(layer.x).toBe(10); + expect(layer.y).toBe(-7.5); + }); + + it('resizes upload canvas layers around the same canvas point', () => { + const resizedLayer = resizeUploadCanvasLayerToImage({ + layer: createLayer(), + originalWidth: 1536, + originalHeight: 1024, + canvasPoint: { x: 110, y: 120 }, + canvasSize: { width: 900, height: 640 }, + viewport: { x: 10, y: 20, scale: 2 }, + }); + + expect(resizedLayer).toMatchObject({ + width: 1536, + height: 1024, + originalWidth: 1536, + originalHeight: 1024, + x: -718, + y: -462, + }); + }); +}); diff --git a/src/components/image-editor/ImageCanvasUploadModel.ts b/src/components/image-editor/ImageCanvasUploadModel.ts new file mode 100644 index 00000000..84033df9 --- /dev/null +++ b/src/components/image-editor/ImageCanvasUploadModel.ts @@ -0,0 +1,148 @@ +import { resolveLayerResolutionSize } from './ImageCanvasEditorModel'; +import type { + CanvasLayer, + CanvasViewport, + EditorAsset, + EditorAssetFolder, +} from './ImageCanvasEditorTypes'; + +type CanvasSize = { width: number; height: number }; +type CanvasPoint = { x: number; y: number }; + +export const UPLOAD_LAYER_FALLBACK_SIZE = { + width: 420, + height: 315, +} as const; + +export function resolveUploadFolderId({ + assetFolders, + requestedFolderId, + activeUploadFolderId, +}: { + assetFolders: EditorAssetFolder[]; + requestedFolderId?: string; + activeUploadFolderId: string; +}) { + const targetFolderId = requestedFolderId ?? activeUploadFolderId; + return assetFolders.some((folder) => folder.id === targetFolderId) + ? targetFolderId + : 'project'; +} + +export function createUploadingAssetPlaceholder({ + uploadIndex, + fileName, + folderId, +}: { + uploadIndex: number; + fileName?: string; + folderId: string; +}): EditorAsset { + return { + id: `upload-${uploadIndex}`, + label: fileName || '上传图片', + src: '', + width: UPLOAD_LAYER_FALLBACK_SIZE.width, + height: UPLOAD_LAYER_FALLBACK_SIZE.height, + folderId, + sourceKind: 'uploaded', + sourceType: 'uploaded', + persisted: false, + uploadStatus: 'uploading', + uploadProgress: 8, + uploadMessage: '准备上传', + }; +} + +export function normalizeUploadScreenPoint({ + canvasPoint, + canvasSize, +}: { + canvasPoint?: CanvasPoint; + canvasSize: CanvasSize; +}): CanvasPoint { + const fallbackScreenPoint = { + x: canvasSize.width > 0 ? canvasSize.width / 2 : 640, + y: canvasSize.height > 0 ? canvasSize.height / 2 : 360, + }; + const screenPoint = canvasPoint ?? fallbackScreenPoint; + return { + x: Number.isFinite(screenPoint.x) ? screenPoint.x : fallbackScreenPoint.x, + y: Number.isFinite(screenPoint.y) ? screenPoint.y : fallbackScreenPoint.y, + }; +} + +export function createUploadCanvasLayer({ + uploadIndex, + fileName, + imageSrc, + canvasPoint, + canvasSize, + viewport, +}: { + uploadIndex: number; + fileName?: string; + imageSrc: string; + canvasPoint?: CanvasPoint; + canvasSize: CanvasSize; + viewport: CanvasViewport; +}): CanvasLayer { + const screenPoint = normalizeUploadScreenPoint({ canvasPoint, canvasSize }); + const safeScale = viewport.scale > 0 ? viewport.scale : 1; + const worldCenterX = (screenPoint.x - viewport.x) / safeScale; + const worldCenterY = (screenPoint.y - viewport.y) / safeScale; + return { + id: `layer-upload-${uploadIndex}`, + resourceId: `local-resource-upload-${uploadIndex}`, + title: fileName || '上传图片', + src: imageSrc, + x: worldCenterX - UPLOAD_LAYER_FALLBACK_SIZE.width / 2, + y: worldCenterY - UPLOAD_LAYER_FALLBACK_SIZE.height / 2, + width: UPLOAD_LAYER_FALLBACK_SIZE.width, + height: UPLOAD_LAYER_FALLBACK_SIZE.height, + originalWidth: UPLOAD_LAYER_FALLBACK_SIZE.width, + originalHeight: UPLOAD_LAYER_FALLBACK_SIZE.height, + zIndex: uploadIndex + 10, + sourceType: 'uploaded', + sourceAssetId: `upload-${uploadIndex}`, + }; +} + +export function resizeUploadCanvasLayerToImage({ + layer, + originalWidth, + originalHeight, + canvasPoint, + canvasSize, + viewport, +}: { + layer: CanvasLayer; + originalWidth: number; + originalHeight: number; + canvasPoint?: CanvasPoint; + canvasSize: CanvasSize; + viewport: CanvasViewport; +}): CanvasLayer { + const safeOriginalWidth = + originalWidth || UPLOAD_LAYER_FALLBACK_SIZE.width; + const safeOriginalHeight = + originalHeight || UPLOAD_LAYER_FALLBACK_SIZE.height; + const { width, height } = resolveLayerResolutionSize( + safeOriginalWidth, + safeOriginalHeight, + UPLOAD_LAYER_FALLBACK_SIZE, + ); + const screenPoint = normalizeUploadScreenPoint({ canvasPoint, canvasSize }); + const safeScale = viewport.scale > 0 ? viewport.scale : 1; + const worldCenterX = (screenPoint.x - viewport.x) / safeScale; + const worldCenterY = (screenPoint.y - viewport.y) / safeScale; + return { + ...layer, + width, + height, + originalWidth: safeOriginalWidth, + originalHeight: safeOriginalHeight, + x: worldCenterX - width / 2, + y: worldCenterY - height / 2, + }; +} diff --git a/src/components/image-editor/useImageCanvasUploadWorkflow.ts b/src/components/image-editor/useImageCanvasUploadWorkflow.ts index 882752e7..220fa2fa 100644 --- a/src/components/image-editor/useImageCanvasUploadWorkflow.ts +++ b/src/components/image-editor/useImageCanvasUploadWorkflow.ts @@ -9,7 +9,6 @@ import { import { ApiClientError } from '../../services/apiClient'; import { createEditorAsset } from '../../services/image-editor/editorProjectClient'; -import { resolveLayerResolutionSize } from './ImageCanvasEditorModel'; import type { CanvasLayer, CanvasTool, @@ -20,6 +19,12 @@ import type { UploadTarget, } from './ImageCanvasEditorTypes'; import { isImageFile, readImageFileAsDataUrl } from './ImageCanvasFileModel'; +import { + createUploadCanvasLayer, + createUploadingAssetPlaceholder, + resizeUploadCanvasLayerToImage, + resolveUploadFolderId, +} from './ImageCanvasUploadModel'; type UploadFilesOptions = { folderId?: string; @@ -210,28 +215,17 @@ export function useImageCanvasUploadWorkflow({ return; } - const fallbackWidth = 420; - const fallbackHeight = 315; - const uploadFolderId = assetFolders.some( - (folder) => folder.id === (options.folderId ?? activeUploadFolderId), - ) - ? (options.folderId ?? activeUploadFolderId) - : 'project'; + const uploadFolderId = resolveUploadFolderId({ + assetFolders, + requestedFolderId: options.folderId, + activeUploadFolderId, + }); const uploadIndex = options.uploadIndex; - const uploadedAsset: EditorAsset = { - id: `upload-${uploadIndex}`, - label: file.name || '上传图片', - src: '', - width: fallbackWidth, - height: fallbackHeight, + const uploadedAsset = createUploadingAssetPlaceholder({ + uploadIndex, + fileName: file.name, folderId: uploadFolderId, - sourceKind: 'uploaded', - sourceType: 'uploaded', - persisted: false, - uploadStatus: 'uploading', - uploadProgress: 8, - uploadMessage: '准备上传', - }; + }); setAssets((currentAssets) => [...currentAssets, uploadedAsset]); setAssetFolders((currentFolders) => currentFolders.map((folder) => @@ -275,40 +269,14 @@ export function useImageCanvasUploadWorkflow({ return; } - const screenPoint = options.canvasPoint ?? { - x: canvasSize.width / 2, - y: canvasSize.height / 2, - }; - const fallbackScreenPoint = { - x: canvasSize.width > 0 ? canvasSize.width / 2 : 640, - y: canvasSize.height > 0 ? canvasSize.height / 2 : 360, - }; - const normalizedScreenPoint = { - x: Number.isFinite(screenPoint.x) - ? screenPoint.x - : fallbackScreenPoint.x, - y: Number.isFinite(screenPoint.y) - ? screenPoint.y - : fallbackScreenPoint.y, - }; - const safeScale = viewport.scale > 0 ? viewport.scale : 1; - const worldCenterX = (normalizedScreenPoint.x - viewport.x) / safeScale; - const worldCenterY = (normalizedScreenPoint.y - viewport.y) / safeScale; - const nextLayer: CanvasLayer = { - id: `layer-upload-${uploadIndex}`, - resourceId: `local-resource-upload-${uploadIndex}`, - title: file.name || '上传图片', - src: imageSrc, - x: worldCenterX - fallbackWidth / 2, - y: worldCenterY - fallbackHeight / 2, - width: fallbackWidth, - height: fallbackHeight, - originalWidth: fallbackWidth, - originalHeight: fallbackHeight, - zIndex: uploadIndex + 10, - sourceType: 'uploaded', - sourceAssetId: `upload-${uploadIndex}`, - }; + const nextLayer = createUploadCanvasLayer({ + uploadIndex, + fileName: file.name, + imageSrc, + canvasPoint: options.canvasPoint, + canvasSize, + viewport, + }); if (options.addToCanvas) { appendCanvasLayersWithResources([nextLayer]); @@ -330,8 +298,8 @@ export function useImageCanvasUploadWorkflow({ folderId: uploadFolderId, label: uploadedAsset.label, imageSrc, - width: fallbackWidth, - height: fallbackHeight, + width: uploadedAsset.width, + height: uploadedAsset.height, sourceType: 'uploaded', }) .then((asset) => { @@ -394,26 +362,22 @@ export function useImageCanvasUploadWorkflow({ if (imageSrc) { const uploadedImage = new Image(); uploadedImage.onload = () => { - const originalWidth = uploadedImage.naturalWidth || fallbackWidth; - const originalHeight = uploadedImage.naturalHeight || fallbackHeight; - const { width, height } = resolveLayerResolutionSize( - originalWidth, - originalHeight, - { width: fallbackWidth, height: fallbackHeight }, - ); + const originalWidth = + uploadedImage.naturalWidth || uploadedAsset.width; + const originalHeight = + uploadedImage.naturalHeight || uploadedAsset.height; if (options.addToCanvas) { setLayers((currentLayers) => currentLayers.map((layer) => layer.id === nextLayer.id - ? { - ...layer, - width, - height, + ? resizeUploadCanvasLayerToImage({ + layer, originalWidth, originalHeight, - x: worldCenterX - width / 2, - y: worldCenterY - height / 2, - } + canvasPoint: options.canvasPoint, + canvasSize, + viewport, + }) : layer, ), );