collab/mermaid/packages/mermaid-layout-elk/src/geometry.tsblame
View source
6dd74de1/* Geometry utilities extracted from render.ts for reuse and testing */
6dd74de2
6dd74de3export interface P {
6dd74de4 x: number;
6dd74de5 y: number;
6dd74de6}
6dd74de7
6dd74de8export interface RectLike {
6dd74de9 x: number; // center x
6dd74de10 y: number; // center y
6dd74de11 width: number;
6dd74de12 height: number;
6dd74de13 padding?: number;
6dd74de14}
6dd74de15
6dd74de16export interface NodeLike {
6dd74de17 intersect?: (p: P) => P | null;
6dd74de18}
6dd74de19
6dd74de20export const EPS = 1;
6dd74de21export const PUSH_OUT = 10;
6dd74de22
6dd74de23export const onBorder = (bounds: RectLike, p: P, tol = 0.5): boolean => {
6dd74de24 const halfW = bounds.width / 2;
6dd74de25 const halfH = bounds.height / 2;
6dd74de26 const left = bounds.x - halfW;
6dd74de27 const right = bounds.x + halfW;
6dd74de28 const top = bounds.y - halfH;
6dd74de29 const bottom = bounds.y + halfH;
6dd74de30
6dd74de31 const onLeft = Math.abs(p.x - left) <= tol && p.y >= top - tol && p.y <= bottom + tol;
6dd74de32 const onRight = Math.abs(p.x - right) <= tol && p.y >= top - tol && p.y <= bottom + tol;
6dd74de33 const onTop = Math.abs(p.y - top) <= tol && p.x >= left - tol && p.x <= right + tol;
6dd74de34 const onBottom = Math.abs(p.y - bottom) <= tol && p.x >= left - tol && p.x <= right + tol;
6dd74de35 return onLeft || onRight || onTop || onBottom;
6dd74de36};
6dd74de37
6dd74de38/**
6dd74de39 * Compute intersection between a rectangle (center x/y, width/height) and the line
6dd74de40 * segment from insidePoint -\> outsidePoint. Returns the point on the rectangle border.
6dd74de41 *
6dd74de42 * This version avoids snapping to outsidePoint when certain variables evaluate to 0
6dd74de43 * (previously caused vertical top/bottom cases to miss the border). It only enforces
6dd74de44 * axis-constant behavior for purely vertical/horizontal approaches.
6dd74de45 */
6dd74de46export const intersection = (node: RectLike, outsidePoint: P, insidePoint: P): P => {
6dd74de47 const x = node.x;
6dd74de48 const y = node.y;
6dd74de49
6dd74de50 const dx = Math.abs(x - insidePoint.x);
6dd74de51 const w = node.width / 2;
6dd74de52 let r = insidePoint.x < outsidePoint.x ? w - dx : w + dx;
6dd74de53 const h = node.height / 2;
6dd74de54
6dd74de55 const Q = Math.abs(outsidePoint.y - insidePoint.y);
6dd74de56 const R = Math.abs(outsidePoint.x - insidePoint.x);
6dd74de57
6dd74de58 if (Math.abs(y - outsidePoint.y) * w > Math.abs(x - outsidePoint.x) * h) {
6dd74de59 // Intersection is top or bottom of rect.
6dd74de60 const q = insidePoint.y < outsidePoint.y ? outsidePoint.y - h - y : y - h - outsidePoint.y;
6dd74de61 r = (R * q) / Q;
6dd74de62 const res = {
6dd74de63 x: insidePoint.x < outsidePoint.x ? insidePoint.x + r : insidePoint.x - R + r,
6dd74de64 y: insidePoint.y < outsidePoint.y ? insidePoint.y + Q - q : insidePoint.y - Q + q,
6dd74de65 };
6dd74de66
6dd74de67 // Keep axis-constant special-cases only
6dd74de68 if (R === 0) {
6dd74de69 res.x = outsidePoint.x;
6dd74de70 }
6dd74de71 if (Q === 0) {
6dd74de72 res.y = outsidePoint.y;
6dd74de73 }
6dd74de74 return res;
6dd74de75 } else {
6dd74de76 // Intersection on sides of rect
6dd74de77 if (insidePoint.x < outsidePoint.x) {
6dd74de78 r = outsidePoint.x - w - x;
6dd74de79 } else {
6dd74de80 r = x - w - outsidePoint.x;
6dd74de81 }
6dd74de82 const q = (Q * r) / R;
6dd74de83 let _x = insidePoint.x < outsidePoint.x ? insidePoint.x + R - r : insidePoint.x - R + r;
6dd74de84 let _y = insidePoint.y < outsidePoint.y ? insidePoint.y + q : insidePoint.y - q;
6dd74de85
6dd74de86 // Only handle axis-constant cases
6dd74de87 if (R === 0) {
6dd74de88 _x = outsidePoint.x;
6dd74de89 }
6dd74de90 if (Q === 0) {
6dd74de91 _y = outsidePoint.y;
6dd74de92 }
6dd74de93
6dd74de94 return { x: _x, y: _y };
6dd74de95 }
6dd74de96};
6dd74de97
6dd74de98export const outsideNode = (node: RectLike, point: P): boolean => {
6dd74de99 const x = node.x;
6dd74de100 const y = node.y;
6dd74de101 const dx = Math.abs(point.x - x);
6dd74de102 const dy = Math.abs(point.y - y);
6dd74de103 const w = node.width / 2;
6dd74de104 const h = node.height / 2;
6dd74de105 return dx >= w || dy >= h;
6dd74de106};
6dd74de107
6dd74de108export const ensureTrulyOutside = (bounds: RectLike, p: P, push = PUSH_OUT): P => {
6dd74de109 const dx = Math.abs(p.x - bounds.x);
6dd74de110 const dy = Math.abs(p.y - bounds.y);
6dd74de111 const w = bounds.width / 2;
6dd74de112 const h = bounds.height / 2;
6dd74de113 if (Math.abs(dx - w) < EPS || Math.abs(dy - h) < EPS) {
6dd74de114 const dirX = p.x - bounds.x;
6dd74de115 const dirY = p.y - bounds.y;
6dd74de116 const len = Math.sqrt(dirX * dirX + dirY * dirY);
6dd74de117 if (len > 0) {
6dd74de118 return {
6dd74de119 x: bounds.x + (dirX / len) * (len + push),
6dd74de120 y: bounds.y + (dirY / len) * (len + push),
6dd74de121 };
6dd74de122 }
6dd74de123 }
6dd74de124 return p;
6dd74de125};
6dd74de126
6dd74de127export const makeInsidePoint = (bounds: RectLike, outside: P, center: P): P => {
6dd74de128 const isVertical = Math.abs(outside.x - bounds.x) < EPS;
6dd74de129 const isHorizontal = Math.abs(outside.y - bounds.y) < EPS;
6dd74de130 return {
6dd74de131 x: isVertical
6dd74de132 ? outside.x
6dd74de133 : outside.x < bounds.x
6dd74de134 ? bounds.x - bounds.width / 4
6dd74de135 : bounds.x + bounds.width / 4,
6dd74de136 y: isHorizontal ? outside.y : center.y,
6dd74de137 };
6dd74de138};
6dd74de139
6dd74de140export const tryNodeIntersect = (node: NodeLike, bounds: RectLike, outside: P): P | null => {
6dd74de141 if (!node?.intersect) {
6dd74de142 return null;
6dd74de143 }
6dd74de144 const res = node.intersect(outside);
6dd74de145 if (!res) {
6dd74de146 return null;
6dd74de147 }
6dd74de148 const wrongSide =
6dd74de149 (outside.x < bounds.x && res.x > bounds.x) || (outside.x > bounds.x && res.x < bounds.x);
6dd74de150 if (wrongSide) {
6dd74de151 return null;
6dd74de152 }
6dd74de153 const dist = Math.hypot(outside.x - res.x, outside.y - res.y);
6dd74de154 if (dist <= EPS) {
6dd74de155 return null;
6dd74de156 }
6dd74de157 return res;
6dd74de158};
6dd74de159
6dd74de160export const fallbackIntersection = (bounds: RectLike, outside: P, center: P): P => {
6dd74de161 const inside = makeInsidePoint(bounds, outside, center);
6dd74de162 return intersection(bounds, outside, inside);
6dd74de163};
6dd74de164
6dd74de165export const computeNodeIntersection = (
6dd74de166 node: NodeLike,
6dd74de167 bounds: RectLike,
6dd74de168 outside: P,
6dd74de169 center: P
6dd74de170): P => {
6dd74de171 const outside2 = ensureTrulyOutside(bounds, outside);
6dd74de172 return tryNodeIntersect(node, bounds, outside2) ?? fallbackIntersection(bounds, outside2, center);
6dd74de173};
6dd74de174
6dd74de175export const replaceEndpoint = (
6dd74de176 points: P[],
6dd74de177 which: 'start' | 'end',
6dd74de178 value: P | null | undefined,
6dd74de179 tol = 0.1
6dd74de180) => {
6dd74de181 if (!value || points.length === 0) {
6dd74de182 return;
6dd74de183 }
6dd74de184
6dd74de185 if (which === 'start') {
6dd74de186 if (
6dd74de187 points.length > 0 &&
6dd74de188 Math.abs(points[0].x - value.x) < tol &&
6dd74de189 Math.abs(points[0].y - value.y) < tol
6dd74de190 ) {
6dd74de191 // duplicate start remove it
6dd74de192 points.shift();
6dd74de193 } else {
6dd74de194 points[0] = value;
6dd74de195 }
6dd74de196 } else {
6dd74de197 const last = points.length - 1;
6dd74de198 if (
6dd74de199 points.length > 0 &&
6dd74de200 Math.abs(points[last].x - value.x) < tol &&
6dd74de201 Math.abs(points[last].y - value.y) < tol
6dd74de202 ) {
6dd74de203 // duplicate end remove it
6dd74de204 points.pop();
6dd74de205 } else {
6dd74de206 points[last] = value;
6dd74de207 }
6dd74de208 }
6dd74de209};