UNPKG

@deepnest/geometryutil

Version:

This tool is used within Applications provides by [www.deepnest.net](https://www.deepnest.net/).

1,232 lines 68.5 kB
/*! * General purpose geometry functions for polygon/Bezier calculations * Copyright 2015 Jack Qiao * Converted to TypeScript by Josef Fröhle, 2024 * Licensed under the MIT license */ 'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.GeometryUtil = void 0; // private shared variables/methods // floating point comparison tolerance const TOL = Math.pow(10, -9); // Floating point error is likely to be above 1 epsilon function _almostEqual(a, b, tolerance) { if (!tolerance) { tolerance = TOL; } return Math.abs(a - b) < tolerance; } // returns true if points are within the given distance function _withinDistance(p1, p2, distance) { const dx = p1.x - p2.x; const dy = p1.y - p2.y; return dx * dx + dy * dy < distance * distance; } function _degreesToRadians(angle) { return angle * (Math.PI / 180); } function _radiansToDegrees(angle) { return angle * (180 / Math.PI); } // normalize vector into a unit vector function _normalizeVector(v) { if (_almostEqual(v.x * v.x + v.y * v.y, 1)) { return v; // given vector was already a unit vector } const len = Math.sqrt(v.x * v.x + v.y * v.y); const inverse = 1 / len; return { x: v.x * inverse, y: v.y * inverse }; } // returns true if p lies on the line segment defined by AB, but not at any endpoints // may need work! function _onSegment(A, B, p, tolerance) { if (!tolerance) { tolerance = TOL; } // vertical line if (_almostEqual(A.x, B.x, tolerance) && _almostEqual(p.x, A.x, tolerance)) { if (!_almostEqual(p.y, B.y, tolerance) && !_almostEqual(p.y, A.y, tolerance) && p.y < Math.max(B.y, A.y, tolerance) && p.y > Math.min(B.y, A.y, tolerance)) { return true; } else { return false; } } // horizontal line if (_almostEqual(A.y, B.y, tolerance) && _almostEqual(p.y, A.y, tolerance)) { if (!_almostEqual(p.x, B.x, tolerance) && !_almostEqual(p.x, A.x, tolerance) && p.x < Math.max(B.x, A.x) && p.x > Math.min(B.x, A.x)) { return true; } else { return false; } } //range check if ((p.x < A.x && p.x < B.x) || (p.x > A.x && p.x > B.x) || (p.y < A.y && p.y < B.y) || (p.y > A.y && p.y > B.y)) { return false; } // exclude end points if ((_almostEqual(p.x, A.x, tolerance) && _almostEqual(p.y, A.y, tolerance)) || (_almostEqual(p.x, B.x, tolerance) && _almostEqual(p.y, B.y, tolerance))) { return false; } const cross = (p.y - A.y) * (B.x - A.x) - (p.x - A.x) * (B.y - A.y); if (Math.abs(cross) > tolerance) { return false; } const dot = (p.x - A.x) * (B.x - A.x) + (p.y - A.y) * (B.y - A.y); if (dot < 0 || _almostEqual(dot, 0, tolerance)) { return false; } const len2 = (B.x - A.x) * (B.x - A.x) + (B.y - A.y) * (B.y - A.y); if (dot > len2 || _almostEqual(dot, len2, tolerance)) { return false; } return true; } // returns the intersection of AB and EF // or null if there are no intersections or other numerical error // if the infinite flag is set, AE and EF describe infinite lines without endpoints, they are finite line segments otherwise function _lineIntersect(A, B, E, F, infinite = false) { const a1 = B.y - A.y; const b1 = A.x - B.x; const c1 = B.x * A.y - A.x * B.y; const a2 = F.y - E.y; const b2 = E.x - F.x; const c2 = F.x * E.y - E.x * F.y; const denom = a1 * b2 - a2 * b1; const x = (b1 * c2 - b2 * c1) / denom; const y = (a2 * c1 - a1 * c2) / denom; if (!isFinite(x) || !isFinite(y)) { return null; } // lines are colinear /*var crossABE = (E.y - A.y) * (B.x - A.x) - (E.x - A.x) * (B.y - A.y); var crossABF = (F.y - A.y) * (B.x - A.x) - (F.x - A.x) * (B.y - A.y); if(_almostEqual(crossABE,0) && _almostEqual(crossABF,0)){ return null; }*/ if (!infinite) { // coincident points do not count as intersecting if (Math.abs(A.x - B.x) > TOL && (A.x < B.x ? x < A.x || x > B.x : x > A.x || x < B.x)) return null; if (Math.abs(A.y - B.y) > TOL && (A.y < B.y ? y < A.y || y > B.y : y > A.y || y < B.y)) return null; if (Math.abs(E.x - F.x) > TOL && (E.x < F.x ? x < E.x || x > F.x : x > E.x || x < F.x)) return null; if (Math.abs(E.y - F.y) > TOL && (E.y < F.y ? y < E.y || y > F.y : y > E.y || y < F.y)) return null; } return { x: x, y: y }; } // public methods exports.GeometryUtil = { withinDistance: _withinDistance, lineIntersect: _lineIntersect, almostEqual: _almostEqual, almostEqualPoints: function (a, b, tolerance) { if (!tolerance) { tolerance = TOL; } const aa = a.x - b.x; const bb = a.y - b.y; if (aa * aa + bb * bb < tolerance * tolerance) { return true; } return false; }, // Bezier algos from http://algorithmist.net/docs/subdivision.pdf QuadraticBezier: { // Roger Willcocks bezier flatness criterion isFlat: function (p1, p2, c1, tol) { tol = 4 * tol * tol; let ux = 2 * c1.x - p1.x - p2.x; ux *= ux; let uy = 2 * c1.y - p1.y - p2.y; uy *= uy; return ux + uy <= tol; }, // turn Bezier into line segments via de Casteljau, returns an array of points linearize: function (p1, p2, c1, tol) { const finished = [p1]; // list of points to return const todo = [{ p1: p1, p2: p2, c1: c1 }]; // list of Beziers to divide // recursion could stack overflow, loop instead while (todo.length > 0) { const segment = todo[0]; if (this.isFlat(segment.p1, segment.p2, segment.c1, tol)) { // reached subdivision limit finished.push({ x: segment.p2.x, y: segment.p2.y }); todo.shift(); } else { const divided = this.subdivide(segment.p1, segment.p2, segment.c1, 0.5); todo.splice(0, 1, divided[0], divided[1]); } } return finished; }, // subdivide a single Bezier // t is the percent along the Bezier to divide at. eg. 0.5 subdivide: function (p1, p2, c1, t) { const mid1 = { x: p1.x + (c1.x - p1.x) * t, y: p1.y + (c1.y - p1.y) * t }; const mid2 = { x: c1.x + (p2.x - c1.x) * t, y: c1.y + (p2.y - c1.y) * t }; const mid3 = { x: mid1.x + (mid2.x - mid1.x) * t, y: mid1.y + (mid2.y - mid1.y) * t }; const seg1 = { p1: p1, p2: mid3, c1: mid1 }; const seg2 = { p1: mid3, p2: p2, c1: mid2 }; return [seg1, seg2]; } }, CubicBezier: { isFlat: function (p1, p2, c1, c2, tol) { tol = 16 * tol * tol; let ux = 3 * c1.x - 2 * p1.x - p2.x; ux *= ux; let uy = 3 * c1.y - 2 * p1.y - p2.y; uy *= uy; let vx = 3 * c2.x - 2 * p2.x - p1.x; vx *= vx; let vy = 3 * c2.y - 2 * p2.y - p1.y; vy *= vy; if (ux < vx) { ux = vx; } if (uy < vy) { uy = vy; } return ux + uy <= tol; }, linearize: function (p1, p2, c1, c2, tol) { const finished = [p1]; // list of points to return const todo = [{ p1: p1, p2: p2, c1: c1, c2: c2 }]; // list of Beziers to divide // recursion could stack overflow, loop instead while (todo.length > 0) { const segment = todo[0]; if (this.isFlat(segment.p1, segment.p2, segment.c1, segment.c2, tol)) { // reached subdivision limit finished.push({ x: segment.p2.x, y: segment.p2.y }); todo.shift(); } else { const divided = this.subdivide(segment.p1, segment.p2, segment.c1, segment.c2, 0.5); todo.splice(0, 1, divided[0], divided[1]); } } return finished; }, subdivide: function (p1, p2, c1, c2, t) { const mid1 = { x: p1.x + (c1.x - p1.x) * t, y: p1.y + (c1.y - p1.y) * t }; const mid2 = { x: c2.x + (p2.x - c2.x) * t, y: c2.y + (p2.y - c2.y) * t }; const mid3 = { x: c1.x + (c2.x - c1.x) * t, y: c1.y + (c2.y - c1.y) * t }; const mida = { x: mid1.x + (mid3.x - mid1.x) * t, y: mid1.y + (mid3.y - mid1.y) * t }; const midb = { x: mid3.x + (mid2.x - mid3.x) * t, y: mid3.y + (mid2.y - mid3.y) * t }; const midx = { x: mida.x + (midb.x - mida.x) * t, y: mida.y + (midb.y - mida.y) * t }; const seg1 = { p1: p1, p2: midx, c1: mid1, c2: mida }; const seg2 = { p1: midx, p2: p2, c1: midb, c2: mid2 }; return [seg1, seg2]; } }, Arc: { linearize: function (p1, p2, rx, ry, angle, largearc, sweep, tol) { const finished = [p2]; // list of points to return let arc = this.svgToCenter(p1, p2, rx, ry, angle, largearc, sweep); const todo = [arc]; // list of arcs to divide // recursion could stack overflow, loop instead while (todo.length > 0) { arc = todo[0]; const fullarc = this.centerToSvg(arc.center, arc.rx, arc.ry, arc.theta, arc.extent, arc.angle); const subarc = this.centerToSvg(arc.center, arc.rx, arc.ry, arc.theta, 0.5 * arc.extent, arc.angle); const arcmid = subarc.p2; const mid = { x: 0.5 * (fullarc.p1.x + fullarc.p2.x), y: 0.5 * (fullarc.p1.y + fullarc.p2.y) }; // compare midpoint of line with midpoint of arc // this is not 100% accurate, but should be a good heuristic for flatness in most cases if (_withinDistance(mid, arcmid, tol)) { finished.unshift(fullarc.p2); todo.shift(); } else { const arc1 = { center: arc.center, rx: arc.rx, ry: arc.ry, theta: arc.theta, extent: 0.5 * arc.extent, angle: arc.angle }; const arc2 = { center: arc.center, rx: arc.rx, ry: arc.ry, theta: arc.theta + 0.5 * arc.extent, extent: 0.5 * arc.extent, angle: arc.angle }; todo.splice(0, 1, arc1, arc2); } } return finished; }, // convert from center point/angle sweep definition to SVG point and flag definition of arcs // ported from http://commons.oreilly.com/wiki/index.php/SVG_Essentials/Paths centerToSvg: function (center, rx, ry, theta1, extent, angleDegrees) { let theta2 = theta1 + extent; theta1 = _degreesToRadians(theta1); theta2 = _degreesToRadians(theta2); const angle = _degreesToRadians(angleDegrees); const cos = Math.cos(angle); const sin = Math.sin(angle); const t1cos = Math.cos(theta1); const t1sin = Math.sin(theta1); const t2cos = Math.cos(theta2); const t2sin = Math.sin(theta2); const x0 = center.x + cos * rx * t1cos + -sin * ry * t1sin; const y0 = center.y + sin * rx * t1cos + cos * ry * t1sin; const x1 = center.x + cos * rx * t2cos + -sin * ry * t2sin; const y1 = center.y + sin * rx * t2cos + cos * ry * t2sin; const largearc = extent > 180 ? 1 : 0; const sweep = extent > 0 ? 1 : 0; return { p1: { x: x0, y: y0 }, p2: { x: x1, y: y1 }, rx: rx, ry: ry, angle: angle, largearc: largearc, sweep: sweep }; }, // convert from SVG format arc to center point arc svgToCenter: function (p1, p2, rx, ry, angleDegrees, largearc, sweep) { const mid = { x: 0.5 * (p1.x + p2.x), y: 0.5 * (p1.y + p2.y) }; const diff = { x: 0.5 * (p2.x - p1.x), y: 0.5 * (p2.y - p1.y) }; const angle = _degreesToRadians(angleDegrees % 360); const cos = Math.cos(angle); const sin = Math.sin(angle); const x1 = cos * diff.x + sin * diff.y; const y1 = -sin * diff.x + cos * diff.y; rx = Math.abs(rx); ry = Math.abs(ry); let Prx = rx * rx; let Pry = ry * ry; const Px1 = x1 * x1; const Py1 = y1 * y1; const radiiCheck = Px1 / Prx + Py1 / Pry; const radiiSqrt = Math.sqrt(radiiCheck); if (radiiCheck > 1) { rx = radiiSqrt * rx; ry = radiiSqrt * ry; Prx = rx * rx; Pry = ry * ry; } let sign = largearc != sweep ? -1 : 1; let sq = (Prx * Pry - Prx * Py1 - Pry * Px1) / (Prx * Py1 + Pry * Px1); sq = sq < 0 ? 0 : sq; const coef = sign * Math.sqrt(sq); const cx1 = coef * ((rx * y1) / ry); const cy1 = coef * -((ry * x1) / rx); const cx = mid.x + (cos * cx1 - sin * cy1); const cy = mid.y + (sin * cx1 + cos * cy1); const ux = (x1 - cx1) / rx; const uy = (y1 - cy1) / ry; const vx = (-x1 - cx1) / rx; const vy = (-y1 - cy1) / ry; let n = Math.sqrt(ux * ux + uy * uy); let p = ux; sign = uy < 0 ? -1 : 1; let theta = sign * Math.acos(p / n); theta = _radiansToDegrees(theta); n = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); p = ux * vx + uy * vy; sign = ux * vy - uy * vx < 0 ? -1 : 1; let delta = sign * Math.acos(p / n); delta = _radiansToDegrees(delta); if (sweep == 1 && delta > 0) { delta -= 360; } else if (sweep == 0 && delta < 0) { delta += 360; } delta %= 360; theta %= 360; return { center: { x: cx, y: cy }, rx: rx, ry: ry, theta: theta, extent: delta, angle: angleDegrees }; } }, // returns the rectangular bounding box of the given polygon getPolygonBounds: function (polygon) { if (!polygon || polygon.length < 3) { return null; } let xmin = polygon[0].x; let xmax = polygon[0].x; let ymin = polygon[0].y; let ymax = polygon[0].y; for (let i = 1; i < polygon.length; i++) { if (polygon[i].x > xmax) { xmax = polygon[i].x; } else if (polygon[i].x < xmin) { xmin = polygon[i].x; } if (polygon[i].y > ymax) { ymax = polygon[i].y; } else if (polygon[i].y < ymin) { ymin = polygon[i].y; } } return { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin }; }, // return true if point is in the polygon, false if outside, and null if exactly on a point or edge pointInPolygon: function (point, polygon, tolerance) { if (!polygon || polygon.length < 3) { return null; } if (!tolerance) { tolerance = TOL; } let inside = false; const offsetx = polygon.offsetx || 0; const offsety = polygon.offsety || 0; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x + offsetx; const yi = polygon[i].y + offsety; const xj = polygon[j].x + offsetx; const yj = polygon[j].y + offsety; if (_almostEqual(xi, point.x, tolerance) && _almostEqual(yi, point.y, tolerance)) { return null; // no result } if (_onSegment({ x: xi, y: yi }, { x: xj, y: yj }, point, tolerance)) { return null; // exactly on the segment } if (_almostEqual(xi, xj, tolerance) && _almostEqual(yi, yj, tolerance)) { // ignore very small lines continue; } const intersect = yi > point.y != yj > point.y && point.x < ((xj - xi) * (point.y - yi)) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; }, // returns the area of the polygon, assuming no self-intersections // a negative area indicates counter-clockwise winding direction polygonArea: function (polygon) { let area = 0; let i, j; for (i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { area += (polygon[j].x + polygon[i].x) * (polygon[j].y - polygon[i].y); } return 0.5 * area; }, // TODO: swap this for a more efficient sweep-line implementation // returns true if the two polygons intersect // returnEdges: never implemented in the original code // returnEdges: if set, return all edges on A that have intersections intersect: function (A, B) { const Aoffsetx = A.offsetx || 0; const Aoffsety = A.offsety || 0; const Boffsetx = B.offsetx || 0; const Boffsety = B.offsety || 0; A = A.slice(0); B = B.slice(0); for (let i = 0; i < A.length - 1; i++) { for (let j = 0; j < B.length - 1; j++) { const a1 = { x: A[i].x + Aoffsetx, y: A[i].y + Aoffsety }; const a2 = { x: A[i + 1].x + Aoffsetx, y: A[i + 1].y + Aoffsety }; const b1 = { x: B[j].x + Boffsetx, y: B[j].y + Boffsety }; const b2 = { x: B[j + 1].x + Boffsetx, y: B[j + 1].y + Boffsety }; let prevbindex = j == 0 ? B.length - 1 : j - 1; let prevaindex = i == 0 ? A.length - 1 : i - 1; let nextbindex = j + 1 == B.length - 1 ? 0 : j + 2; let nextaindex = i + 1 == A.length - 1 ? 0 : i + 2; // go even further back if we happen to hit on a loop end point if (B[prevbindex] == B[j] || (_almostEqual(B[prevbindex].x, B[j].x) && _almostEqual(B[prevbindex].y, B[j].y))) { prevbindex = prevbindex == 0 ? B.length - 1 : prevbindex - 1; } if (A[prevaindex] == A[i] || (_almostEqual(A[prevaindex].x, A[i].x) && _almostEqual(A[prevaindex].y, A[i].y))) { prevaindex = prevaindex == 0 ? A.length - 1 : prevaindex - 1; } // go even further forward if we happen to hit on a loop end point if (B[nextbindex] == B[j + 1] || (_almostEqual(B[nextbindex].x, B[j + 1].x) && _almostEqual(B[nextbindex].y, B[j + 1].y))) { nextbindex = nextbindex == B.length - 1 ? 0 : nextbindex + 1; } if (A[nextaindex] == A[i + 1] || (_almostEqual(A[nextaindex].x, A[i + 1].x) && _almostEqual(A[nextaindex].y, A[i + 1].y))) { nextaindex = nextaindex == A.length - 1 ? 0 : nextaindex + 1; } const a0 = { x: A[prevaindex].x + Aoffsetx, y: A[prevaindex].y + Aoffsety }; const b0 = { x: B[prevbindex].x + Boffsetx, y: B[prevbindex].y + Boffsety }; const a3 = { x: A[nextaindex].x + Aoffsetx, y: A[nextaindex].y + Aoffsety }; const b3 = { x: B[nextbindex].x + Boffsetx, y: B[nextbindex].y + Boffsety }; if (_onSegment(a1, a2, b1) || (_almostEqual(a1.x, b1.x) && _almostEqual(a1.y, b1.y))) { // if a point is on a segment, it could intersect or it could not. Check via the neighboring points const b0in = this.pointInPolygon(b0, A); const b2in = this.pointInPolygon(b2, A); if ((b0in === true && b2in === false) || (b0in === false && b2in === true)) { return true; } else { continue; } } if (_onSegment(a1, a2, b2) || (_almostEqual(a2.x, b2.x) && _almostEqual(a2.y, b2.y))) { // if a point is on a segment, it could intersect or it could not. Check via the neighboring points const b1in = this.pointInPolygon(b1, A); const b3in = this.pointInPolygon(b3, A); if ((b1in === true && b3in === false) || (b1in === false && b3in === true)) { return true; } else { continue; } } if (_onSegment(b1, b2, a1) || (_almostEqual(a1.x, b2.x) && _almostEqual(a1.y, b2.y))) { // if a point is on a segment, it could intersect or it could not. Check via the neighboring points const a0in = this.pointInPolygon(a0, B); const a2in = this.pointInPolygon(a2, B); if ((a0in === true && a2in === false) || (a0in === false && a2in === true)) { return true; } else { continue; } } if (_onSegment(b1, b2, a2) || (_almostEqual(a2.x, b1.x) && _almostEqual(a2.y, b1.y))) { // if a point is on a segment, it could intersect or it could not. Check via the neighboring points const a1in = this.pointInPolygon(a1, B); const a3in = this.pointInPolygon(a3, B); if ((a1in === true && a3in === false) || (a1in === false && a3in === true)) { return true; } else { continue; } } const p = _lineIntersect(b1, b2, a1, a2); if (p !== null) { return true; } } } return false; }, // placement algos as outlined in [1] http://www.cs.stir.ac.uk/~goc/papers/EffectiveHueristic2DAOR2013.pdf // returns a continuous polyline representing the normal-most edge of the given polygon // eg. a normal vector of [-1, 0] will return the left-most edge of the polygon // this is essentially algo 8 in [1], generalized for any vector direction polygonEdge: function (polygon, normal) { if (!polygon || polygon.length < 3) { return null; } normal = _normalizeVector(normal); const direction = { x: -normal.y, y: normal.x }; // find the max and min points, they will be the endpoints of our edge let min = null; let max = null; const dotproduct = []; for (let i = 0; i < polygon.length; i++) { const dot = polygon[i].x * direction.x + polygon[i].y * direction.y; dotproduct.push(dot); if (min === null || dot < min) { min = dot; } if (max === null || dot > max) { max = dot; } } if (min === null || max === null) { throw new Error('Invalid polygon: no min or max found'); } // there may be multiple vertices with min/max values. In which case we choose the one that is normal-most (eg. left most) let indexmin = 0; let indexmax = 0; let normalmin = null; let normalmax = null; for (let i = 0; i < polygon.length; i++) { if (_almostEqual(dotproduct[i], min)) { const dot = polygon[i].x * normal.x + polygon[i].y * normal.y; if (normalmin === null || dot > normalmin) { normalmin = dot; indexmin = i; } } else if (_almostEqual(dotproduct[i], max)) { const dot = polygon[i].x * normal.x + polygon[i].y * normal.y; if (normalmax === null || dot > normalmax) { normalmax = dot; indexmax = i; } } } // now we have two edges bound by min and max points, figure out which edge faces our direction vector let indexleft = indexmin - 1; let indexright = indexmin + 1; if (indexleft < 0) { indexleft = polygon.length - 1; } if (indexright >= polygon.length) { indexright = 0; } const minvertex = polygon[indexmin]; const left = polygon[indexleft]; const right = polygon[indexright]; const leftvector = { x: left.x - minvertex.x, y: left.y - minvertex.y }; const rightvector = { x: right.x - minvertex.x, y: right.y - minvertex.y }; const dotleft = leftvector.x * direction.x + leftvector.y * direction.y; const dotright = rightvector.x * direction.x + rightvector.y * direction.y; // -1 = left, 1 = right let scandirection = -1; if (_almostEqual(dotleft, 0)) { scandirection = 1; } else if (_almostEqual(dotright, 0)) { scandirection = -1; } else { let normaldotleft; let normaldotright; if (_almostEqual(dotleft, dotright)) { // the points line up exactly along the normal vector normaldotleft = leftvector.x * normal.x + leftvector.y * normal.y; normaldotright = rightvector.x * normal.x + rightvector.y * normal.y; } else if (dotleft < dotright) { // normalize right vertex so normal projection can be directly compared normaldotleft = leftvector.x * normal.x + leftvector.y * normal.y; normaldotright = (rightvector.x * normal.x + rightvector.y * normal.y) * (dotleft / dotright); } else { // normalize left vertex so normal projection can be directly compared normaldotleft = leftvector.x * normal.x + leftvector.y * normal.y * (dotright / dotleft); normaldotright = rightvector.x * normal.x + rightvector.y * normal.y; } if (normaldotleft > normaldotright) { scandirection = -1; } else { // technically they could be equal, (ie. the segments bound by left and right points are incident) // in which case we'll have to climb up the chain until lines are no longer incident // for now we'll just not handle it and assume people aren't giving us garbage input.. scandirection = 1; } } // connect all points between indexmin and indexmax along the scan direction const edge = []; let count = 0; let i = indexmin; while (count < polygon.length) { if (i >= polygon.length) { i = 0; } else if (i < 0) { i = polygon.length - 1; } edge.push(polygon[i]); if (i == indexmax) { break; } i += scandirection; count++; } return edge; }, // returns the normal distance from p to a line segment defined by s1 s2 // this is basically algo 9 in [1], generalized for any vector direction // eg. normal of [-1, 0] returns the horizontal distance between the point and the line segment // sxinclusive: if true, include endpoints instead of excluding them pointLineDistance: function (p, s1, s2, normal, s1inclusive, s2inclusive) { normal = _normalizeVector(normal); const dir = { x: normal.y, y: -normal.x }; const pdot = p.x * dir.x + p.y * dir.y; const s1dot = s1.x * dir.x + s1.y * dir.y; const s2dot = s2.x * dir.x + s2.y * dir.y; const pdotnorm = p.x * normal.x + p.y * normal.y; const s1dotnorm = s1.x * normal.x + s1.y * normal.y; const s2dotnorm = s2.x * normal.x + s2.y * normal.y; // point is exactly along the edge in the normal direction if (_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot)) { // point lies on an endpoint if (_almostEqual(pdotnorm, s1dotnorm)) { return null; } if (_almostEqual(pdotnorm, s2dotnorm)) { return null; } // point is outside both endpoints if (pdotnorm > s1dotnorm && pdotnorm > s2dotnorm) { return Math.min(pdotnorm - s1dotnorm, pdotnorm - s2dotnorm); } if (pdotnorm < s1dotnorm && pdotnorm < s2dotnorm) { return -Math.min(s1dotnorm - pdotnorm, s2dotnorm - pdotnorm); } // point lies between endpoints const diff1 = pdotnorm - s1dotnorm; const diff2 = pdotnorm - s2dotnorm; if (diff1 > 0) { return diff1; } else { return diff2; } } // point else if (_almostEqual(pdot, s1dot)) { if (s1inclusive) { return pdotnorm - s1dotnorm; } else { return null; } } else if (_almostEqual(pdot, s2dot)) { if (s2inclusive) { return pdotnorm - s2dotnorm; } else { return null; } } else if ((pdot < s1dot && pdot < s2dot) || (pdot > s1dot && pdot > s2dot)) { return null; // point doesn't collide with segment } return pdotnorm - s1dotnorm + ((s1dotnorm - s2dotnorm) * (s1dot - pdot)) / (s1dot - s2dot); }, pointDistance: function (p, s1, s2, normal, infinite) { normal = _normalizeVector(normal); const dir = { x: normal.y, y: -normal.x }; const pdot = p.x * dir.x + p.y * dir.y; const s1dot = s1.x * dir.x + s1.y * dir.y; const s2dot = s2.x * dir.x + s2.y * dir.y; const pdotnorm = p.x * normal.x + p.y * normal.y; const s1dotnorm = s1.x * normal.x + s1.y * normal.y; const s2dotnorm = s2.x * normal.x + s2.y * normal.y; if (!infinite) { if (((pdot < s1dot || _almostEqual(pdot, s1dot)) && (pdot < s2dot || _almostEqual(pdot, s2dot))) || ((pdot > s1dot || _almostEqual(pdot, s1dot)) && (pdot > s2dot || _almostEqual(pdot, s2dot)))) { return null; // dot doesn't collide with segment, or lies directly on the vertex } if (_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot) && pdotnorm > s1dotnorm && pdotnorm > s2dotnorm) { return Math.min(pdotnorm - s1dotnorm, pdotnorm - s2dotnorm); } if (_almostEqual(pdot, s1dot) && _almostEqual(pdot, s2dot) && pdotnorm < s1dotnorm && pdotnorm < s2dotnorm) { return -Math.min(s1dotnorm - pdotnorm, s2dotnorm - pdotnorm); } } return -(pdotnorm - s1dotnorm + ((s1dotnorm - s2dotnorm) * (s1dot - pdot)) / (s1dot - s2dot)); }, segmentDistance: function (A, B, E, F, direction) { const normal = { x: direction.y, y: -direction.x }; const reverse = { x: -direction.x, y: -direction.y }; const dotA = A.x * normal.x + A.y * normal.y; const dotB = B.x * normal.x + B.y * normal.y; const dotE = E.x * normal.x + E.y * normal.y; const dotF = F.x * normal.x + F.y * normal.y; const crossA = A.x * direction.x + A.y * direction.y; const crossB = B.x * direction.x + B.y * direction.y; const crossE = E.x * direction.x + E.y * direction.y; const crossF = F.x * direction.x + F.y * direction.y; // TODO: is never used - remove? /* const crossABmin = Math.min(crossA, crossB) const crossABmax = Math.max(crossA, crossB) const crossEFmax = Math.max(crossE, crossF) const crossEFmin = Math.min(crossE, crossF) */ const ABmin = Math.min(dotA, dotB); const ABmax = Math.max(dotA, dotB); const EFmax = Math.max(dotE, dotF); const EFmin = Math.min(dotE, dotF); // segments that will merely touch at one point if (_almostEqual(ABmax, EFmin, TOL) || _almostEqual(ABmin, EFmax, TOL)) { return null; } // segments miss eachother completely if (ABmax < EFmin || ABmin > EFmax) { return null; } let overlap; if ((ABmax > EFmax && ABmin < EFmin) || (EFmax > ABmax && EFmin < ABmin)) { overlap = 1; } else { const minMax = Math.min(ABmax, EFmax); const maxMin = Math.max(ABmin, EFmin); const maxMax = Math.max(ABmax, EFmax); const minMin = Math.min(ABmin, EFmin); overlap = (minMax - maxMin) / (maxMax - minMin); } const crossABE = (E.y - A.y) * (B.x - A.x) - (E.x - A.x) * (B.y - A.y); const crossABF = (F.y - A.y) * (B.x - A.x) - (F.x - A.x) * (B.y - A.y); // lines are colinear if (_almostEqual(crossABE, 0) && _almostEqual(crossABF, 0)) { const ABnorm = { x: B.y - A.y, y: A.x - B.x }; const EFnorm = { x: F.y - E.y, y: E.x - F.x }; const ABnormlength = Math.sqrt(ABnorm.x * ABnorm.x + ABnorm.y * ABnorm.y); ABnorm.x /= ABnormlength; ABnorm.y /= ABnormlength; const EFnormlength = Math.sqrt(EFnorm.x * EFnorm.x + EFnorm.y * EFnorm.y); EFnorm.x /= EFnormlength; EFnorm.y /= EFnormlength; // segment normals must point in opposite directions if (Math.abs(ABnorm.y * EFnorm.x - ABnorm.x * EFnorm.y) < TOL && ABnorm.y * EFnorm.y + ABnorm.x * EFnorm.x < 0) { // normal of AB segment must point in same direction as given direction vector const normdot = ABnorm.y * direction.y + ABnorm.x * direction.x; // the segments merely slide along eachother if (_almostEqual(normdot, 0, TOL)) { return null; } if (normdot < 0) { return 0; } } return null; } const distances = []; // coincident points if (_almostEqual(dotA, dotE)) { distances.push(crossA - crossE); } else if (_almostEqual(dotA, dotF)) { distances.push(crossA - crossF); } else if (dotA > EFmin && dotA < EFmax) { let d = this.pointDistance(A, E, F, reverse); if (d !== null && _almostEqual(d, 0)) { // A currently touches EF, but AB is moving away from EF const dB = this.pointDistance(B, E, F, reverse, true); if (dB !== null && (dB < 0 || _almostEqual(dB * overlap, 0))) { d = null; } } if (d !== null) { distances.push(d); } } if (_almostEqual(dotB, dotE)) { distances.push(crossB - crossE); } else if (_almostEqual(dotB, dotF)) { distances.push(crossB - crossF); } else if (dotB > EFmin && dotB < EFmax) { let d = this.pointDistance(B, E, F, reverse); if (d !== null && _almostEqual(d, 0)) { // crossA>crossB A currently touches EF, but AB is moving away from EF const dA = this.pointDistance(A, E, F, reverse, true); if (dA !== null && (dA < 0 || _almostEqual(dA * overlap, 0))) { d = null; } } if (d !== null) { distances.push(d); } } if (dotE > ABmin && dotE < ABmax) { let d = this.pointDistance(E, A, B, direction); if (d !== null && _almostEqual(d, 0)) { // crossF<crossE A currently touches EF, but AB is moving away from EF const dF = this.pointDistance(F, A, B, direction, true); if (dF !== null && (dF < 0 || _almostEqual(dF * overlap, 0))) { d = null; } } if (d !== null) { distances.push(d); } } if (dotF > ABmin && dotF < ABmax) { let d = this.pointDistance(F, A, B, direction); if (d !== null && _almostEqual(d, 0)) { // && crossE<crossF A currently touches EF, but AB is moving away from EF const dE = this.pointDistance(E, A, B, direction, true); if (dE !== null && (dE < 0 || _almostEqual(dE * overlap, 0))) { d = null; } } if (d !== null) { distances.push(d); } } if (distances.length == 0) { return null; } return Math.min(...distances); }, polygonSlideDistance: function (A, B, direction, ignoreNegative) { let A1, A2, B1, B2; const Aoffsetx = A.offsetx || 0; const Aoffsety = A.offsety || 0; const Boffsetx = B.offsetx || 0; const Boffsety = B.offsety || 0; A = A.slice(0); B = B.slice(0); // close the loop for polygons if (A[0] != A[A.length - 1]) { A.push(A[0]); } if (B[0] != B[B.length - 1]) { B.push(B[0]); } const edgeA = A; const edgeB = B; let distance = null; //let p, s1, s2, d let d; const dir = _normalizeVector(direction); // TODO: mind is never used - remove? // eslint-disable-next-line @typescript-eslint/no-unused-vars const normal = { x: dir.y, y: -dir.x }; // TODO: mind is never used - remove? // eslint-disable-next-line @typescript-eslint/no-unused-vars const reverse = { x: -dir.x, y: -dir.y }; for (let i = 0; i < edgeB.length - 1; i++) { // TODO: mind is never used - remove? // eslint-disable-next-line @typescript-eslint/no-unused-vars const mind = null; for (let j = 0; j < edgeA.length - 1; j++) { A1 = { x: edgeA[j].x + Aoffsetx, y: edgeA[j].y + Aoffsety }; A2 = { x: edgeA[j + 1].x + Aoffsetx, y: edgeA[j + 1].y + Aoffsety }; B1 = { x: edgeB[i].x + Boffsetx, y: edgeB[i].y + Boffsety }; B2 = { x: edgeB[i + 1].x + Boffsetx, y: edgeB[i + 1].y + Boffsety }; if ((_almostEqual(A1.x, A2.x) && _almostEqual(A1.y, A2.y)) || (_almostEqual(B1.x, B2.x) && _almostEqual(B1.y, B2.y))) { continue; // ignore extremely small lines } d = this.segmentDistance(A1, A2, B1, B2, dir); if (d !== null && (distance === null || d < distance)) { if (!ignoreNegative || d > 0 || _almostEqual(d, 0)) { distance = d; } } } } return distance; }, // project each point of B onto A in the given direction, and return the polygonProjectionDistance: function (A, B, direction) { const Boffsetx = B.offsetx || 0; const Boffsety = B.offsety || 0; const Aoffsetx = A.offsetx || 0; const Aoffsety = A.offsety || 0; A = A.slice(0); B = B.slice(0); // close the loop for polygons if (A[0] != A[A.length - 1]) { A.push(A[0]); } if (B[0] != B[B.length - 1]) { B.push(B[0]); } const edgeA = A; const edgeB = B; let distance = null; let p, d, s1, s2; for (let i = 0; i < edgeB.length; i++) { // the shortest/most negative projection of B onto A let minprojection = null; for (let j = 0; j < edgeA.length - 1; j++) { p = { x: edgeB[i].x + Boffsetx, y: edgeB[i].y + Boffsety }; s1 = { x: edgeA[j].x + Aoffsetx, y: edgeA[j].y + Aoffsety }; s2 = { x: edgeA[j + 1].x + Aoffsetx, y: edgeA[j + 1].y + Aoffsety }; if (Math.abs((s2.y - s1.y) * direction.x - (s2.x - s1.x) * direction.y) < TOL) { continue; } // project point, ignore edge boundaries d = this.pointDistance(p, s1, s2, direction); if (d !== null && (minprojection === null || d < minprojection)) { minprojection = d; } } if (minprojection !== null && (distance === null || minprojection > distance)) { distance = minprojection; } } return distance; }, // searches for an arrangement of A and B such that they do not overlap // if an NFP is given, only search for startpoints that have not already been traversed in the given NFP searchStartPoint: function (A, B, inside, NFP = []) { // clone arrays A = A.slice(0); B = B.slice(0); // close the loop for polygons if (A[0] != A[A.length - 1]) { A.push(A[0]); } if (B[0] != B[B.length - 1]) { B.push(B[0]); } for (let i = 0; i < A.length - 1; i++) { if (!A[i].marked) { A[i].marked = true; for (let j = 0; j < B.length; j++) { B.offsetx = A[i].x - B[j].x; B.offsety = A[i].y - B[j].y; let Binside = null; for (let k = 0; k < B.length; k++) { const inpoly = this.pointInPolygon({ x: B[k].x + B.offsetx, y: B[k].y + B.offsety }, A); if (inpoly !== null) { Binside = inpoly; break; } } if (Binside === null) { // A and B are the same return null; } let startPoint = { x: B.offsetx, y: B.offsety }; if (((Binside && inside) || (!Binside && !inside)) && !this.intersect(A, B) && !inNfp(startPoint, NFP)) { return startPoint; } // slide B along vector let vx = A[i + 1].x - A[i].x; let vy = A[i + 1].y - A[i].y; const d1 = this.polygonProjectionDistance(A, B, { x: vx, y: vy }); const d2 = this.polygonProjectionDistance(B, A, { x: -vx, y: -vy }); let d = null; // todo: clean this up if (d1 === null && d2 === null) { // nothin } else if (d1 === null) { d = d2; } else if (d2 === null) { d = d1; } else { d = Math.min(d1, d2); } // only slide until no longer negative // TODO: clean this up // if (d !== null && !_almostEqual(d, 0) && d > 0) { // } else { // continue // } if (d === null || _almostEqual(d, 0) || d <= 0) { continue; } const vd2 = vx * vx + vy * vy; if (d * d < vd2 && !_almostEqual(d * d, vd2)) { const vd = Math.sqrt(vx * vx + vy * vy); vx *= d / vd; vy *= d / vd; } B.offsetx += vx; B.offsety += vy; for (let k = 0; k < B.length; k++) { const inpoly = this.pointInPolygon({ x: B[k].x + B.offsetx, y: B[k].y + B.offsety }, A); if (inpoly !== null) { Binside = inpoly; break; } } startPoint = { x: B.offsetx, y: B.offsety }; if (((Binside && inside) || (!Binside && !inside)) && !this.intersect(A, B) && !inNfp(startPoint, NFP)) { return startPoint; } } } } // returns true if point already exists in the given nfp function inNfp(p, nfp) { if (!nfp || nfp.length == 0) { return false; } for (let i = 0; i < nfp.length; i++) { for (let j = 0; j < nfp[i].length; j++) { if (_almostEqual(p.x, nfp[i][j].x) && _almostEqual(p.y, nfp[i][j].y)) { return true; } } } return false; } return null; }, isRectangle: function (poly, tolerance) { const bb = this.getPolygonBounds(poly); if (!bb) { return false; } if (!tolerance) { tolerance = TOL; } for (let i = 0; i < poly.length; i++) { if (!_almostEqual(poly[i].x, bb.x) && !_almostEqual(poly[i].x, bb.x + bb.width)) { return false; } if (!_almostEqual(poly[i].y, bb.y) && !_almostEqual(poly[i].y, bb.y + bb.height)) { return false; } } return true; }, // returns an interior NFP for the special case where A is a rectangle noFitPolygonRectangle: function (A, B) { let minAx = A[0].x; let minAy = A[0].y; let maxAx = A[0].x; let maxAy = A[0].y; for (let i = 1; i < A.length; i++) { if (A[i].x < minAx) { minAx = A[i].x; } if (A[i].y < minAy) { minAy = A[i].y; } if (A[i].x > maxAx) { maxAx = A[i].x; } if (A[i].y > maxAy) { maxAy = A[i].y; } } let minBx = B[0].x; let minBy = B[0].y; let maxBx = B[0].x; let maxBy = B[0].y; for (let i = 1; i < B.length; i++) { if (B[i].x < minBx) { minBx = B[i].x; } if (B[i].y < minBy) { minBy = B[i].y; } if (B[i].x > maxBx) { maxBx = B[i].x; } if (B[i].y > maxBy) { maxBy = B[i].y; } } if (maxBx - minBx > maxAx - minAx) { return null; } if (maxBy - minBy > maxAy - minAy) { return null; } return [ [ { x: minAx - minBx + B[0].x, y: minAy - minBy + B[0].y }, { x: maxAx - maxBx + B[0].x, y: minAy - minBy + B[0].y }, { x: maxAx - maxBx + B[0].x, y: maxAy - maxBy + B[0].y }, { x: minAx - minBx + B[0].x, y: maxAy - maxBy + B[0].y } ] ]; }, // given a static polygon A and a movable polygon B, compute a no fit polygon by orbiting B