import { CANVAS_PROJECT_TITLE_MAX_LENGTH } from "@/lib/canvas/project-title"; export const VIDEO_CANVAS_WORKSPACE_META_KEY = "videoCanvasWorkspace"; export const DEFAULT_VIDEO_CANVAS_NAME = "画布 1"; export type VideoCanvasWorkspaceMeta = { canvasName: string; workspaceId: string; }; export type VideoCanvasWorkspaceProjectLike = { canvasData?: unknown; id: string; }; export type VideoCanvasDataRecord = Record & { edges: Array>; nodes: Array>; selectedNodeIds?: string[]; viewport: { x: number; y: number; zoom: number }; }; function asRecord(value: unknown): Record | null { return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : null; } export function normalizeVideoCanvasName( value: unknown, fallback = DEFAULT_VIDEO_CANVAS_NAME, ) { const normalizedFallback = String(fallback || DEFAULT_VIDEO_CANVAS_NAME) .slice(0, CANVAS_PROJECT_TITLE_MAX_LENGTH) .trim() || DEFAULT_VIDEO_CANVAS_NAME; const candidate = String(value ?? "") .slice(0, CANVAS_PROJECT_TITLE_MAX_LENGTH) .trim(); return candidate || normalizedFallback; } export function getExplicitVideoCanvasWorkspaceMeta( canvasData: unknown, ): VideoCanvasWorkspaceMeta | null { const record = asRecord(canvasData); const meta = asRecord(record?.[VIDEO_CANVAS_WORKSPACE_META_KEY]); const workspaceId = String(meta?.workspaceId || "").trim(); const canvasName = normalizeVideoCanvasName(meta?.canvasName, ""); if (!(workspaceId && canvasName)) return null; return { canvasName, workspaceId }; } export function resolveVideoCanvasWorkspaceMeta( project: VideoCanvasWorkspaceProjectLike, ): VideoCanvasWorkspaceMeta { return ( getExplicitVideoCanvasWorkspaceMeta(project.canvasData) || { canvasName: DEFAULT_VIDEO_CANVAS_NAME, workspaceId: project.id, } ); } export function getVideoCanvasWorkspaceProjects< T extends VideoCanvasWorkspaceProjectLike, >(projects: T[], currentProject: T) { const { workspaceId } = resolveVideoCanvasWorkspaceMeta(currentProject); return projects.filter((project) => { if (project.id === currentProject.id || project.id === workspaceId) { return true; } return ( getExplicitVideoCanvasWorkspaceMeta(project.canvasData)?.workspaceId === workspaceId ); }); } export function withVideoCanvasWorkspaceMeta( canvasData: unknown, meta: VideoCanvasWorkspaceMeta, ): VideoCanvasDataRecord { const record = asRecord(canvasData) || {}; const viewport = asRecord(record.viewport); const selectedNodeIds = Array.isArray(record.selectedNodeIds) ? record.selectedNodeIds.filter( (nodeId): nodeId is string => typeof nodeId === "string", ) : undefined; return { ...record, edges: Array.isArray(record.edges) ? record.edges.map((edge) => asRecord(edge) || {}) : [], nodes: Array.isArray(record.nodes) ? record.nodes.map((node) => asRecord(node) || {}) : [], ...(selectedNodeIds ? { selectedNodeIds } : {}), viewport: { x: Number(viewport?.x) || 0, y: Number(viewport?.y) || 0, zoom: Number(viewport?.zoom) || 1, }, [VIDEO_CANVAS_WORKSPACE_META_KEY]: { canvasName: normalizeVideoCanvasName(meta.canvasName), workspaceId: meta.workspaceId, }, }; } export function getNextVideoCanvasName(existingNames: string[]) { const names = new Set(existingNames.map((name) => normalizeVideoCanvasName(name))); let index = 1; while (names.has(`画布 ${index}`)) index += 1; return `画布 ${index}`; } export function getVideoCanvasCopyName( sourceName: string, existingNames: string[], ) { const names = new Set(existingNames.map((name) => normalizeVideoCanvasName(name))); const normalizedSource = normalizeVideoCanvasName(sourceName); let index = 1; while (true) { const suffix = `副本${index}`; const candidate = `${normalizedSource.slice( 0, CANVAS_PROJECT_TITLE_MAX_LENGTH - suffix.length, )}${suffix}`; if (!names.has(candidate)) return candidate; index += 1; } } function remapCanvasValue( value: unknown, nodeIds: Map, ): unknown { if (typeof value === "string") return nodeIds.get(value) || value; if (Array.isArray(value)) { return value.map((item) => remapCanvasValue(item, nodeIds)); } const record = asRecord(value); if (!record) return value; return Object.fromEntries( Object.entries(record).map(([key, item]) => [ key, remapCanvasValue(item, nodeIds), ]), ); } export function duplicateVideoCanvasData( canvasData: unknown, meta: VideoCanvasWorkspaceMeta, createId = () => crypto.randomUUID(), ) { const record = asRecord(canvasData) || {}; const nodes = Array.isArray(record.nodes) ? record.nodes : []; const nodeIds = new Map(); for (const node of nodes) { const nodeId = String(asRecord(node)?.id || "").trim(); if (nodeId) nodeIds.set(nodeId, `node-${createId()}`); } const remapped = remapCanvasValue(record, nodeIds) as Record; const edges = Array.isArray(remapped.edges) ? remapped.edges : []; remapped.edges = edges.map((edge) => ({ ...(asRecord(edge) || {}), id: `edge-${createId()}`, })); return withVideoCanvasWorkspaceMeta(remapped, meta); }