big cmap refactoring

This commit is contained in:
2026-09-03 08:40:15 +02:00
parent f0562a06cc
commit 2c141b6d3f
32 changed files with 5616 additions and 4216 deletions
+524
View File
@@ -0,0 +1,524 @@
/**
* Render the node, link and connector primitives of a diagram.
*
* These classes contain geometry and DOM presentation only. Application code
* reaches them through DiagramNode and DiagramLink handles.
* Derived from ionstage/cmap 0.1.3, (c) 2015 iOnStage, MIT License.
*/
import { Component, dom, helper } from "./drawing-support.js";
class Node extends Component {
constructor(props) {
super();
this.visible = true;
this.content = this.prop(props.content, '', helper.toString);
this.contentType = this.prop(props.contentType, helper.CONTENT_TYPE_TEXT, helper.toContentType);
this.x = this.prop(props.x, 0, helper.toNumber);
this.y = this.prop(props.y, 0, helper.toNumber);
this.width = this.prop(props.width, 75, helper.toNumber);
this.height = this.prop(props.height, 30, helper.toNumber);
this.backgroundColor = this.prop(props.backgroundColor, '#a7cbe6', helper.toString);
this.borderColor = this.prop(props.borderColor, '#333', helper.toString);
this.borderWidth = this.prop(props.borderWidth, 2, helper.toNumber);
this.textColor = this.prop(props.textColor, '#333', helper.toString);
this.zIndex = this.prop('auto');
this.element = this.prop(null);
this.parentElement = this.prop(null);
this.cache = this.prop({});
this.relations = this.prop([]);
this.moveHandler = null;
}
cx() {
return this.x() + this.width() / 2;
}
cy() {
return this.y() + this.height() / 2;
}
borderRadius() {
return 4;
}
contains(x, y, tolerance) {
var nx = this.x();
var ny = this.y();
var nwidth = this.width();
var nheight = this.height();
return (nx - tolerance <= x && x <= nx + nwidth + tolerance &&
ny - tolerance <= y && y <= ny + nheight + tolerance);
}
style() {
var contentType = this.contentType();
var lineHeight = (contentType === helper.CONTENT_TYPE_TEXT) ? this.height() : 14;
var textAlign = (contentType === helper.CONTENT_TYPE_TEXT) ? 'center' : 'left';
var translate = 'translate(' + this.x() + 'px, ' + this.y() + 'px)';
var borderWidthOffset = this.borderWidth() * 2;
return {
backgroundColor: this.backgroundColor(),
border: this.borderWidth() + 'px solid ' + this.borderColor(),
borderRadius: this.borderRadius() + 'px',
color: this.textColor(),
display: this.visible ? '' : 'none',
height: (this.height() - borderWidthOffset) + 'px',
lineHeight: (lineHeight - borderWidthOffset) + 'px',
msTransform: translate,
overflow: 'hidden',
pointerEvents: 'auto',
position: 'absolute',
textAlign: textAlign,
textOverflow: 'ellipsis',
transform: translate,
webkitTransform: translate,
whiteSpace: 'nowrap',
width: (this.width() - borderWidthOffset) + 'px',
zIndex: this.zIndex()
};
}
redraw() {
var element = this.element();
var parentElement = this.parentElement();
if (!parentElement && !element)
return;
// add element
if (parentElement && !element) {
element = dom.el('<div>');
this.element(element);
dom.append(parentElement, element);
this.redraw();
return;
}
// remove element
if (!parentElement && element) {
dom.remove(element);
this.element(null);
this.cache({});
return;
}
var cache = this.cache();
// update element
var content = this.content();
if (content !== cache.content) {
var contentType = this.contentType();
if (contentType === helper.CONTENT_TYPE_TEXT)
dom.text(element, content);
else if (contentType === helper.CONTENT_TYPE_HTML)
dom.html(element, content);
cache.content = content;
}
var style = this.style();
dom.css(element, helper.diffObj(style, cache.style));
cache.style = style;
this.notifyRendered();
}
}
class Link extends Component {
constructor(props) {
super();
this.visible = true;
this.content = this.prop(props.content, '', helper.toString);
this.contentType = this.prop(props.contentType, helper.CONTENT_TYPE_TEXT, helper.toContentType);
this.cx = this.prop(props.cx, 100, helper.toNumber);
this.cy = this.prop(props.cy, 40, helper.toNumber);
this.width = this.prop(props.width, 50, helper.toNumber);
this.height = this.prop(props.height, 20, helper.toNumber);
this.backgroundColor = this.prop(props.backgroundColor, 'white', helper.toString);
this.borderColor = this.prop(props.borderColor, '#333', helper.toString);
this.borderWidth = this.prop(props.borderWidth, 2, helper.toNumber);
this.textColor = this.prop(props.textColor, '#333', helper.toString);
this.sourceX = this.prop(props.sourceX, this.cx() - 70, helper.toNumber);
this.sourceY = this.prop(props.sourceY, this.cy(), helper.toNumber);
this.targetX = this.prop(props.targetX, this.cx() + 70, helper.toNumber);
this.targetY = this.prop(props.targetY, this.cy(), helper.toNumber);
this.lineColor = this.prop(props.lineColor, '#333', helper.toString);
this.lineWidth = this.prop(props.lineWidth, 2, helper.toNumber);
this.hasArrow = this.prop(props.hasArrow, true, helper.toBoolean);
this.zIndex = this.prop('auto');
this.element = this.prop(null);
this.parentElement = this.prop(null);
this.cache = this.prop({});
this.relations = this.prop([]);
this.connectionChangeHandler = null;
}
straighten(sx, sy, tx, ty) {
if (arguments.length === 0) {
this.cx((this.sourceX() + this.targetX()) / 2);
this.cy((this.sourceY() + this.targetY()) / 2);
return;
}
this.cx((sx + tx) / 2);
this.cy((sy + ty) / 2);
this.sourceX(sx);
this.sourceY(sy);
this.targetX(tx);
this.targetY(ty);
}
contains(x, y, tolerance) {
var content = this.content();
var lcx = this.cx();
var lcy = this.cy();
// content area
if (content) {
var lwidth = this.width();
var lheight = this.height();
var lx = lcx - lwidth / 2;
var ly = lcy - lheight / 2;
if (lx - tolerance <= x && x <= lx + lwidth + tolerance &&
ly - tolerance <= y && y <= ly + lheight + tolerance) {
return true;
}
}
var lineWidth = this.lineWidth();
// source path
if (this.containsPath(this.sourceX(), this.sourceY(), lcx, lcy, x, y, lineWidth / 2 + tolerance))
return true;
// target path
if (this.containsPath(this.targetX(), this.targetY(), lcx, lcy, x, y, lineWidth / 2 + tolerance))
return true;
return false;
}
containsPath(x0, y0, x1, y1, x, y, d) {
var ax = x1 - x0;
var ay = y1 - y0;
var bx = x - x0;
var by = y - y0;
var r = (ax * bx + ay * by) / (ax * ax + ay * ay);
if (0 <= r && r <= 1) {
var px = x0 + r * ax;
var py = y0 + r * ay;
var dx = px - x;
var dy = py - y;
if (dx * dx + dy * dy <= d * d)
return true;
}
return false;
}
style() {
return {
display: this.visible ? '' : 'none',
pointerEvents: 'none',
position: 'absolute',
zIndex: this.zIndex()
};
}
pathContainerStyle() {
var width = Math.max(this.cx(), this.sourceX(), this.targetX());
var height = Math.max(this.cy(), this.sourceY(), this.targetY());
return {
height: height + 'px',
overflow: 'visible',
position: 'absolute',
width: width + 'px'
};
}
lineAttributes() {
var d = [
'M', this.sourceX(), this.sourceY(),
'L', this.cx(), this.cy(),
'L', this.targetX(), this.targetY()
].join(' ');
return {
d: d,
fill: 'none',
stroke: this.lineColor(),
'stroke-linecap': 'round',
'stroke-width': this.lineWidth()
};
}
arrowAttributes() {
var cx = this.cx();
var cy = this.cy();
var tx = this.targetX();
var ty = this.targetY();
var radians = Math.atan2(ty - cy, tx - cx);
var p0 = {
x: 15 * Math.cos(radians - 26 * Math.PI / 180),
y: 15 * Math.sin(radians - 26 * Math.PI / 180)
};
var p1 = {
x: 15 * Math.cos(radians + 26 * Math.PI / 180),
y: 15 * Math.sin(radians + 26 * Math.PI / 180)
};
var p2 = {
x: 7 * Math.cos(radians),
y: 7 * Math.sin(radians)
};
var d = [
'M', tx - p0.x, ty - p0.y,
'L', tx, ty,
'L', tx - p1.x, ty - p1.y,
'Q', tx - p2.x, ty - p2.y, tx - p0.x, ty - p0.y,
'Z'
].join(' ');
return {
d: d,
fill: this.lineColor(),
stroke: this.lineColor(),
'stroke-linejoin': 'round',
'stroke-width': this.lineWidth(),
visibility: this.hasArrow() ? 'visible' : 'hidden'
};
}
contentStyle() {
var contentType = this.contentType();
var lineHeight = (contentType === helper.CONTENT_TYPE_TEXT) ? this.height() : 14;
var textAlign = (contentType === helper.CONTENT_TYPE_TEXT) ? 'center' : 'left';
var x = this.cx() - this.width() / 2;
var y = this.cy() - this.height() / 2;
var translate = 'translate(' + x + 'px, ' + y + 'px)';
var borderWidthOffset = this.borderWidth() * 2;
return {
backgroundColor: this.backgroundColor(),
border: this.borderWidth() + 'px solid ' + this.borderColor(),
borderRadius: '4px',
color: this.textColor(),
height: (this.height() - borderWidthOffset) + 'px',
lineHeight: (lineHeight - borderWidthOffset) + 'px',
msTransform: translate,
overflow: 'hidden',
position: 'absolute',
textAlign: textAlign,
textOverflow: 'ellipsis',
transform: translate,
visibility: this.content() ? 'visible' : 'hidden',
webkitTransform: translate,
whiteSpace: 'nowrap',
width: (this.width() - borderWidthOffset) + 'px'
};
}
redraw() {
var element = this.element();
var parentElement = this.parentElement();
if (!parentElement && !element)
return;
// add element
if (parentElement && !element) {
element = dom.el('<div>');
dom.html(element, '<svg><path></path><path></path></svg><div></div>');
this.element(element);
dom.append(parentElement, element);
this.redraw();
return;
}
// remove element
if (!parentElement && element) {
dom.remove(element);
this.element(null);
this.cache({});
return;
}
var cache = this.cache();
// update path container element
var pathContainerStyle = this.pathContainerStyle();
var pathContainerElement = dom.child(element, 0);
dom.css(pathContainerElement, helper.diffObj(pathContainerStyle, cache.pathContainerElementStyle));
cache.pathContainerElementStyle = contentStyle;
// update line element
var lineAttributes = this.lineAttributes();
var lineElement = dom.child(pathContainerElement, 0);
dom.attr(lineElement, helper.diffObj(lineAttributes, cache.lineAttributes));
cache.lineAttributes = lineAttributes;
// update arrow element
var arrowAttributes = this.arrowAttributes();
var arrowElement = dom.child(pathContainerElement, 1);
dom.attr(arrowElement, helper.diffObj(arrowAttributes, cache.arrowAttributes));
cache.arrowAttributes = arrowAttributes;
// update content element
var content = this.content();
var contentStyle = this.contentStyle();
var contentElement = dom.child(element, 1);
if (content !== cache.content) {
var contentType = this.contentType();
if (contentType === helper.CONTENT_TYPE_TEXT)
dom.text(contentElement, content);
else if (contentType === helper.CONTENT_TYPE_HTML)
dom.html(contentElement, content);
cache.content = content;
}
dom.css(contentElement, helper.diffObj(contentStyle, cache.contentStyle));
cache.contentStyle = contentStyle;
// update container element
var style = this.style();
dom.css(element, helper.diffObj(style, cache.style));
cache.style = style;
this.notifyRendered();
}
}
class Connector extends Component {
constructor(props) {
super();
this.x = this.prop(props.x, 0, helper.toNumber);
this.y = this.prop(props.y, 0, helper.toNumber);
this.color = this.prop(Connector.COLOR_UNCONNECTED);
this.zIndex = this.prop('auto');
this.element = this.prop(null);
this.parentElement = this.prop(null);
this.cache = this.prop({});
this.relations = this.prop([]);
}
r() {
return 16;
}
contains(x, y, tolerance) {
var dx = x - this.x();
var dy = y - this.y();
var r = this.r() + tolerance;
return (dx * dx + dy * dy <= r * r);
}
style() {
var r = this.r();
var x = this.x() - r;
var y = this.y() - r;
var translate = 'translate(' + x + 'px, ' + y + 'px)';
return {
backgroundColor: this.color(),
border: '2px solid lightgray',
borderRadius: '50%',
boxSizing: 'border-box',
height: r * 2 + 'px',
msTransform: translate,
opacity: 0.6,
pointerEvents: 'none',
position: 'absolute',
transform: translate,
webkitTransform: translate,
width: r * 2 + 'px',
zIndex: this.zIndex()
};
}
redraw() {
var element = this.element();
var parentElement = this.parentElement();
if (!parentElement && !element)
return;
// add element
if (parentElement && !element) {
element = dom.el('<div>');
this.element(element);
dom.append(parentElement, element);
this.redraw();
return;
}
// remove element
if (!parentElement && element) {
dom.remove(element);
this.element(null);
return;
}
var cache = this.cache();
// update element
var style = this.style();
dom.css(element, helper.diffObj(style, cache.style));
cache.style = style;
this.notifyRendered();
}
}
Connector.COLOR_CONNECTED = 'lightgreen';
Connector.COLOR_UNCONNECTED = 'pink';
export { Connector, Link as DrawingLink, Node as DrawingNode };