拆分编辑器上传模型

抽出上传文件夹解析和画布落点计算

补充上传模型单测

更新 TRACKING.md 记录第三十六阶段验证
This commit is contained in:
2026-06-17 18:06:53 +08:00
parent 4abf00d007
commit b5707ac2b9
4 changed files with 335 additions and 71 deletions

View File

@@ -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> = {},
): EditorAssetFolder {
return {
id: 'project',
label: '项目素材',
collapsed: false,
systemDefault: true,
persisted: true,
...overrides,
};
}
function createLayer(overrides: Partial<CanvasLayer> = {}): 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,
});
});
});

View File

@@ -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,
};
}

View File

@@ -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,
),
);