ai-chatbot/lib/canvas/video-workspace.ts

178 lines
5.3 KiB
TypeScript

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<string, unknown> & {
edges: Array<Record<string, unknown>>;
nodes: Array<Record<string, unknown>>;
selectedNodeIds?: string[];
viewport: { x: number; y: number; zoom: number };
};
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: 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<string, string>,
): 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<string, string>();
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<string, unknown>;
const edges = Array.isArray(remapped.edges) ? remapped.edges : [];
remapped.edges = edges.map((edge) => ({
...(asRecord(edge) || {}),
id: `edge-${createId()}`,
}));
return withVideoCanvasWorkspaceMeta(remapped, meta);
}