61fe4c48af
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 16s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Failing after 18s
Project CI / AI game creator shell Rust smoke (pull_request) Failing after 16s
Project CI / Backend tests (pull_request) Failing after 16s
Project CI / AI game creator shell Rust crates (pull_request) Failing after 16s
Project CI / Native shell tests (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 7s
Project CI / Repository checks (pull_request) Failing after 12s
Project CI / AI game creator shell web tests (pull_request) Failing after 12s
- 后端资源投影按 asset_kind 分叉,新增 project_editor_client_media_src 与 EDITOR_MODEL3D_ASSET_KIND:model3d 只投影预览图,模型本体只留 objectKey,预览缺失才回落历史口径 - tripo3d 存储新增按字节魔数识别模型 content_type 与扩展名映射,不采信 provider 声明与下载响应头 - editor_project_storage 把 model3d 加入 EDITOR_CANVAS_ASSET_KINDS 白名单 - 前端 CanvasAssetKind 与 image-canvas-core 类型新增 model3d:画布卡片渲染预览图或 3D 占位,类型角标新增「3D模型」,覆盖资格只在它与空类别之间成立 - 新增 model3d-preview 子目录(Model3dViewerModal / Model3dViewerModel),Tailwind 内联、不新增 CSS,失败原因分超限 / 格式不可识别 / 无 WebGL2 三类展示 - 选中工具条新增只读 preview-model3d 能力位,仅 objectKey 为空时置灰;打开预览同时选中图层并暂停画布快捷键与舞台交互 - 查看器包源对象 format 改为可选,新增「声明 Content-Type → 字节魔数 → 地址扩展名」三级格式判定并导出支持格式清单 - 导出工作流新增 3d_models/ 目录,模型下载按真实响应头 MIME 定扩展名、对象键扩展名兜底 - 新增 3D 生成入口:底部工具栏浮动子选项与文生 / 图生 3D 面板,落 projectResource + canvasCompletion,复用占位框与任务侧栏 - 3D 定价只读实时定价查询,段缺失即入口不可提交;幂等键由前端铸造,用户重试必须换键 - 新增 ADR 0003 / 0004 与实施计划、里程碑文档;更新 Tripo 技术方案、后端架构、CONTEXT 术语、decision-log 与 pitfalls - .gitignore 忽略仓库根目录手工下载的 3D 样例模型
715 lines
18 KiB
TypeScript
715 lines
18 KiB
TypeScript
import { resolvePreferredAssetUploadFolder } from './ImageCanvasAssetLibraryModel';
|
|
import { resolveLayerResolutionSize } from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasGenerationInputs,
|
|
CanvasLayer,
|
|
CanvasMediaType,
|
|
CanvasViewport,
|
|
CharacterReferenceImage,
|
|
EditorAsset,
|
|
EditorAssetFolder,
|
|
GenerateDialogState,
|
|
QuickEditPanelState,
|
|
} from './ImageCanvasEditorTypes';
|
|
import {
|
|
resolveSpecGenerationReferences,
|
|
setSpecGenerationReferences,
|
|
} from './ImageCanvasGenerationDialogModel';
|
|
import {
|
|
appendLimitedImageReferences,
|
|
QUICK_EDIT_REFERENCE_LIMIT,
|
|
resolveDialogExtraImageReferenceLimit,
|
|
resolveExtraImageReferenceLimit,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { resolveUploadedAssetKind } from './ImageCanvasUploadUtils';
|
|
|
|
type CanvasSize = { width: number; height: number };
|
|
type CanvasPoint = { x: number; y: number };
|
|
type UploadImageDimensions = { width: number; height: number } | null;
|
|
type PersistedUploadAsset = {
|
|
assetId: string;
|
|
folderId: string;
|
|
label: string;
|
|
imageSrc: string;
|
|
mediaType?: CanvasMediaType;
|
|
width: number;
|
|
height: number;
|
|
objectKey?: string | null;
|
|
assetObjectId?: string | null;
|
|
generationInputs?: CanvasGenerationInputs | null;
|
|
};
|
|
type GenerationReferenceUploadTarget =
|
|
| 'character-reference'
|
|
| 'character-spec'
|
|
| 'generation-reference'
|
|
| 'quick-edit-reference'
|
|
| 'video-reference-image'
|
|
| 'video-reference-video'
|
|
| 'video-reference-audio'
|
|
| 'icon-spec'
|
|
| 'spec-reference'
|
|
| 'ui-design-icon-spec'
|
|
| 'publication-reference';
|
|
|
|
let uploadedGenerationReferenceSequence = 0;
|
|
|
|
const VIDEO_REFERENCE_LIMITS = {
|
|
image: 9,
|
|
video: 3,
|
|
audio: 3,
|
|
} as const;
|
|
type VideoReferenceMediaType = keyof typeof VIDEO_REFERENCE_LIMITS;
|
|
|
|
export const UPLOAD_LAYER_FALLBACK_SIZE = {
|
|
width: 420,
|
|
height: 315,
|
|
} as const;
|
|
export const UPLOAD_VIDEO_FALLBACK_SIZE = {
|
|
width: 560,
|
|
height: 315,
|
|
} as const;
|
|
export const UPLOAD_AUDIO_FALLBACK_SIZE = {
|
|
width: 420,
|
|
height: 120,
|
|
} as const;
|
|
|
|
export function resolveUploadFallbackSize(mediaType: CanvasMediaType) {
|
|
if (mediaType === 'video') {
|
|
return UPLOAD_VIDEO_FALLBACK_SIZE;
|
|
}
|
|
if (mediaType === 'audio') {
|
|
return UPLOAD_AUDIO_FALLBACK_SIZE;
|
|
}
|
|
return UPLOAD_LAYER_FALLBACK_SIZE;
|
|
}
|
|
|
|
export function resolveUploadFolderId({
|
|
assetFolders,
|
|
requestedFolderId,
|
|
activeUploadFolderId,
|
|
preferUploadFolder = false,
|
|
}: {
|
|
assetFolders: EditorAssetFolder[];
|
|
requestedFolderId?: string;
|
|
activeUploadFolderId: string;
|
|
preferUploadFolder?: boolean;
|
|
}) {
|
|
const targetFolderId =
|
|
requestedFolderId ??
|
|
(preferUploadFolder
|
|
? resolvePreferredAssetUploadFolder(assetFolders)?.id
|
|
: activeUploadFolderId) ??
|
|
activeUploadFolderId;
|
|
return assetFolders.some((folder) => folder.id === targetFolderId)
|
|
? targetFolderId
|
|
: 'project';
|
|
}
|
|
|
|
export function createUploadedGenerationReference({
|
|
idPrefix,
|
|
index,
|
|
fileName,
|
|
fallbackLabel,
|
|
imageSrc,
|
|
mediaType = 'image',
|
|
mimeType,
|
|
sizeBytes,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
resourceId,
|
|
sourceAssetId,
|
|
}: {
|
|
idPrefix: string;
|
|
index?: number;
|
|
fileName?: string;
|
|
fallbackLabel: string;
|
|
imageSrc: string;
|
|
mediaType?: CanvasMediaType;
|
|
mimeType?: string;
|
|
sizeBytes?: number;
|
|
durationSeconds?: number;
|
|
objectKey?: string | null;
|
|
assetObjectId?: string | null;
|
|
resourceId?: string | null;
|
|
sourceAssetId?: string | null;
|
|
}): CharacterReferenceImage {
|
|
uploadedGenerationReferenceSequence += 1;
|
|
return {
|
|
id:
|
|
typeof index === 'number'
|
|
? `${idPrefix}-${Date.now()}-${index}-${uploadedGenerationReferenceSequence}`
|
|
: `${idPrefix}-${Date.now()}-${uploadedGenerationReferenceSequence}`,
|
|
label: fileName || fallbackLabel,
|
|
src: imageSrc,
|
|
mediaType,
|
|
mimeType,
|
|
sizeBytes,
|
|
durationSeconds,
|
|
objectKey,
|
|
assetObjectId,
|
|
resourceId,
|
|
sourceAssetId,
|
|
};
|
|
}
|
|
|
|
export function setFailedGenerationIdle(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
export function applyGenerationReferenceUpload({
|
|
dialog,
|
|
target,
|
|
references,
|
|
}: {
|
|
dialog: GenerateDialogState | null;
|
|
target: GenerationReferenceUploadTarget;
|
|
references: CharacterReferenceImage[];
|
|
}): GenerateDialogState | null {
|
|
const firstReference = references[0];
|
|
if (!firstReference) {
|
|
return dialog;
|
|
}
|
|
if (target === 'spec-reference') {
|
|
if (dialog?.mode !== 'spec') {
|
|
return dialog;
|
|
}
|
|
return setSpecGenerationReferences(
|
|
setFailedGenerationIdle(dialog),
|
|
dialog.specType === 'icon'
|
|
? [firstReference]
|
|
: appendLimitedImageReferences(
|
|
resolveSpecGenerationReferences(dialog),
|
|
references,
|
|
resolveDialogExtraImageReferenceLimit(dialog),
|
|
),
|
|
);
|
|
}
|
|
if (target === 'character-spec') {
|
|
return dialog?.mode === 'character'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
characterSpecReference: firstReference,
|
|
characterReferences: dialog.characterReferences,
|
|
}
|
|
: dialog;
|
|
}
|
|
if (target === 'icon-spec') {
|
|
return dialog?.mode === 'icon'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
iconSpecReference: firstReference,
|
|
generationReferences: appendLimitedImageReferences(
|
|
[],
|
|
dialog.generationReferences ?? [],
|
|
resolveDialogExtraImageReferenceLimit(dialog),
|
|
),
|
|
}
|
|
: dialog;
|
|
}
|
|
if (target === 'ui-design-icon-spec') {
|
|
return dialog?.mode === 'ui-design'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
uiDesignSpecReference: firstReference,
|
|
generationReferences: dialog.generationReferences,
|
|
}
|
|
: dialog;
|
|
}
|
|
if (target === 'publication-reference') {
|
|
return dialog?.mode === 'publication'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
publicationReferences: appendLimitedImageReferences(
|
|
dialog.publicationReferences,
|
|
references,
|
|
resolveDialogExtraImageReferenceLimit(dialog),
|
|
),
|
|
composerOpen: true,
|
|
}
|
|
: dialog;
|
|
}
|
|
if (target === 'generation-reference') {
|
|
if (dialog?.mode === 'spec') {
|
|
return setSpecGenerationReferences(
|
|
setFailedGenerationIdle(dialog),
|
|
dialog.specType === 'icon'
|
|
? [firstReference]
|
|
: appendLimitedImageReferences(
|
|
resolveSpecGenerationReferences(dialog),
|
|
references,
|
|
resolveDialogExtraImageReferenceLimit(dialog),
|
|
),
|
|
);
|
|
}
|
|
return dialog?.mode === 'generate' ||
|
|
dialog?.mode === 'scene' ||
|
|
dialog?.mode === 'video' ||
|
|
dialog?.mode === 'icon' ||
|
|
dialog?.mode === 'ui-design' ||
|
|
dialog?.mode === 'model3d-image-to-model'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
generationReferences:
|
|
dialog.mode === 'video'
|
|
? appendLimitedVideoReferences(
|
|
dialog.generationReferences ?? [],
|
|
references,
|
|
'image',
|
|
)
|
|
: appendLimitedImageReferences(
|
|
dialog.generationReferences,
|
|
references,
|
|
resolveDialogExtraImageReferenceLimit(dialog),
|
|
),
|
|
}
|
|
: dialog;
|
|
}
|
|
if (
|
|
target === 'video-reference-image' ||
|
|
target === 'video-reference-video' ||
|
|
target === 'video-reference-audio'
|
|
) {
|
|
const mediaType: VideoReferenceMediaType =
|
|
target === 'video-reference-video'
|
|
? 'video'
|
|
: target === 'video-reference-audio'
|
|
? 'audio'
|
|
: 'image';
|
|
return dialog?.mode === 'video'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
generationReferences: appendLimitedVideoReferences(
|
|
dialog.generationReferences ?? [],
|
|
references,
|
|
mediaType,
|
|
),
|
|
composerOpen: true,
|
|
}
|
|
: dialog;
|
|
}
|
|
return dialog?.mode === 'character'
|
|
? {
|
|
...setFailedGenerationIdle(dialog),
|
|
characterReferences: appendLimitedImageReferences(
|
|
dialog.characterReferences,
|
|
references,
|
|
resolveDialogExtraImageReferenceLimit(dialog),
|
|
),
|
|
}
|
|
: dialog;
|
|
}
|
|
|
|
export function applyQuickEditReferenceUpload({
|
|
panel,
|
|
references,
|
|
}: {
|
|
panel: QuickEditPanelState | null;
|
|
references: CharacterReferenceImage[];
|
|
}): QuickEditPanelState | null {
|
|
if (!panel) {
|
|
return panel;
|
|
}
|
|
return {
|
|
...panel,
|
|
status: panel.status === 'failed' ? 'idle' : panel.status,
|
|
errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage,
|
|
quickEditReferences: appendLimitedImageReferences(
|
|
panel.quickEditReferences,
|
|
references,
|
|
resolveExtraImageReferenceLimit(
|
|
panel.model,
|
|
QUICK_EDIT_REFERENCE_LIMIT,
|
|
1,
|
|
),
|
|
),
|
|
};
|
|
}
|
|
|
|
function appendLimitedVideoReferences(
|
|
currentReferences: CharacterReferenceImage[],
|
|
nextReferences: CharacterReferenceImage[],
|
|
mediaType: VideoReferenceMediaType,
|
|
) {
|
|
const limit = VIDEO_REFERENCE_LIMITS[mediaType];
|
|
const currentCount = currentReferences.filter(
|
|
(reference) => (reference.mediaType ?? 'image') === mediaType,
|
|
).length;
|
|
const remaining = Math.max(0, limit - currentCount);
|
|
return [
|
|
...currentReferences,
|
|
...nextReferences.slice(0, remaining).map((reference) => ({
|
|
...reference,
|
|
mediaType,
|
|
})),
|
|
];
|
|
}
|
|
|
|
export function createUploadingAssetPlaceholder({
|
|
uploadIndex,
|
|
fileName,
|
|
folderId,
|
|
mediaType = 'image',
|
|
imageDimensions,
|
|
}: {
|
|
uploadIndex: number;
|
|
fileName?: string;
|
|
folderId: string;
|
|
mediaType?: CanvasMediaType;
|
|
imageDimensions?: UploadImageDimensions;
|
|
}): EditorAsset {
|
|
const fallbackSize = resolveUploadFallbackSize(mediaType);
|
|
const size =
|
|
mediaType !== 'audio' && imageDimensions
|
|
? resolveLayerResolutionSize(
|
|
imageDimensions.width,
|
|
imageDimensions.height,
|
|
fallbackSize,
|
|
)
|
|
: fallbackSize;
|
|
return {
|
|
id: `upload-${uploadIndex}`,
|
|
label:
|
|
fileName ||
|
|
(mediaType === 'audio'
|
|
? '上传音频'
|
|
: mediaType === 'video'
|
|
? '上传视频'
|
|
: '上传图片'),
|
|
src: '',
|
|
mediaType,
|
|
width: size.width,
|
|
height: size.height,
|
|
folderId,
|
|
sourceKind: 'uploaded',
|
|
sourceType: 'uploaded',
|
|
persisted: false,
|
|
assetKind: resolveUploadedAssetKind(mediaType),
|
|
uploadStatus: 'uploading',
|
|
uploadProgress: 8,
|
|
uploadMessage: '准备上传',
|
|
};
|
|
}
|
|
|
|
export function expandUploadFolder({
|
|
folders,
|
|
folderId,
|
|
}: {
|
|
folders: EditorAssetFolder[];
|
|
folderId: string;
|
|
}) {
|
|
return folders.map((folder) =>
|
|
folder.id === folderId
|
|
? {
|
|
...folder,
|
|
collapsed: false,
|
|
}
|
|
: folder,
|
|
);
|
|
}
|
|
|
|
export function applyUploadAssetReadSuccess({
|
|
assets,
|
|
uploadAssetId,
|
|
imageSrc,
|
|
}: {
|
|
assets: EditorAsset[];
|
|
uploadAssetId: string;
|
|
imageSrc: string;
|
|
}) {
|
|
return assets.map((asset) =>
|
|
asset.id === uploadAssetId
|
|
? {
|
|
...asset,
|
|
src: imageSrc,
|
|
uploadProgress: 42,
|
|
uploadMessage: '读取素材',
|
|
}
|
|
: asset,
|
|
);
|
|
}
|
|
|
|
export function applyUploadAssetReadFailure({
|
|
assets,
|
|
uploadAssetId,
|
|
}: {
|
|
assets: EditorAsset[];
|
|
uploadAssetId: string;
|
|
}) {
|
|
return assets.map((asset) =>
|
|
asset.id === uploadAssetId
|
|
? {
|
|
...asset,
|
|
uploadStatus: 'failed' as const,
|
|
uploadProgress: 100,
|
|
uploadMessage: '读取失败',
|
|
}
|
|
: asset,
|
|
);
|
|
}
|
|
|
|
export function applyUploadAssetCreatePending({
|
|
assets,
|
|
uploadAssetId,
|
|
}: {
|
|
assets: EditorAsset[];
|
|
uploadAssetId: string;
|
|
}) {
|
|
return assets.map((asset) =>
|
|
asset.id === uploadAssetId
|
|
? {
|
|
...asset,
|
|
uploadProgress: 68,
|
|
uploadMessage: '上传中',
|
|
}
|
|
: asset,
|
|
);
|
|
}
|
|
|
|
export function applyPersistedUploadAsset({
|
|
assets,
|
|
uploadAssetId,
|
|
persistedAsset,
|
|
}: {
|
|
assets: EditorAsset[];
|
|
uploadAssetId: string;
|
|
persistedAsset: PersistedUploadAsset;
|
|
}) {
|
|
return assets.map((asset) =>
|
|
asset.id === uploadAssetId
|
|
? {
|
|
...asset,
|
|
id: persistedAsset.assetId,
|
|
folderId: persistedAsset.folderId,
|
|
label: persistedAsset.label,
|
|
src: persistedAsset.imageSrc,
|
|
mediaType: persistedAsset.mediaType ?? asset.mediaType,
|
|
width: persistedAsset.width,
|
|
height: persistedAsset.height,
|
|
objectKey: persistedAsset.objectKey ?? undefined,
|
|
assetObjectId: persistedAsset.assetObjectId ?? undefined,
|
|
generationInputs:
|
|
persistedAsset.generationInputs ?? asset.generationInputs,
|
|
persisted: true,
|
|
uploadStatus: undefined,
|
|
uploadProgress: undefined,
|
|
uploadMessage: undefined,
|
|
}
|
|
: asset,
|
|
);
|
|
}
|
|
|
|
export function applyUploadAssetCreateFailure({
|
|
assets,
|
|
uploadAssetId,
|
|
message,
|
|
}: {
|
|
assets: EditorAsset[];
|
|
uploadAssetId: string;
|
|
message: string;
|
|
}) {
|
|
return assets.map((asset) =>
|
|
asset.id === uploadAssetId
|
|
? {
|
|
...asset,
|
|
uploadStatus: 'failed' as const,
|
|
uploadProgress: 100,
|
|
uploadMessage: message,
|
|
}
|
|
: asset,
|
|
);
|
|
}
|
|
|
|
export function applyMeasuredUploadAssetSize({
|
|
assets,
|
|
uploadAssetId,
|
|
originalWidth,
|
|
originalHeight,
|
|
}: {
|
|
assets: EditorAsset[];
|
|
uploadAssetId: string;
|
|
originalWidth: number;
|
|
originalHeight: number;
|
|
}) {
|
|
return assets.map((asset) =>
|
|
asset.id === uploadAssetId
|
|
? {
|
|
...asset,
|
|
width: originalWidth,
|
|
height: originalHeight,
|
|
}
|
|
: asset,
|
|
);
|
|
}
|
|
|
|
export function bindUploadLayerToPersistedAsset({
|
|
layers,
|
|
layerId,
|
|
persistedAsset,
|
|
}: {
|
|
layers: CanvasLayer[];
|
|
layerId: string;
|
|
persistedAsset: PersistedUploadAsset;
|
|
}) {
|
|
return layers.map((layer) =>
|
|
layer.id === layerId
|
|
? {
|
|
...layer,
|
|
src: persistedAsset.imageSrc || layer.src,
|
|
sourceAssetId: persistedAsset.assetId,
|
|
mediaType: persistedAsset.mediaType ?? layer.mediaType,
|
|
objectKey: persistedAsset.objectKey ?? layer.objectKey,
|
|
assetObjectId: persistedAsset.assetObjectId ?? layer.assetObjectId,
|
|
}
|
|
: layer,
|
|
);
|
|
}
|
|
|
|
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 buildUploadedMediaGenerationInputs(
|
|
mediaType: CanvasMediaType,
|
|
durationSeconds: number | undefined,
|
|
): CanvasGenerationInputs | undefined {
|
|
if (
|
|
mediaType === 'image' ||
|
|
typeof durationSeconds !== 'number' ||
|
|
!Number.isFinite(durationSeconds) ||
|
|
durationSeconds <= 0
|
|
) {
|
|
return undefined;
|
|
}
|
|
const roundedDurationSeconds = Math.round(durationSeconds * 1_000) / 1_000;
|
|
return {
|
|
fields: [{ title: '时长', value: `${roundedDurationSeconds}秒` }],
|
|
references: [],
|
|
};
|
|
}
|
|
|
|
export function createUploadCanvasLayer({
|
|
uploadIndex,
|
|
fileName,
|
|
imageSrc,
|
|
objectKey,
|
|
assetObjectId,
|
|
mediaType = 'image',
|
|
imageDimensions,
|
|
durationSeconds,
|
|
canvasPoint,
|
|
canvasSize,
|
|
viewport,
|
|
}: {
|
|
uploadIndex: number;
|
|
fileName?: string;
|
|
imageSrc: string;
|
|
objectKey?: string | null;
|
|
assetObjectId?: string | null;
|
|
mediaType?: CanvasMediaType;
|
|
imageDimensions?: UploadImageDimensions;
|
|
durationSeconds?: number;
|
|
canvasPoint?: CanvasPoint;
|
|
canvasSize: CanvasSize;
|
|
viewport: CanvasViewport;
|
|
}): CanvasLayer {
|
|
const fallbackSize = resolveUploadFallbackSize(mediaType);
|
|
const layerSize =
|
|
mediaType !== 'audio' && imageDimensions
|
|
? resolveLayerResolutionSize(
|
|
imageDimensions.width,
|
|
imageDimensions.height,
|
|
fallbackSize,
|
|
)
|
|
: fallbackSize;
|
|
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 ||
|
|
(mediaType === 'audio'
|
|
? '上传音频'
|
|
: mediaType === 'video'
|
|
? '上传视频'
|
|
: '上传图片'),
|
|
src: imageSrc,
|
|
mediaType,
|
|
assetKind: resolveUploadedAssetKind(mediaType),
|
|
x: worldCenterX - layerSize.width / 2,
|
|
y: worldCenterY - layerSize.height / 2,
|
|
width: layerSize.width,
|
|
height: layerSize.height,
|
|
originalWidth: layerSize.width,
|
|
originalHeight: layerSize.height,
|
|
zIndex: uploadIndex + 10,
|
|
sourceType: 'uploaded',
|
|
sourceAssetId: `upload-${uploadIndex}`,
|
|
objectKey: objectKey ?? undefined,
|
|
assetObjectId: assetObjectId ?? undefined,
|
|
generationInputs: buildUploadedMediaGenerationInputs(
|
|
mediaType,
|
|
durationSeconds,
|
|
),
|
|
};
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|