Files
racket-wiki/static/cmap/engine/diagram-link.js
T
2026-09-03 08:40:15 +02:00

114 lines
3.4 KiB
JavaScript

import {
DiagramComponent,
pickAttributes,
validateOptionalHandler
} from "./diagram-component.js";
import { DrawingLink, DrawingSurface, DrawingTriple } from "./drawing-core.js";
import { DiagramNode } from "./diagram-node.js";
const LINK_ATTRIBUTES = [
"content",
"contentType",
"cx",
"cy",
"width",
"height",
"backgroundColor",
"borderColor",
"borderWidth",
"textColor",
"sourceX",
"sourceY",
"targetX",
"targetY",
"lineColor",
"lineWidth",
"hasArrow"
];
/**
* Public rendering handle for one diagram link.
*
* A link owns only visual and interaction state. Its source and target are
* DiagramNode instances from the same DiagramEngine.
*/
export class DiagramLink extends DiagramComponent {
constructor(engine, attributes) {
const component = new DrawingLink(pickAttributes(attributes, LINK_ATTRIBUTES));
super(engine, component, LINK_ATTRIBUTES);
}
/** Read or replace the source node. Pass null to disconnect it. */
sourceNode(node) {
return this.connectNode(DrawingSurface.CONNECTION_TYPE_SOURCE, node);
}
/** Read or replace the target node. Pass null to disconnect it. */
targetNode(node) {
return this.connectNode(DrawingSurface.CONNECTION_TYPE_TARGET, node);
}
/** Register a callback for a source or target connection change. */
onConnectionChange(handler) {
validateOptionalHandler(handler, "connection-change");
this.component.connectionChangeHandler = handler ? (type, node, event) => {
handler(this, type, node ? this.engine.handleFor(node) : null, event);
} : null;
return this;
}
/** Straighten both link segments between their current endpoints. */
straighten() {
const relation = this.component.relations()
.find((candidate) => candidate instanceof DrawingTriple);
const sourceNode = relation ? relation.sourceNode() : null;
const targetNode = relation ? relation.targetNode() : null;
if (!sourceNode || !targetNode) {
this.component.straighten();
return this;
}
const sourcePoint = relation.connectedPoint(
sourceNode, targetNode.cx(), targetNode.cy());
const targetPoint = relation.connectedPoint(
targetNode, sourceNode.cx(), sourceNode.cy());
this.component.straighten(
sourcePoint.x, sourcePoint.y, targetPoint.x, targetPoint.y);
return this;
}
/** Read or update one connected endpoint. */
connectNode(type, node) {
const connectedComponent = this.engine.drawingSurface
.connectedNode(type, this.component);
if (node === undefined) {
return connectedComponent ? this.engine.handleFor(connectedComponent) : null;
}
if (node !== null && !this.validateNode(type, node)) return this;
if (connectedComponent) {
this.engine.drawingSurface.disconnect(type, connectedComponent, this.component);
}
if (node !== null) {
this.engine.drawingSurface.connect(type, node.component, this.component);
}
return this;
}
/** Ensure an endpoint belongs to this engine and is not used at both ends. */
validateNode(type, node) {
if (!(node instanceof DiagramNode) || node.engine !== this.engine) {
throw new TypeError("Invalid diagram node");
}
const otherType = DrawingSurface.anotherConnectionType(type);
const otherNode = this.engine.drawingSurface.connectedNode(otherType, this.component);
if (otherNode === node.component) {
return false;
}
return true;
}
}