rspace-online/lib/mi-action-executor.ts

158 lines
4.7 KiB
TypeScript

/**
* 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<string, any>) => any;
findFreePosition: (width: number, height: number) => { x: number; y: number };
SHAPE_DEFAULTS: Record<string, { width: number; height: number }>;
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, string>): 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<string, string>();
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<string, string>): 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` };
}
}
}