@maxgraph/core
Version:
maxGraph is a fully client side JavaScript diagramming library that uses SVG and HTML for rendering.
681 lines (677 loc) • 24 kB
JavaScript
"use strict";
/*
Copyright 2021-present The maxGraph project Contributors
Copyright (c) 2006-2015, JGraph Ltd
Copyright (c) 2006-2015, Gaudenz Alder
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.relativeCcw = exports.ptLineDist = exports.ptSegDistSq = exports.intersection = exports.mod = exports.isInteger = exports.isNumeric = exports.intersectsHotspot = exports.intersects = exports.contains = exports.rectangleIntersectsSegment = exports.getPerimeterPoint = exports.getDirectedBounds = exports.findNearestSegment = exports.reversePortConstraints = exports.getPortConstraints = exports.getRotatedPoint = exports.getBoundingBox = exports.arcToCurves = exports.toDegree = exports.toRadians = void 0;
const Constants_js_1 = require("./Constants.js");
const Point_js_1 = __importDefault(require("../view/geometry/Point.js"));
const Rectangle_js_1 = __importDefault(require("../view/geometry/Rectangle.js"));
const utils_js_1 = require("../internal/utils.js");
/**
* Converts the given degree to radians.
*/
const toRadians = (deg) => {
return (Math.PI * deg) / 180;
};
exports.toRadians = toRadians;
/**
* Converts the given radians to degree.
*/
const toDegree = (rad) => {
return (rad * 180) / Math.PI;
};
exports.toDegree = toDegree;
/**
* Converts the given arc to a series of curves.
*/
const arcToCurves = (x0, y0, r1, r2, angle, largeArcFlag, sweepFlag, x, y) => {
x -= x0;
y -= y0;
if (r1 === 0 || r2 === 0) {
return [];
}
const fS = sweepFlag;
const psai = angle;
r1 = Math.abs(r1);
r2 = Math.abs(r2);
const ctx = -x / 2;
const cty = -y / 2;
const cpsi = Math.cos((psai * Math.PI) / 180);
const spsi = Math.sin((psai * Math.PI) / 180);
const rxd = cpsi * ctx + spsi * cty;
const ryd = -1 * spsi * ctx + cpsi * cty;
const rxdd = rxd * rxd;
const rydd = ryd * ryd;
const r1x = r1 * r1;
const r2y = r2 * r2;
const lamda = rxdd / r1x + rydd / r2y;
let sds;
if (lamda > 1) {
r1 = Math.sqrt(lamda) * r1;
r2 = Math.sqrt(lamda) * r2;
sds = 0;
}
else {
let seif = 1;
if (largeArcFlag === fS) {
seif = -1;
}
sds =
seif * Math.sqrt((r1x * r2y - r1x * rydd - r2y * rxdd) / (r1x * rydd + r2y * rxdd));
}
const txd = (sds * r1 * ryd) / r2;
const tyd = (-1 * sds * r2 * rxd) / r1;
const tx = cpsi * txd - spsi * tyd + x / 2;
const ty = spsi * txd + cpsi * tyd + y / 2;
let rad = Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1) - Math.atan2(0, 1);
let s1 = rad >= 0 ? rad : 2 * Math.PI + rad;
rad =
Math.atan2((-ryd - tyd) / r2, (-rxd - txd) / r1) -
Math.atan2((ryd - tyd) / r2, (rxd - txd) / r1);
let dr = rad >= 0 ? rad : 2 * Math.PI + rad;
if (!fS && dr > 0) {
dr -= 2 * Math.PI;
}
else if (fS && dr < 0) {
dr += 2 * Math.PI;
}
const sse = (dr * 2) / Math.PI;
const seg = Math.ceil(sse < 0 ? -1 * sse : sse);
const segr = dr / seg;
const t = ((8 / 3) * Math.sin(segr / 4) * Math.sin(segr / 4)) / Math.sin(segr / 2);
const cpsir1 = cpsi * r1;
const cpsir2 = cpsi * r2;
const spsir1 = spsi * r1;
const spsir2 = spsi * r2;
let mc = Math.cos(s1);
let ms = Math.sin(s1);
let x2 = -t * (cpsir1 * ms + spsir2 * mc);
let y2 = -t * (spsir1 * ms - cpsir2 * mc);
let x3 = 0;
let y3 = 0;
const result = [];
for (let n = 0; n < seg; ++n) {
s1 += segr;
mc = Math.cos(s1);
ms = Math.sin(s1);
x3 = cpsir1 * mc - spsir2 * ms + tx;
y3 = spsir1 * mc + cpsir2 * ms + ty;
const dx = -t * (cpsir1 * ms + spsir2 * mc);
const dy = -t * (spsir1 * ms - cpsir2 * mc);
// CurveTo updates x0, y0 so need to restore it
const index = n * 6;
result[index] = Number(x2 + x0);
result[index + 1] = Number(y2 + y0);
result[index + 2] = Number(x3 - dx + x0);
result[index + 3] = Number(y3 - dy + y0);
result[index + 4] = Number(x3 + x0);
result[index + 5] = Number(y3 + y0);
x2 = x3 + dx;
y2 = y3 + dy;
}
return result;
};
exports.arcToCurves = arcToCurves;
/**
* Returns the bounding box for the rotated rectangle.
*
* @param rect {@link Rectangle} to be rotated.
* @param angle Number that represents the angle (in degrees).
* @param cx Optional {@link Point} that represents the rotation center. If no
* rotation center is given then the center of rect is used.
*/
const getBoundingBox = (rect, rotation, cx = null) => {
let result = null;
if (rect && rotation !== 0) {
const rad = (0, exports.toRadians)(rotation);
const cos = Math.cos(rad);
const sin = Math.sin(rad);
cx = cx != null ? cx : new Point_js_1.default(rect.x + rect.width / 2, rect.y + rect.height / 2);
let p1 = new Point_js_1.default(rect.x, rect.y);
let p2 = new Point_js_1.default(rect.x + rect.width, rect.y);
let p3 = new Point_js_1.default(p2.x, rect.y + rect.height);
let p4 = new Point_js_1.default(rect.x, p3.y);
p1 = (0, exports.getRotatedPoint)(p1, cos, sin, cx);
p2 = (0, exports.getRotatedPoint)(p2, cos, sin, cx);
p3 = (0, exports.getRotatedPoint)(p3, cos, sin, cx);
p4 = (0, exports.getRotatedPoint)(p4, cos, sin, cx);
result = new Rectangle_js_1.default(p1.x, p1.y, 0, 0);
result.add(new Rectangle_js_1.default(p2.x, p2.y, 0, 0));
result.add(new Rectangle_js_1.default(p3.x, p3.y, 0, 0));
result.add(new Rectangle_js_1.default(p4.x, p4.y, 0, 0));
}
return result;
};
exports.getBoundingBox = getBoundingBox;
/**
* Rotates the given point by the given cos and sin.
*/
const getRotatedPoint = (pt, cos, sin, c = new Point_js_1.default()) => {
const x = pt.x - c.x;
const y = pt.y - c.y;
const x1 = x * cos - y * sin;
const y1 = y * cos + x * sin;
return new Point_js_1.default(x1 + c.x, y1 + c.y);
};
exports.getRotatedPoint = getRotatedPoint;
/**
* Returns an integer mask of the port constraints for the given terminal and edge.
*
* @param terminal {@link CellState} that represents the terminal.
* @param edge {@link CellState} that represents the edge.
* @param source Boolean that specifies if the terminal is the source terminal.
* @param defaultValue Default value to be returned if no port constraint is defined in the terminal.
* @return the mask of port constraint directions
*/
const getPortConstraints = (terminal, edge, source, defaultValue) => {
const value = terminal.style.portConstraint ??
(source ? edge.style.sourcePortConstraint : edge.style.targetPortConstraint);
if ((0, utils_js_1.isNullish)(value)) {
return defaultValue;
}
// The implementation here is derived from the mxGraph implementation
// In mxGraph, the configuration of several directions was done by concatenating them in a string without separator like in "portConstraint=northsouth"
// See https://github.com/jgraph/mxgraph/blob/v4.2.2/javascript/examples/orthogonal.html#L101-L102
// Other examples exist in draw.io: https://github.com/jgraph/drawio/blob/acd938b1/src/main/webapp/js/diagramly/Dialogs.js#L2302-L2305
// The implementation here still supports the string concatenation of several directions to allow import of mxGraph XML models without additional transformations.
const directions = value.toString();
let returnValue = Constants_js_1.DIRECTION_MASK.NONE;
const constraintRotationEnabled = terminal.style.portConstraintRotation ?? false;
const rotation = constraintRotationEnabled ? (terminal.style.rotation ?? 0) : 0;
let quad = 0;
if (rotation > 45) {
quad = 1;
if (rotation >= 135) {
quad = 2;
}
}
else if (rotation < -45) {
quad = 3;
if (rotation <= -135) {
quad = 2;
}
}
if (directions.indexOf('north') >= 0) {
switch (quad) {
case 0:
returnValue |= Constants_js_1.DIRECTION_MASK.NORTH;
break;
case 1:
returnValue |= Constants_js_1.DIRECTION_MASK.EAST;
break;
case 2:
returnValue |= Constants_js_1.DIRECTION_MASK.SOUTH;
break;
case 3:
returnValue |= Constants_js_1.DIRECTION_MASK.WEST;
break;
}
}
if (directions.indexOf('west') >= 0) {
switch (quad) {
case 0:
returnValue |= Constants_js_1.DIRECTION_MASK.WEST;
break;
case 1:
returnValue |= Constants_js_1.DIRECTION_MASK.NORTH;
break;
case 2:
returnValue |= Constants_js_1.DIRECTION_MASK.EAST;
break;
case 3:
returnValue |= Constants_js_1.DIRECTION_MASK.SOUTH;
break;
}
}
if (directions.indexOf('south') >= 0) {
switch (quad) {
case 0:
returnValue |= Constants_js_1.DIRECTION_MASK.SOUTH;
break;
case 1:
returnValue |= Constants_js_1.DIRECTION_MASK.WEST;
break;
case 2:
returnValue |= Constants_js_1.DIRECTION_MASK.NORTH;
break;
case 3:
returnValue |= Constants_js_1.DIRECTION_MASK.EAST;
break;
}
}
if (directions.indexOf('east') >= 0) {
switch (quad) {
case 0:
returnValue |= Constants_js_1.DIRECTION_MASK.EAST;
break;
case 1:
returnValue |= Constants_js_1.DIRECTION_MASK.SOUTH;
break;
case 2:
returnValue |= Constants_js_1.DIRECTION_MASK.WEST;
break;
case 3:
returnValue |= Constants_js_1.DIRECTION_MASK.NORTH;
break;
}
}
return returnValue;
};
exports.getPortConstraints = getPortConstraints;
/**
* Reverse the port constraint bitmask. For example, north | east
* becomes south | west
*/
const reversePortConstraints = (constraint) => {
let result = 0;
result = (constraint & Constants_js_1.DIRECTION_MASK.WEST) << 3;
result |= (constraint & Constants_js_1.DIRECTION_MASK.NORTH) << 1;
result |= (constraint & Constants_js_1.DIRECTION_MASK.SOUTH) >> 1;
result |= (constraint & Constants_js_1.DIRECTION_MASK.EAST) >> 3;
return result;
};
exports.reversePortConstraints = reversePortConstraints;
/**
* Finds the index of the nearest segment on the given cell state for
* the specified coordinate pair.
*/
const findNearestSegment = (state, x, y) => {
let index = -1;
if (state.absolutePoints.length > 0) {
let last = state.absolutePoints[0];
let min = null;
for (let i = 1; i < state.absolutePoints.length; i += 1) {
const current = state.absolutePoints[i];
if (!last || !current)
continue;
const dist = (0, exports.ptSegDistSq)(last.x, last.y, current.x, current.y, x, y);
if (min == null || dist < min) {
min = dist;
index = i - 1;
}
last = current;
}
}
return index;
};
exports.findNearestSegment = findNearestSegment;
/**
* Adds the given margins to the given rectangle and rotates and flips the
* rectangle according to the respective styles in style.
*/
const getDirectedBounds = (rect, m, style, flipH, flipV) => {
const d = style?.direction ?? 'east';
flipH ?? (flipH = style?.flipH ?? false);
flipV ?? (flipV = style?.flipV ?? false);
m.x = Math.round(Math.max(0, Math.min(rect.width, m.x)));
m.y = Math.round(Math.max(0, Math.min(rect.height, m.y)));
m.width = Math.round(Math.max(0, Math.min(rect.width, m.width)));
m.height = Math.round(Math.max(0, Math.min(rect.height, m.height)));
if ((flipV && (d === 'south' || d === 'north')) ||
(flipH && (d === 'east' || d === 'west'))) {
const tmp = m.x;
m.x = m.width;
m.width = tmp;
}
if ((flipH && (d === 'south' || d === 'north')) ||
(flipV && (d === 'east' || d === 'west'))) {
const tmp = m.y;
m.y = m.height;
m.height = tmp;
}
const m2 = Rectangle_js_1.default.fromRectangle(m);
if (d === 'south') {
m2.y = m.x;
m2.x = m.height;
m2.width = m.y;
m2.height = m.width;
}
else if (d === 'west') {
m2.y = m.height;
m2.x = m.width;
m2.width = m.x;
m2.height = m.y;
}
else if (d === 'north') {
m2.y = m.width;
m2.x = m.y;
m2.width = m.height;
m2.height = m.x;
}
return new Rectangle_js_1.default(rect.x + m2.x, rect.y + m2.y, rect.width - m2.width - m2.x, rect.height - m2.height - m2.y);
};
exports.getDirectedBounds = getDirectedBounds;
/**
* Returns the intersection between the polygon defined by the array of
* points and the line between center and point.
*/
const getPerimeterPoint = (pts, center, point) => {
let min = null;
for (let i = 0; i < pts.length - 1; i += 1) {
const pt = (0, exports.intersection)(pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y, center.x, center.y, point.x, point.y);
if (pt != null) {
const dx = point.x - pt.x;
const dy = point.y - pt.y;
const ip = { p: pt, distSq: dy * dy + dx * dx };
if (ip != null && (min == null || min.distSq > ip.distSq)) {
min = ip;
}
}
}
return min != null ? min.p : null;
};
exports.getPerimeterPoint = getPerimeterPoint;
/**
* Returns true if the given rectangle intersects the given segment.
*
* @param bounds {@link Rectangle} that represents the rectangle.
* @param p1 {@link Point} that represents the first point of the segment.
* @param p2 {@link Point} that represents the second point of the segment.
*/
const rectangleIntersectsSegment = (bounds, p1, p2) => {
const top = bounds.y;
const left = bounds.x;
const bottom = top + bounds.height;
const right = left + bounds.width;
// Find min and max X for the segment
let minX = p1.x;
let maxX = p2.x;
if (p1.x > p2.x) {
minX = p2.x;
maxX = p1.x;
}
// Find the intersection of the segment's and rectangle's x-projections
if (maxX > right) {
maxX = right;
}
if (minX < left) {
minX = left;
}
if (minX > maxX) {
// If their projections do not intersect return false
return false;
}
// Find corresponding min and max Y for min and max X we found before
let minY = p1.y;
let maxY = p2.y;
const dx = p2.x - p1.x;
if (Math.abs(dx) > 0.0000001) {
const a = (p2.y - p1.y) / dx;
const b = p1.y - a * p1.x;
minY = a * minX + b;
maxY = a * maxX + b;
}
if (minY > maxY) {
const tmp = maxY;
maxY = minY;
minY = tmp;
}
// Find the intersection of the segment's and rectangle's y-projections
if (maxY > bottom) {
maxY = bottom;
}
if (minY < top) {
minY = top;
}
if (minY > maxY) {
// If Y-projections do not intersect return false
return false;
}
return true;
};
exports.rectangleIntersectsSegment = rectangleIntersectsSegment;
/**
* Returns true if the specified point (x, y) is contained in the given rectangle.
*
* @param bounds {@link Rectangle} that represents the area.
* @param x X-coordinate of the point.
* @param y Y-coordinate of the point.
*/
const contains = (bounds, x, y) => {
return (bounds.x <= x &&
bounds.x + bounds.width >= x &&
bounds.y <= y &&
bounds.y + bounds.height >= y);
};
exports.contains = contains;
/**
* Returns true if the two rectangles intersect.
*
* @param a {@link Rectangle} to be checked for intersection.
* @param b {@link Rectangle} to be checked for intersection.
*/
const intersects = (a, b) => {
let tw = a.width;
let th = a.height;
let rw = b.width;
let rh = b.height;
if (rw <= 0 || rh <= 0 || tw <= 0 || th <= 0) {
return false;
}
const tx = a.x;
const ty = a.y;
const rx = b.x;
const ry = b.y;
rw += rx;
rh += ry;
tw += tx;
th += ty;
return ((rw < rx || rw > tx) &&
(rh < ry || rh > ty) &&
(tw < tx || tw > rx) &&
(th < ty || th > ry));
};
exports.intersects = intersects;
/**
* Returns true if the state and the hotspot intersect.
*
* @param state <CellState>
* @param x X-coordinate.
* @param y Y-coordinate.
* @param hotspot Optional size of the hostpot.
* @param min Optional min size of the hostpot.
* @param max Optional max size of the hostpot.
*/
const intersectsHotspot = (state, x, y, hotspot, min, max) => {
hotspot = hotspot != null ? hotspot : 1;
min = min != null ? min : 0;
max = max != null ? max : 0;
if (hotspot > 0) {
let cx = state.getCenterX();
let cy = state.getCenterY();
let w = state.width;
let h = state.height;
const style = state.style;
const start = (style?.startSize ?? 0) * state.view.scale;
if (start > 0) {
if (style?.horizontal ?? true) {
cy = state.y + start / 2;
h = start;
}
else {
cx = state.x + start / 2;
w = start;
}
}
w = Math.max(min, w * hotspot);
h = Math.max(min, h * hotspot);
if (max > 0) {
w = Math.min(w, max);
h = Math.min(h, max);
}
const rect = new Rectangle_js_1.default(cx - w / 2, cy - h / 2, w, h);
const alpha = (0, exports.toRadians)(style?.rotation ?? 0);
if (alpha != 0) {
const cos = Math.cos(-alpha);
const sin = Math.sin(-alpha);
const cx = new Point_js_1.default(state.getCenterX(), state.getCenterY());
const pt = (0, exports.getRotatedPoint)(new Point_js_1.default(x, y), cos, sin, cx);
x = pt.x;
y = pt.y;
}
return (0, exports.contains)(rect, x, y);
}
return true;
};
exports.intersectsHotspot = intersectsHotspot;
/**
* Returns true if the specified value is numeric, that is, if it is not
* null, not an empty string, not a HEX number and isNaN returns false.
*
* @param n String representing the possibly numeric value.
*/
const isNumeric = (n) => {
return (!Number.isNaN(parseFloat(n)) &&
isFinite(+n) &&
(typeof n !== 'string' || n.toLowerCase().indexOf('0x') < 0));
};
exports.isNumeric = isNumeric;
/**
* Returns true if the given value is an valid integer number.
*
* @param n String representing the possibly numeric value.
*/
const isInteger = (n) => {
return String(parseInt(n)) === String(n);
};
exports.isInteger = isInteger;
/**
* Returns the remainder of division of n by m. You should use this instead
* of the built-in operation as the built-in operation does not properly
* handle negative numbers.
*/
const mod = (n, m) => {
return ((n % m) + m) % m;
};
exports.mod = mod;
/**
* Returns the intersection of two lines as an {@link Point}.
*
* @param x0 X-coordinate of the first line's startpoint.
* @param y0 X-coordinate of the first line's startpoint.
* @param x1 X-coordinate of the first line's endpoint.
* @param y1 Y-coordinate of the first line's endpoint.
* @param x2 X-coordinate of the second line's startpoint.
* @param y2 Y-coordinate of the second line's startpoint.
* @param x3 X-coordinate of the second line's endpoint.
* @param y3 Y-coordinate of the second line's endpoint.
*/
const intersection = (x0, y0, x1, y1, x2, y2, x3, y3) => {
const denom = (y3 - y2) * (x1 - x0) - (x3 - x2) * (y1 - y0);
const nume_a = (x3 - x2) * (y0 - y2) - (y3 - y2) * (x0 - x2);
const nume_b = (x1 - x0) * (y0 - y2) - (y1 - y0) * (x0 - x2);
const ua = nume_a / denom;
const ub = nume_b / denom;
if (ua >= 0.0 && ua <= 1.0 && ub >= 0.0 && ub <= 1.0) {
// Get the intersection point
const x = x0 + ua * (x1 - x0);
const y = y0 + ua * (y1 - y0);
return new Point_js_1.default(x, y);
}
// No intersection
return null;
};
exports.intersection = intersection;
/**
* Returns the square distance between a segment and a point. To get the
* distance between a point and a line (with infinite length) use
* {@link Utils#ptLineDist}.
*
* @param x1 X-coordinate of the startpoint of the segment.
* @param y1 Y-coordinate of the startpoint of the segment.
* @param x2 X-coordinate of the endpoint of the segment.
* @param y2 Y-coordinate of the endpoint of the segment.
* @param px X-coordinate of the point.
* @param py Y-coordinate of the point.
*/
const ptSegDistSq = (x1, y1, x2, y2, px, py) => {
x2 -= x1;
y2 -= y1;
px -= x1;
py -= y1;
let dotprod = px * x2 + py * y2;
let projlenSq;
if (dotprod <= 0.0) {
projlenSq = 0.0;
}
else {
px = x2 - px;
py = y2 - py;
dotprod = px * x2 + py * y2;
if (dotprod <= 0.0) {
projlenSq = 0.0;
}
else {
projlenSq = (dotprod * dotprod) / (x2 * x2 + y2 * y2);
}
}
let lenSq = px * px + py * py - projlenSq;
if (lenSq < 0) {
lenSq = 0;
}
return lenSq;
};
exports.ptSegDistSq = ptSegDistSq;
/**
* Returns the distance between a line defined by two points and a point.
* To get the distance between a point and a segment (with a specific
* length) use {@link Utils#ptSeqDistSq}.
*
* @param x1 X-coordinate of point 1 of the line.
* @param y1 Y-coordinate of point 1 of the line.
* @param x2 X-coordinate of point 1 of the line.
* @param y2 Y-coordinate of point 1 of the line.
* @param px X-coordinate of the point.
* @param py Y-coordinate of the point.
*/
const ptLineDist = (x1, y1, x2, y2, px, py) => {
return (Math.abs((y2 - y1) * px - (x2 - x1) * py + x2 * y1 - y2 * x1) /
Math.sqrt((y2 - y1) * (y2 - y1) + (x2 - x1) * (x2 - x1)));
};
exports.ptLineDist = ptLineDist;
/**
* Returns 1 if the given point on the right side of the segment, 0 if its
* on the segment, and -1 if the point is on the left side of the segment.
*
* @param x1 X-coordinate of the startpoint of the segment.
* @param y1 Y-coordinate of the startpoint of the segment.
* @param x2 X-coordinate of the endpoint of the segment.
* @param y2 Y-coordinate of the endpoint of the segment.
* @param px X-coordinate of the point.
* @param py Y-coordinate of the point.
*/
const relativeCcw = (x1, y1, x2, y2, px, py) => {
x2 -= x1;
y2 -= y1;
px -= x1;
py -= y1;
let ccw = px * y2 - py * x2;
if (ccw == 0.0) {
ccw = px * x2 + py * y2;
if (ccw > 0.0) {
px -= x2;
py -= y2;
ccw = px * x2 + py * y2;
if (ccw < 0.0) {
ccw = 0.0;
}
}
}
return ccw < 0.0 ? -1 : ccw > 0.0 ? 1 : 0;
};
exports.relativeCcw = relativeCcw;