import type { PlatformResourceFilterOption } from '../../../packages/shared/src/components/PlatformResourceFilterBar'; import { type CharacterReferenceImage, type EditorAsset, } from './ImageCanvasEditorTypes'; /** * 参考图弹窗的分类口径:只按素材的媒体类型分组,与画布的「功能分类」各自独立 * (画布用 manifest `category`,这里是编辑器素材库自己的可读分类)。 * * `image-sequence` 单列一类,不并进「图片」:序列帧是逐帧动画资产,不是静态图片, * 而参考图链路(`appendLimitedImageReferences`)只接受 `mediaType === 'image'`。 * 混进「图片」会变成「按图片选中、按图片的文案拒掉」——用户看到的分类与它为什么被拒 * 互相矛盾。单列之后行为与音频 / 视频一致:看得见、选得中、给了明确提示。 */ export type ProjectAssetPickerMediaCategory = | 'image' | 'video' | 'audio' | 'image-sequence'; export type ProjectAssetPickerCategory = | 'all' | ProjectAssetPickerMediaCategory; export const PROJECT_ASSET_PICKER_CATEGORY_OPTIONS: readonly PlatformResourceFilterOption[] = [ { id: 'all', label: '全部' }, { id: 'image', label: '图片' }, { id: 'video', label: '视频' }, { id: 'audio', label: '音频' }, { id: 'image-sequence', label: '序列帧' }, ]; export function projectAssetPickerCategory( asset: Pick, ): ProjectAssetPickerMediaCategory { const mediaType = asset.mediaType ?? 'image'; if (mediaType === 'video') return 'video'; if (mediaType === 'audio') return 'audio'; if (mediaType === 'image-sequence') return 'image-sequence'; return 'image'; } /** * 卡片缩略图能不能直接画:图片与序列帧的 `src` 都是图片字节 * (序列帧是首帧 / 当前帧),其它媒体类型退回类型图标。 * * 这条只决定「怎么画」,不决定「能不能当参考图」——后者由参考图链路按 * `mediaType === 'image'` 收口。 */ export function projectAssetPickerUsesThumbnail( category: ProjectAssetPickerMediaCategory, ): boolean { return category === 'image' || category === 'image-sequence'; } /** * 弹窗筛选判据:分类 + 搜索词,两者叠加。 * 与资源画布、`@` 面板的筛选状态无关,调用方各自持有输入值。 */ export function projectAssetMatchesPickerFilter( asset: EditorAsset, { category, query }: { category: ProjectAssetPickerCategory; query: string }, ): boolean { if (category !== 'all' && projectAssetPickerCategory(asset) !== category) { return false; } const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return true; return [asset.label, asset.assetKind ?? ''].some((value) => value.toLowerCase().includes(normalizedQuery), ); } /** * 从项目素材弹窗加进参考图列表的引用 id 前缀。 * 用于在确认时整组替换,避免画布点选 / 上传来的参考图被一并清掉。 */ export const PROJECT_ASSET_REFERENCE_ID_PREFIX = 'project-asset-'; export function isProjectAssetPickerReference(reference: { id: string; }): boolean { return reference.id.startsWith(PROJECT_ASSET_REFERENCE_ID_PREFIX); } /** * 把弹窗里选中的素材转成参考图。`sourceAssetId` 用素材自身 id, * 因此提交时 `createGenerationInputReference` 会按 `asset` 记下这份 ID 快照, * 源素材后续变化不影响已提交任务。 */ export function projectAssetPickerReferences( assets: readonly EditorAsset[], selectedAssetIds: readonly string[], ): CharacterReferenceImage[] { if (selectedAssetIds.length === 0) return []; const assetById = new Map(assets.map((asset) => [asset.id, asset])); const references: CharacterReferenceImage[] = []; const seen = new Set(); for (const assetId of selectedAssetIds) { const asset = assetById.get(assetId); if (!asset || seen.has(asset.id)) continue; seen.add(asset.id); references.push({ id: `${PROJECT_ASSET_REFERENCE_ID_PREFIX}${asset.id}`, label: asset.label, src: asset.objectKey?.trim() || asset.src, mediaType: asset.mediaType ?? 'image', ...(asset.objectKey ? { objectKey: asset.objectKey } : {}), ...(asset.assetObjectId ? { assetObjectId: asset.assetObjectId } : {}), resourceId: asset.sourceResourceId ?? null, sourceAssetId: asset.id, }); } return references; }