145 lines
3.8 KiB
TypeScript
145 lines
3.8 KiB
TypeScript
/**
|
|
* MiCanvasBridge — Singleton that connects canvas state to the MI assistant.
|
|
*
|
|
* Listens to selection, viewport, and connection changes on the canvas
|
|
* and exposes a structured context snapshot for the MI system prompt.
|
|
*/
|
|
|
|
export interface ShapeInfo {
|
|
id: string;
|
|
type: string;
|
|
x: number;
|
|
y: number;
|
|
width: number;
|
|
height: number;
|
|
content?: string;
|
|
title?: string;
|
|
}
|
|
|
|
export interface Connection {
|
|
arrowId: string;
|
|
sourceId: string;
|
|
targetId: string;
|
|
}
|
|
|
|
export interface ShapeGroup {
|
|
shapeIds: string[];
|
|
}
|
|
|
|
export interface CanvasContext {
|
|
selectedShapes: ShapeInfo[];
|
|
allShapes: ShapeInfo[];
|
|
connections: Connection[];
|
|
viewport: { x: number; y: number; scale: number };
|
|
shapeGroups: ShapeGroup[];
|
|
shapeCountByType: Record<string, number>;
|
|
}
|
|
|
|
export class MiCanvasBridge {
|
|
static instance: MiCanvasBridge | null = null;
|
|
|
|
selectedShapeIds: string[] = [];
|
|
viewport = { x: 0, y: 0, scale: 1 };
|
|
|
|
private canvasContent: HTMLElement | null = null;
|
|
|
|
constructor(canvasContent?: HTMLElement) {
|
|
if (MiCanvasBridge.instance) return MiCanvasBridge.instance;
|
|
this.canvasContent = canvasContent || document.getElementById("canvas-content");
|
|
MiCanvasBridge.instance = this;
|
|
}
|
|
|
|
setSelection(ids: string[]) {
|
|
this.selectedShapeIds = ids;
|
|
}
|
|
|
|
setViewport(x: number, y: number, scale: number) {
|
|
this.viewport = { x, y, scale };
|
|
}
|
|
|
|
/** Build full context snapshot for the MI system prompt. */
|
|
getCanvasContext(): CanvasContext {
|
|
const allShapes = this.#collectShapes();
|
|
const connections = this.#collectConnections();
|
|
const selectedShapes = allShapes.filter((s) => this.selectedShapeIds.includes(s.id));
|
|
const shapeGroups = this.#buildShapeGroups(allShapes, connections);
|
|
const shapeCountByType: Record<string, number> = {};
|
|
for (const s of allShapes) {
|
|
shapeCountByType[s.type] = (shapeCountByType[s.type] || 0) + 1;
|
|
}
|
|
|
|
return {
|
|
selectedShapes,
|
|
allShapes,
|
|
connections,
|
|
viewport: { ...this.viewport },
|
|
shapeGroups,
|
|
shapeCountByType,
|
|
};
|
|
}
|
|
|
|
#collectShapes(): ShapeInfo[] {
|
|
if (!this.canvasContent) return [];
|
|
return [...this.canvasContent.children]
|
|
.filter(
|
|
(el) =>
|
|
el.tagName?.includes("-") &&
|
|
el.id &&
|
|
!el.tagName.toLowerCase().includes("arrow"),
|
|
)
|
|
.map((el: any) => ({
|
|
id: el.id,
|
|
type: el.tagName.toLowerCase(),
|
|
x: el.x ?? 0,
|
|
y: el.y ?? 0,
|
|
width: el.width ?? 0,
|
|
height: el.height ?? 0,
|
|
...(el.content ? { content: String(el.content).slice(0, 120) } : {}),
|
|
...(el.title ? { title: String(el.title).slice(0, 80) } : {}),
|
|
}));
|
|
}
|
|
|
|
#collectConnections(): Connection[] {
|
|
if (!this.canvasContent) return [];
|
|
return [...this.canvasContent.querySelectorAll("folk-arrow")]
|
|
.filter((el: any) => el.id && el.sourceId && el.targetId)
|
|
.map((el: any) => ({
|
|
arrowId: el.id,
|
|
sourceId: el.sourceId,
|
|
targetId: el.targetId,
|
|
}));
|
|
}
|
|
|
|
/** BFS on the arrow graph to find clusters of connected shapes. */
|
|
#buildShapeGroups(shapes: ShapeInfo[], connections: Connection[]): ShapeGroup[] {
|
|
const adj = new Map<string, Set<string>>();
|
|
for (const c of connections) {
|
|
if (!adj.has(c.sourceId)) adj.set(c.sourceId, new Set());
|
|
if (!adj.has(c.targetId)) adj.set(c.targetId, new Set());
|
|
adj.get(c.sourceId)!.add(c.targetId);
|
|
adj.get(c.targetId)!.add(c.sourceId);
|
|
}
|
|
|
|
const visited = new Set<string>();
|
|
const groups: ShapeGroup[] = [];
|
|
|
|
for (const shape of shapes) {
|
|
if (visited.has(shape.id) || !adj.has(shape.id)) continue;
|
|
const group: string[] = [];
|
|
const queue = [shape.id];
|
|
while (queue.length) {
|
|
const id = queue.shift()!;
|
|
if (visited.has(id)) continue;
|
|
visited.add(id);
|
|
group.push(id);
|
|
for (const neighbor of adj.get(id) || []) {
|
|
if (!visited.has(neighbor)) queue.push(neighbor);
|
|
}
|
|
}
|
|
if (group.length > 1) groups.push({ shapeIds: group });
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
}
|