import { DrawingSurface } from "./drawing-core.js"; import { validateOptionalHandler } from "./diagram-component.js"; import { DiagramLink } from "./diagram-link.js"; import { DiagramNode } from "./diagram-node.js"; import { DiagramGroup } from "./diagram-group.js"; /** * Render and interact with a diagram of nodes and links. * * DiagramEngine is the complete public boundary of the drawing engine. It * translates low-level hit-test results to DiagramNode and DiagramLink * handles and owns every rendering object's lifetime. */ export class DiagramEngine { constructor(element) { this.drawingSurface = new DrawingSurface(element); this.handles = new Map(); this.selectionHandler = null; this.activationHandler = null; this.filter = null; this.groups = new Set(); this.destroyed = false; this.drawingSurface.selectionHandler = (component, event) => { if (this.selectionHandler) { this.selectionHandler(component ? this.handleFor(component) : null, event); } }; this.drawingSurface.activationHandler = (component, event) => { if (this.activationHandler) { this.activationHandler(component ? this.handleFor(component) : null, event); } }; } /** Register a callback for a single hit-tested component selection. */ onSelection(handler) { validateOptionalHandler(handler, "selection"); this.selectionHandler = handler || null; return this; } /** Register a callback for activation of one hit-tested component. */ onActivation(handler) { validateOptionalHandler(handler, "activation"); this.activationHandler = handler || null; return this; } /** Create and render a node owned by this engine. */ node(attributes) { this.ensureActive(); const node = new DiagramNode(this, attributes); this.addComponent(node); return node; } /** Create and render a link owned by this engine. */ link(attributes) { this.ensureActive(); const link = new DiagramLink(this, attributes); this.addComponent(link); return link; } /** * Create a generic group frame for nodes and nested groups. * @returns {DiagramGroup} A group owned by this engine. */ group(options) { this.ensureActive(); const group = new DiagramGroup(this, options); this.groups.add(group); return group; } /** * goal : Install a policy that controls component visibility. * pre : filter is null or a function receiving a public component handle. * post : All existing components use the new policy immediately. * result : This engine, for fluent setup. * internals : A policy may return a boolean or `{ visible }`; the component's * own visibility remains the base value and is combined with the policy. */ setFilter(filter) { if (filter !== null && filter !== undefined && typeof filter !== "function") { throw new TypeError("A diagram filter must be a function"); } this.filter = filter || null; for (const handle of this.handles.values()) this.applyFilter(handle); for (const group of this.groups) group.redraw(); return this; } /** Read or update the diagram zoom factor. */ zoom(value) { if (value === undefined) return this.drawingSurface.zoomFactor; const factor = Number(value); if (!Number.isFinite(factor) || factor <= 0) { throw new TypeError("Invalid zoom factor"); } this.drawingSurface.zoomFactor = factor; const element = this.drawingSurface.element(); if (element) element.style.zoom = String(factor); return factor; } /** Destroy the surface and all nodes and links created through this engine. */ destroy() { if (this.destroyed) return; const element = this.drawingSurface.element(); this.selectionHandler = null; this.activationHandler = null; this.drawingSurface.selectionHandler = null; this.drawingSurface.activationHandler = null; for (const group of this.groups) group.destroy(); this.groups.clear(); for (const component of this.drawingSurface.componentList().toArray()) { component.dispose(); component.parentElement(null); } this.drawingSurface.dispose(); this.handles.clear(); this.destroyed = true; if (element && element.parentNode) element.parentNode.removeChild(element); } /** Return the public handle associated with a low-level drawing object. */ handleFor(component) { return this.handles.get(component) || null; } /** Add a newly constructed public component to the drawing surface. */ addComponent(handle) { this.handles.set(handle.component, handle); this.drawingSurface.add(handle.component); this.applyFilter(handle); } /** Remove a public component and forget its low-level drawing object. */ removeComponent(handle) { if (!handle || handle.engine !== this || !this.handles.has(handle.component)) return; this.drawingSurface.remove(handle.component); this.handles.delete(handle.component); } /** Return the DOM surface on which components and group frames are drawn. */ surfaceElement() { return this.drawingSurface.element(); } /** Apply the current base visibility and optional external filter to a handle. */ applyFilter(handle) { const decision = this.filter ? this.filter(handle) : true; let visible = typeof decision === "boolean" ? decision : decision?.visible !== false; if (handle instanceof DiagramLink) { const source = handle.sourceNode(); const target = handle.targetNode(); visible = visible && (!source || source.visible()) && (!target || target.visible()); } handle.component.visible = handle.baseVisible && visible; handle.component.redraw(); if (handle instanceof DiagramNode) { for (const candidate of this.handles.values()) { if (!(candidate instanceof DiagramLink)) continue; if (candidate.sourceNode() === handle || candidate.targetNode() === handle) { this.applyFilter(candidate); } } } for (const group of this.groups) { if (group.members.has(handle)) group.redraw(); } } /** Reject operations after the engine and its DOM surface were destroyed. */ ensureActive() { if (this.destroyed) throw new Error("The diagram engine has been destroyed"); } }