185 lines
6.2 KiB
JavaScript
185 lines
6.2 KiB
JavaScript
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");
|
|
}
|
|
}
|