178 lines
5.3 KiB
TypeScript
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);
|
|
}
|