/** * MiActionExecutor — Executes parsed MI actions against the live canvas. * * Relies on `window.__canvasApi` exposed by canvas.html, which provides: * { newShape, findFreePosition, SHAPE_DEFAULTS, setupShapeEventListeners, sync, canvasContent } */ import type { MiAction } from "./mi-actions"; export interface ExecutionResult { action: MiAction; ok: boolean; shapeId?: string; error?: string; } interface CanvasApi { newShape: (tagName: string, props?: Record) => any; findFreePosition: (width: number, height: number) => { x: number; y: number }; SHAPE_DEFAULTS: Record; setupShapeEventListeners: (shape: any) => void; sync: any; canvasContent: HTMLElement; } function getCanvasApi(): CanvasApi | null { return (window as any).__canvasApi || null; } /** Resolve `$N` backreferences in an id string. */ function resolveRef(id: string, refMap: Map): string { if (id.startsWith("$")) { return refMap.get(id) || id; } return id; } export class MiActionExecutor { static instance: MiActionExecutor | null = null; constructor() { if (MiActionExecutor.instance) return MiActionExecutor.instance; MiActionExecutor.instance = this; } execute(actions: MiAction[]): ExecutionResult[] { const api = getCanvasApi(); if (!api) { return actions.map((a) => ({ action: a, ok: false, error: "Canvas API not available" })); } const results: ExecutionResult[] = []; const refMap = new Map(); for (const action of actions) { try { const result = this.#executeOne(action, api, refMap); results.push(result); } catch (e: any) { results.push({ action, ok: false, error: e.message }); } } return results; } #executeOne(action: MiAction, api: CanvasApi, refMap: Map): ExecutionResult { switch (action.type) { case "create-shape": { const shape = api.newShape(action.tagName, action.props || {}); if (!shape) { return { action, ok: false, error: `Failed to create ${action.tagName}` }; } if (action.ref) { refMap.set(action.ref, shape.id); } return { action, ok: true, shapeId: shape.id }; } case "update-shape": { const el = api.canvasContent.querySelector(`#${CSS.escape(action.shapeId)}`); if (!el) { return { action, ok: false, error: `Shape ${action.shapeId} not found` }; } for (const [key, value] of Object.entries(action.fields)) { (el as any)[key] = value; } api.sync.updateShape?.(action.shapeId); return { action, ok: true, shapeId: action.shapeId }; } case "delete-shape": { const el = api.canvasContent.querySelector(`#${CSS.escape(action.shapeId)}`); if (!el) { return { action, ok: false, error: `Shape ${action.shapeId} not found` }; } api.sync.deleteShape(action.shapeId); el.remove(); return { action, ok: true, shapeId: action.shapeId }; } case "connect": { const sourceId = resolveRef(action.sourceId, refMap); const targetId = resolveRef(action.targetId, refMap); const arrow = document.createElement("folk-arrow"); const arrowId = `arrow-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; arrow.id = arrowId; (arrow as any).sourceId = sourceId; (arrow as any).targetId = targetId; if (action.color) (arrow as any).color = action.color; api.canvasContent.appendChild(arrow); api.sync.registerShape(arrow); return { action, ok: true, shapeId: arrowId }; } case "move-shape": { const el = api.canvasContent.querySelector(`#${CSS.escape(action.shapeId)}`); if (!el) { return { action, ok: false, error: `Shape ${action.shapeId} not found` }; } (el as any).x = action.x; (el as any).y = action.y; api.sync.updateShape?.(action.shapeId); return { action, ok: true, shapeId: action.shapeId }; } case "transform": { // Delegate to mi-selection-transforms if available const transforms = (window as any).__miSelectionTransforms; if (transforms && transforms[action.transform]) { const shapeEls = action.shapeIds .map((id) => api.canvasContent.querySelector(`#${CSS.escape(id)}`)) .filter(Boolean) as HTMLElement[]; if (shapeEls.length === 0) { return { action, ok: false, error: "No matching shapes found" }; } transforms[action.transform](shapeEls); // Persist positions for (const el of shapeEls) { api.sync.updateShape?.(el.id); } return { action, ok: true }; } return { action, ok: false, error: `Unknown transform: ${action.transform}` }; } case "navigate": { window.location.href = action.path; return { action, ok: true }; } default: return { action, ok: false, error: `Unknown action type` }; } } }