Files
Endoleap/src/lib/canvas-subgraphs.ts
T

62 lines
4.9 KiB
TypeScript

import type { CanvasDocument } from "./canvas.ts";
export function subgraphParents(documents: Map<string, CanvasDocument>) {
const parents = new Map<string, { boardId: string; frameId: string }>();
for (const [boardId, document] of documents) for (const node of document.nodes) if (node.data.subgraph) parents.set(node.data.subgraph.boardId, { boardId, frameId: node.id });
return parents;
}
export function subgraphMoveParticipants(documents: Map<string, CanvasDocument>, parent: string, source: string, target: string, ids: string[]) {
const participants = new Set([parent, source, target]);
const parents = subgraphParents(documents);
const addDescendants = (board: string, seen = new Set<string>()) => {
if (seen.has(board)) throw new Error("subgraph_cycle"); seen.add(board); participants.add(board);
for (const node of documents.get(board)?.nodes || []) if (node.data.subgraph) addDescendants(node.data.subgraph.boardId, seen);
};
for (const node of documents.get(source)?.nodes || []) if (ids.includes(node.id) && node.data.subgraph) addDescendants(node.data.subgraph.boardId);
const ancestor = parents.get(parent);
if (ancestor) {
const frame = documents.get(ancestor.boardId)?.nodes.find((node) => node.id === ancestor.frameId);
const affected = frame?.data.ports?.some((port) => {
if (source === parent) return ids.includes(port.nodeId);
const inner = documents.get(parent)?.nodes.find((node) => node.id === port.nodeId);
return inner?.data.subgraph?.boardId === source && inner.data.ports?.some((p) => `port:${p.id}` === port.handle && p.direction === port.direction && ids.includes(p.nodeId));
});
if (affected) participants.add(ancestor.boardId);
}
return [...participants];
}
export function subgraphMovePath(documents: Map<string, CanvasDocument>, source: string, target: string) {
const parents = subgraphParents(documents);
const chain = (board: string) => { const result = [board]; while (parents.has(board)) { board = parents.get(board)!.boardId; if (result.includes(board)) throw new Error("subgraph_cycle"); result.push(board); } return result; };
const from = chain(source), to = chain(target), common = from.find((id) => to.includes(id));
if (!common) throw new Error("ownership_conflict");
const path: { parent: string; frame: string; source: string; target: string }[] = [];
for (let i = 0; from[i] !== common; i++) { const relation = parents.get(from[i])!; path.push({ parent: relation.boardId, frame: relation.frameId, source: from[i], target: relation.boardId }); }
for (let i = to.indexOf(common) - 1; i >= 0; i--) { const relation = parents.get(to[i])!; path.push({ parent: relation.boardId, frame: relation.frameId, source: relation.boardId, target: to[i] }); }
return path;
}
export function exposedConnection(documents: Map<string, CanvasDocument>, source: { boardId: string; id: string; handle?: string | null }, target: { boardId: string; id: string; handle?: string | null }) {
const parents = subgraphParents(documents);
const ancestors = (id: string) => { const result = [id]; while (parents.has(id)) { id = parents.get(id)!.boardId; if (result.includes(id)) throw new Error("subgraph_cycle"); result.push(id); } return result; };
const owner = ancestors(source.boardId).find((id) => ancestors(target.boardId).includes(id));
if (!owner) throw new Error("ownership_conflict");
const expose = (endpoint: typeof source, direction: "input" | "output") => {
let { boardId, id, handle } = endpoint;
while (boardId !== owner) {
const parent = parents.get(boardId)!;
const frame = documents.get(parent.boardId)?.nodes.find((node) => node.id === parent.frameId);
const port = frame?.data.ports?.find((port) => port.nodeId === id && port.handle === handle && port.direction === direction);
if (!port) throw new Error("boundary_port_required");
id = parent.frameId; handle = `port:${port.id}`; boardId = parent.boardId;
}
return { id, handle };
};
return { boardId: owner, source: expose(source, "output"), target: expose(target, "input") };
}
export function connectionTransactionPlan(documents: Map<string, CanvasDocument>, source: { boardId: string; id: string; handle: string }, target: { boardId: string; id: string; handle: string }) {
const parents = subgraphParents(documents);
const path = (id: string) => { const result = [id]; while (parents.has(id)) { id = parents.get(id)!.boardId; if (result.includes(id)) throw new Error("subgraph_cycle"); result.push(id); } return result; };
const from = path(source.boardId), to = path(target.boardId), owner = from.find((id) => to.includes(id));
if (!owner) throw new Error("ownership_conflict");
return { boardId: owner, participants: [...new Set([...from.slice(0, from.indexOf(owner) + 1), ...to.slice(0, to.indexOf(owner) + 1)])], source: { board_id: source.boardId, node_id: source.id, handle: source.handle }, target: { board_id: target.boardId, node_id: target.id, handle: target.handle } };
}