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

309 lines
7.6 KiB
JavaScript

/**
* Keep diagram endpoints and connector controls geometrically related.
* Derived from ionstage/cmap 0.1.3, (c) 2015 iOnStage, MIT License.
*/
import { Connector, DrawingLink as Link, DrawingNode as Node } from "./drawing-components.js";
class Relation {
constructor() {
}
prop(initialValue) {
var cache = initialValue;
return function(value) {
if (typeof value === 'undefined')
return cache;
cache = value;
};
}
update() {
}
}
class Triple extends Relation {
constructor(props) {
super();
this.link = this.prop(props.link);
this.sourceNode = this.prop(props.sourceNode || null);
this.targetNode = this.prop(props.targetNode || null);
this.skipNextUpdate = this.prop(false);
this.nodePositionsCache = this.prop({});
}
update(changedComponent) {
if (this.skipNextUpdate()) {
this.skipNextUpdate(false);
return;
}
var link = this.link();
var sourceNode = this.sourceNode();
var targetNode = this.targetNode();
if (changedComponent instanceof Node)
this.updateNode(link, sourceNode, targetNode, changedComponent);
else if (changedComponent instanceof Link)
this.updateLink(link, sourceNode, targetNode);
}
updateNode(link, sourceNode, targetNode, changedNode) {
if (sourceNode && targetNode)
this.rotateLink(link, sourceNode, targetNode, changedNode);
else
this.shiftLink(link, sourceNode, targetNode, changedNode);
this.updateNodePositionsCache();
}
rotateLink(link, sourceNode, targetNode, changedNode) {
var cache = this.nodePositionsCache();
var sncx = cache.sncx;
var sncy = cache.sncy;
var tncx = cache.tncx;
var tncy = cache.tncy;
var lcx = link.cx();
var lcy = link.cy();
var ts_dx = tncx - sncx;
var ts_dy = tncy - sncy;
var cs_dx = lcx - sncx;
var cs_dy = lcy - sncy;
var ts_rad0 = Math.atan2(ts_dy, ts_dx);
var cs_rad0 = Math.atan2(cs_dy, cs_dx);
// changed node position
if (changedNode === sourceNode) {
sncx = sourceNode.cx();
sncy = sourceNode.cy();
} else if (changedNode === targetNode) {
tncx = targetNode.cx();
tncy = targetNode.cy();
}
// center positions of two nodes are equal
if (cs_rad0 === 0) {
link.cx((sncx + tncx) / 2);
link.cy((sncy + tncy) / 2);
return;
}
var ts_d0 = Math.sqrt(ts_dx * ts_dx + ts_dy * ts_dy);
var cs_d0 = Math.sqrt(cs_dx * cs_dx + cs_dy * cs_dy);
var ts_cs_rad = ts_rad0 - cs_rad0;
ts_dx = tncx - sncx;
ts_dy = tncy - sncy;
var ts_rad1 = Math.atan2(ts_dy, ts_dx);
var cs_rad1 = ts_rad1 - ts_cs_rad;
var ts_d1 = Math.sqrt(ts_dx * ts_dx + ts_dy * ts_dy);
var d_rate = (ts_d0 !== 0) ? ts_d1 / ts_d0 : 1;
var cs_d1 = cs_d0 * d_rate;
lcx = sncx + cs_d1 * Math.cos(cs_rad1);
lcy = sncy + cs_d1 * Math.sin(cs_rad1);
link.cx(lcx);
link.cy(lcy);
}
shiftLink(link, sourceNode, targetNode, changedNode) {
var cache = this.nodePositionsCache();
var ncx = changedNode.cx();
var ncy = changedNode.cy();
if (changedNode === sourceNode) {
link.targetX(link.targetX() + (ncx - cache.sncx));
link.targetY(link.targetY() + (ncy - cache.sncy));
} else if (changedNode === targetNode) {
link.sourceX(link.sourceX() + (ncx - cache.tncx));
link.sourceY(link.sourceY() + (ncy - cache.tncy));
}
}
updateLink(link, sourceNode, targetNode) {
var lx, ly, p;
if (sourceNode) {
// connect link to source node
lx = targetNode ? link.cx() : link.targetX();
ly = targetNode ? link.cy() : link.targetY();
p = this.connectedPoint(sourceNode, lx, ly);
link.sourceX(p.x);
link.sourceY(p.y);
}
if (targetNode) {
// connect link to target node
lx = sourceNode ? link.cx() : link.sourceX();
ly = sourceNode ? link.cy() : link.sourceY();
p = this.connectedPoint(targetNode, lx, ly);
link.targetX(p.x);
link.targetY(p.y);
}
if (!sourceNode || !targetNode) {
// link content moves to midpoint
link.cx((link.sourceX() + link.targetX()) / 2);
link.cy((link.sourceY() + link.targetY()) / 2);
}
}
updateLinkAngle(radians) {
var link = this.link();
var sourceNode = this.sourceNode();
var targetNode = this.targetNode();
var ldx = link.targetX() - link.sourceX();
var ldy = link.targetY() - link.sourceY();
var d = Math.sqrt(ldx * ldx + ldy * ldy);
var connectedNode = sourceNode || targetNode;
var cx = connectedNode.cx();
var cy = connectedNode.cy();
var lx = cx + d * Math.cos(radians);
var ly = cy + d * Math.sin(radians);
var p = this.connectedPoint(connectedNode, lx, ly);
if (connectedNode === sourceNode)
link.straighten(p.x, p.y, lx + p.x - cx, ly + p.y - cy);
else if (connectedNode === targetNode)
link.straighten(lx + p.x - cx, ly + p.y - cy, p.x, p.y);
}
updateNodePositionsCache() {
var sourceNode = this.sourceNode();
var targetNode = this.targetNode();
var cache = this.nodePositionsCache();
if (sourceNode) {
cache.sncx = sourceNode.cx();
cache.sncy = sourceNode.cy();
}
if (targetNode) {
cache.tncx = targetNode.cx();
cache.tncy = targetNode.cy();
}
}
connectedPoint(node, lx, ly) {
var nx = node.x();
var ny = node.y();
var nwidth = node.width();
var nheight = node.height();
var ncx = node.cx();
var ncy = node.cy();
var alpha = Math.atan2(ly - ncy, lx - ncx);
var beta = Math.PI / 2 - alpha;
var t = Math.atan2(nheight, nwidth);
var x, y;
// left edge
if (alpha < t - Math.PI || alpha > Math.PI - t) {
x = nx;
y = ncy - nwidth * Math.tan(alpha) / 2;
}
// top edge
else if (alpha < -t) {
x = ncx - nheight * Math.tan(beta) / 2;
y = ny;
}
// right edge
else if (alpha < t) {
x = nx + nwidth;
y = ncy + nwidth * Math.tan(alpha) / 2;
}
// bottom edge
else {
x = ncx + nheight * Math.tan(beta) / 2;
y = ny + nheight;
}
var x0, y0, l, ex, ey;
var r = node.borderRadius();
var atCorner = false;
// top-left corner
if (x < nx + r && y < ny + r) {
x0 = nx + r;
y0 = ny + r;
atCorner = true;
}
// top-right corner
else if (x > nx + nwidth - r && y < ny + r) {
x0 = nx + nwidth - r;
y0 = ny + r;
atCorner = true;
}
// bottom-left corner
else if (x < nx + r && y > ny + nheight - r) {
x0 = nx + r;
y0 = ny + nheight - r;
atCorner = true;
}
// bottom-right corner
else if (x > nx + nwidth - r && y > ny + nheight - r) {
x0 = nx + nwidth - r;
y0 = ny + nheight - r;
atCorner = true;
}
if (atCorner) {
l = Math.sqrt((x0 - x) * (x0 - x) + (y0 - y) * (y0 - y));
ex = (x0 - x) / l;
ey = (y0 - y) / l;
x = x0 - r * ex;
y = y0 - r * ey;
}
return {
x: x,
y: y
};
}
}
class LinkConnectorRelation extends Relation {
constructor(props) {
super();
this.type = this.prop(props.type);
this.link = this.prop(props.link);
this.connector = this.prop(props.connector);
}
isConnected(isConnected) {
var color = isConnected ? Connector.COLOR_CONNECTED : Connector.COLOR_UNCONNECTED;
this.connector().color(color);
}
update(changedComponent) {
var type = this.type();
var link = this.link();
var connector = this.connector();
if (changedComponent === link) {
connector.x(link[type + 'X']());
connector.y(link[type + 'Y']());
}
}
}
export { LinkConnectorRelation, Triple as DrawingTriple };