UNPKG

fernandez-polygon-decomposition

Version:

An algorithm to decompose polygons with holes from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" by J Fernández

1,682 lines (1,415 loc) 53.9 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var robustCompare = _interopDefault(require('robust-compare')); var robustCompress = _interopDefault(require('robust-compress')); var robustOrientation = _interopDefault(require('robust-orientation')); var robustProduct = _interopDefault(require('robust-product')); var robustDiff = _interopDefault(require('robust-subtract')); function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { return; } var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } /** * Useful to avoid floating point problems. */ var EPSILON = 0.00000001; var robust = true; var setRobustness = function setRobustness(bool) { robust = bool; }; var getRobustness = function getRobustness() { return robust; }; /** * Compares two vertices of the same polygon. Both of the vertex must be defined by an unique id. * * @param {{ x: number, y: number, id: number }} vertex1 * @param {{ x: number, y: number, id: number }} vertex2 * @returns {boolean} */ function vertexEquality(vertex1, vertex2) { if (vertex1.id === undefined || vertex2.id === undefined) { throw new Error('A vertex must be defined by an unique id.'); } return vertex1.id === vertex2.id; } /** * Compares two vertices. Both of the vertex must be defined by an unique id and optionnally by an originalId. * This method is used to compare vertices after the absoption phase of the absHol procedure. * * @param {{ x: number, y: number, id: number, originalId: number }} vertex1 * @param {{ x: number, y: number, id: number, originalId: number }} vertex2 * @returns {boolean} */ function vertexEqualityAfterAbsorption(vertex1, vertex2) { if (vertex1.id === undefined || vertex2.id === undefined) { throw new Error('A vertex must be defined by an unique id.'); } return (vertex1.originalId || vertex1.id) === (vertex2.originalId || vertex2.id); } /** * @param {{ x: number, y: number }} point1 * @param {{ x: number, y: number }} point2 * @returns {boolean} */ function pointEquality(point1, point2) { return point1.x === point2.x && point1.y === point2.y; } /** * @param {{ x: number, y: number }} point1 * @param {{ x: number, y: number }} point2 * @returns {number} */ function squaredDistance(point1, point2) { return (point1.x - point2.x) * (point1.x - point2.x) + (point1.y - point2.y) * (point1.y - point2.y); } /** * Checks if the polygon is flat (its area is 0). * Using orientation for robustness * * @param {{{ x: number, y: number, id: number }[]}} polygon * @returns {boolean} */ function isFlat(polygon) { var polygonLength = polygon.length; return polygon.every(function (point, index) { var previousPoint = polygon[(index - 1 + polygonLength) % polygonLength]; var nextPoint = polygon[(index + 1) % polygonLength]; return orientation(previousPoint, point, nextPoint) === 0; }); } /** * Check if the polygon vertices are clockwise ordered. * Using orientation for robustness * * @param {{ x: number, y: number}[]} polygon * @returns {boolean} */ function isClockwiseOrdered(polygon) { if (!isSimple(polygon)) { throw new Error('The isClockwiseOrdered method only works with simple polygons'); } var polygonLength = polygon.length; // find bottom right vertex var bottomRightVertexIndex = 0; var bottomRightVertex = polygon[0]; for (var i = 1; i < polygonLength; i++) { var vertex = polygon[i]; if (vertex.y < bottomRightVertex.y || vertex.y === bottomRightVertex.y && vertex.x > bottomRightVertex.x) { bottomRightVertex = vertex; bottomRightVertexIndex = i; } } return orientation(polygon[(bottomRightVertexIndex - 1 + polygonLength) % polygonLength], bottomRightVertex, polygon[(bottomRightVertexIndex + 1) % polygonLength]) > 0; } /** * This method always returns a new array * * @param {{ x: number, y: number}[]} polygon * @returns {{ x: number, y: number}[]} */ function orderClockwise(polygon) { if (!isClockwiseOrdered(polygon)) { return _toConsumableArray(polygon).reverse(); } return _toConsumableArray(polygon); } /** * See https://stackoverflow.com/questions/38856588/given-three-coordinate-points-how-do-you-detect-when-the-angle-between-them-cro. * The three points are in clockwise order. * If the result if positive, then it is a clockwise turn, if it is negative, a ccw one. * If the result is 0, the points are collinear. * * @param {{ x: number, y: number}} point1 * @param {{ x: number, y: number}} point2 * @param {{ x: number, y: number}} point3 * @returns {number} */ function orientation(point1, point2, point3) { if (robust) { var o = robustOrientation([point1.x, point1.y], [point2.x, point2.y], [point3.x, point3.y]); return o === 0 ? o : -o; // the y-axis is inverted } else { // return -((point1.y - point3.y) * (point2.x - point3.x) - (point1.x - point3.x) * (point2.y - point3.y)); return (point2.x - point1.x) * (point3.y - point1.y) - (point2.y - point1.y) * (point3.x - point1.x); } } /** * Checks on which side of the line (point2, point3) the point point1 is. * * @param {{ x: number, y: number}} point1 * @param {{ x: number, y: number}} point2 * @param {{ x: number, y: number}} point3 * @returns {number} */ function sideOfLine(point1, point2, point3) { return orientation(point2, point3, point1); } var VERTEX_CODE = 100; var EDGE_CODE = 101; /** * Winding number algorithm. * See https://en.wikipedia.org/wiki/Point_in_polygon?#Winding_number_algorithm * And more specifically http://www.inf.usi.ch/hormann/papers/Hormann.2001.TPI.pdf * * @param {{ x: number, y: number}} point * @param {{ x: number, y: number}[]} polygon * @returns {number} */ function windingNumber(point, polygon) { var polygonPoint = polygon[0]; if (polygonPoint.x === point.x && polygonPoint.y === point.y) { return VERTEX_CODE; } var polygonLength = polygon.length; var wn = 0; for (var i = 0; i < polygonLength; i++) { var _polygonPoint = polygon[i]; var nextPolygonPoint = polygon[(i + 1) % polygonLength]; if (nextPolygonPoint.y === point.y) { if (nextPolygonPoint.x === point.x) { return VERTEX_CODE; } else { if (_polygonPoint.y === point.y && nextPolygonPoint.x > point.x === _polygonPoint.x < point.x) { return EDGE_CODE; } } } if (_polygonPoint.y < point.y !== nextPolygonPoint.y < point.y) { // crossing if (_polygonPoint.x >= point.x) { if (nextPolygonPoint.x > point.x) { // wn += 2 * (nextPolygonPoint.y > polygonPoint.y ? 1 : -1) - 1; wn += nextPolygonPoint.y > _polygonPoint.y ? 1 : -1; } else { var det = (_polygonPoint.x - point.x) * (nextPolygonPoint.y - point.y) - (nextPolygonPoint.x - point.x) * (_polygonPoint.y - point.y); if (det === 0) { return EDGE_CODE; } else if (det > 0 === nextPolygonPoint.y > _polygonPoint.y) { // right_crossing // wn += 2 * (nextPolygonPoint.y > polygonPoint.y ? 1 : -1) - 1; wn += nextPolygonPoint.y > _polygonPoint.y ? 1 : -1; } } } else { if (nextPolygonPoint.x > point.x) { var _det = (_polygonPoint.x - point.x) * (nextPolygonPoint.y - point.y) - (nextPolygonPoint.x - point.x) * (_polygonPoint.y - point.y); if (_det === 0) { return EDGE_CODE; } else if (_det > 0 === nextPolygonPoint.y > _polygonPoint.y) { // right_crossing // wn += 2 * (nextPolygonPoint.y > polygonPoint.y ? 1 : -1) - 1; wn += nextPolygonPoint.y > _polygonPoint.y ? 1 : -1; } } } } } return wn; } /** * Winding number algorithm. * Using robust arithmetic. * * @param {{ x: number, y: number}} point * @param {{ x: number, y: number}[]} polygon * @returns {number} */ function robustWindingNumber(point, polygon) { var polygonPoint = polygon[0]; if (polygonPoint.x === point.x && polygonPoint.y === point.y) { return VERTEX_CODE; } var polygonLength = polygon.length; var wn = 0; for (var i = 0; i < polygonLength; i++) { var _polygonPoint2 = polygon[i]; var nextPolygonPoint = polygon[(i + 1) % polygonLength]; if (nextPolygonPoint.y === point.y) { if (nextPolygonPoint.x === point.x) { return VERTEX_CODE; } else { if (_polygonPoint2.y === point.y && nextPolygonPoint.x > point.x === _polygonPoint2.x < point.x) { return EDGE_CODE; } } } if (_polygonPoint2.y < point.y !== nextPolygonPoint.y < point.y) { // crossing if (_polygonPoint2.x >= point.x) { if (nextPolygonPoint.x > point.x) { // wn += 2 * ((nextPolygonPoint.y > polygonPoint.y) | 0) - 1; wn += nextPolygonPoint.y > _polygonPoint2.y ? 1 : -1; } else { var det = robustDiff(robustProduct(robustDiff([_polygonPoint2.x], [point.x]), robustDiff([nextPolygonPoint.y], [point.y])), robustProduct(robustDiff([nextPolygonPoint.x], [point.x]), robustDiff([_polygonPoint2.y], [point.y]))); var detComparison = robustCompare(det, [0]); if (detComparison === 0) { return EDGE_CODE; } else if (detComparison > 0 === nextPolygonPoint.y > _polygonPoint2.y) { // right_crossing // wn += 2 * ((nextPolygonPoint.y > polygonPoint.y) | 0) - 1; wn += nextPolygonPoint.y > _polygonPoint2.y ? 1 : -1; } } } else { if (nextPolygonPoint.x > point.x) { var _det2 = robustDiff(robustProduct(robustDiff([_polygonPoint2.x], [point.x]), robustDiff([nextPolygonPoint.y], [point.y])), robustProduct(robustDiff([nextPolygonPoint.x], [point.x]), robustDiff([_polygonPoint2.y], [point.y]))); var _detComparison = robustCompare(_det2, [0]); if (_detComparison === 0) { return EDGE_CODE; } else if (_detComparison > 0 === nextPolygonPoint.y > _polygonPoint2.y) { // right_crossing // wn += 2 * ((nextPolygonPoint.y > polygonPoint.y) | 0) - 1; wn += nextPolygonPoint.y > _polygonPoint2.y ? 1 : -1; } } } } } return wn; } /** * Checks if the point is inside (or on the edge) of the polygon. * * @param {{ x: number, y: number}} point * @param {{ x: number, y: number}[]} polygon * @returns {boolean} */ function inPolygon(point, polygon) { if (robust) { // return classifyPoint(polygon.map(({ x, y }) => [x, y]), [point.x, point.y]) <= 0; return robustWindingNumber(point, polygon) !== 0; } else { return windingNumber(point, polygon) !== 0; } } /** * Checks if the point is inside (or on the edge) of a convex polygon. * We assume that the vertices are in clockwise order. * * @param {{ x: number, y: number}} point * @param {{ x: number, y: number}[]} convexPolygon * @returns {boolean} */ function inConvexPolygon(point, convexPolygon) { var polygonLength = convexPolygon.length; return convexPolygon.every(function (previousPoint, index) { var nextPoint = convexPolygon[(index + 1) % polygonLength]; return orientation(previousPoint, point, nextPoint) <= 0; }); } /** * Check if the polygon polygon2 is (at least partially) contained by the polygon polygon1. * * @param {{ x: number, y: number, id: number }[]} polygon1 * @param {{ x: number, y: number, id: number }[]} polygon2 * @returns {boolean} */ function containsPolygon(polygon1, polygon2) { return polygon2.some(function (vertex) { return inPolygon(vertex, polygon1); }); } /** * Check if the polygon polygon2 is totally contained by the polygon polygon1. * * @param {{ x: number, y: number, id: number }[]} polygon1 * @param {{ x: number, y: number, id: number }[]} polygon2 * @returns {boolean} */ function containsEntirePolygon(polygon1, polygon2) { return polygon2.every(function (vertex) { return inPolygon(vertex, polygon1); }); } /** * Given a vertex of one polygon, returns the next vertex (in clockwise order) of this polygon. * * @param {{ x: number, y: number, id: number }} vertex * @param {{ x: number, y: number, id: number }[]} polygon * @returns {{ x: number, y: number, id: number }} */ function nextVertex(vertex, polygon) { var polygonLength = polygon.length; var vertexIndex = polygon.findIndex(function (v) { return vertexEquality(vertex, v); }); if (vertexIndex === -1) { throw new Error('could not find vertex'); } return polygon[(vertexIndex + 1) % polygonLength]; } /** * Given a vertex of one polygon, returns the previous vertex (in clockwise order) of this polygon. * * @param {{ x: number, y: number, id: number }} vertex * @param {{ x: number, y: number, id: number }[]} polygon * @returns {{ x: number, y: number, id: number }} */ function previousVertex(vertex, polygon) { var polygonLength = polygon.length; var vertexIndex = polygon.findIndex(function (v) { return vertexEquality(vertex, v); }); if (vertexIndex === -1) { throw new Error('could not find vertex'); } return polygon[(vertexIndex - 1 + polygonLength) % polygonLength]; } /** * Checks if a point is one of the vertex of a polygon. * * @param {{ x: number, y: number }} point * @param {{ x: number, y: number, id: number }[]} polygon * @returns {boolean} */ function isAVertex(point, polygon) { return polygon.some(function (v) { return pointEquality(v, point); }); } /** * Returns all the notches of a given polygon. * * @param {{ x: number, y: number, id: number }[]} polygon * @returns {{ x: number, y: number, id: number }[]} */ function getNotches(polygon) { var polygonLength = polygon.length; return polygon.filter(function (vertex, vertexIndex) { return orientation(polygon[(vertexIndex - 1 + polygonLength) % polygonLength], vertex, polygon[(vertexIndex + 1) % polygonLength]) < 0; }); } /** * Returns all the edges of a given polygon. * An edge is one segment between two consecutive vertex of the polygon. * * @param {{ x: number, y: number, id: number }[]} polygon * @returns {{ a: { x: number, y: number, id: number }, b: { x: number, y: number, id: number }}[]} */ function getEdges(polygon) { var edges = []; var polygonLength = polygon.length; for (var i = 0; i < polygonLength; i++) { edges.push({ a: polygon[i], b: polygon[(i + 1) % polygonLength] }); } return edges; } /** * Given a vertex of one polygon, returns the next notch (in clockwise order) of this polygon. * Returns null if there are no notch in the polygon. * * @param {{ x: number, y: number, id: number }} vertex * @param {{ x: number, y: number, id: number }[]} polygon * @returns {({ x: number, y: number, id: number }|null)} */ function nextNotch(vertex, polygon) { var polygonLength = polygon.length; var vertexIndex = polygon.findIndex(function (v) { return vertexEquality(vertex, v); }); var notchIndex = (vertexIndex + 1) % polygonLength; while (notchIndex !== vertexIndex) { if (orientation(polygon[(notchIndex - 1 + polygonLength) % polygonLength], polygon[notchIndex], polygon[(notchIndex + 1) % polygonLength]) < 0) { return polygon[notchIndex]; } notchIndex = (notchIndex + 1) % polygonLength; } // if we started by the only notch, it will return the notch. if (orientation(polygon[(notchIndex - 1 + polygonLength) % polygonLength], polygon[notchIndex], polygon[(notchIndex + 1) % polygonLength]) < 0) { return polygon[notchIndex]; } return null; } /** * Given a vertex of one polygon, returns the previous notch (in clockwise order) of this polygon. * Returns null if there are no notch in the polygon. * * @param {{ x: number, y: number, id: number }} vertex * @param {{ x: number, y: number, id: number }[]} polygon * @returns {({ x: number, y: number, id: number }|null)} */ function previousNotch(vertex, polygon) { var polygonLength = polygon.length; var vertexIndex = polygon.findIndex(function (v) { return vertexEquality(vertex, v); }); var notchIndex = (vertexIndex - 1 + polygonLength) % polygonLength; while (notchIndex !== vertexIndex) { if (orientation(polygon[(notchIndex - 1 + polygonLength) % polygonLength], polygon[notchIndex], polygon[(notchIndex + 1) % polygonLength]) < 0) { return polygon[notchIndex]; } notchIndex = (notchIndex - 1 + polygonLength) % polygonLength; } // if we started by the only notch, it will return the notch. if (orientation(polygon[(notchIndex - 1 + polygonLength) % polygonLength], polygon[notchIndex], polygon[(notchIndex + 1) % polygonLength]) < 0) { return polygon[notchIndex]; } return null; } /** * Removes polygon2 from polygon1. * polygon2 vertices must be a subset of polygon1 vertices * * @param {{ x: number, y: number, id: number }[]} polygon1 * @param {{ x: number, y: number, id: number }[]} polygon2 * @returns {{ x: number, y: number, id: number }[]} */ function substractPolygons(polygon1, polygon2) { // const firstIndex = polygon1.findIndex(p => pointEquality(p, polygon2[0])); // const lastIndex = polygon1.findIndex(p => pointEquality(p, polygon2[polygon2.length - 1])); var firstIndex = polygon1.findIndex(function (p) { return vertexEquality(p, polygon2[0]); }); var lastIndex = polygon1.findIndex(function (p) { return vertexEquality(p, polygon2[polygon2.length - 1]); }); if (firstIndex < lastIndex) { return [].concat(_toConsumableArray(polygon1.slice(0, firstIndex)), [polygon1[firstIndex]], _toConsumableArray(polygon1.slice(lastIndex))); } else { return _toConsumableArray(polygon1.slice(lastIndex, firstIndex + 1)); } } /** * @param {{{ x: number, y: number, id: number }[]}} polygon * @return {boolean} */ function isConvex(polygon) { var polygonLength = polygon.length; return polygon.every(function (vertex, vertexIndex) { return orientation(polygon[(vertexIndex - 1 + polygonLength) % polygonLength], vertex, polygon[(vertexIndex + 1) % polygonLength]) >= 0; }); } /** * Quick hack to convert a non-overlapping increasing sequence into a number. * * @param {number[]} sequence * @returns {number} */ function robustSequenceToNumber(sequence) { var compressedSequence = robustCompress(_toConsumableArray(sequence)); return compressedSequence[compressedSequence.length - 1]; } /** * See http://paulbourke.net/geometry/pointlineplane/ * This method performs exact arithmetic calculations (except for the x and y values) * * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} line1 * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} line2 * @returns {{ x: number, y: number, insideSegment1: boolean, onEdgeSegment1: boolean, insideSegment2: boolean, onEdgeSegment2: boolean }} */ function robustLineIntersection(line1, line2) { var _line1$a = line1.a, x1 = _line1$a.x, y1 = _line1$a.y, _line1$b = line1.b, x2 = _line1$b.x, y2 = _line1$b.y; var _line2$a = line2.a, x3 = _line2$a.x, y3 = _line2$a.y, _line2$b = line2.b, x4 = _line2$b.x, y4 = _line2$b.y; var y4y3 = robustDiff([y4], [y3]); var x2x1 = robustDiff([x2], [x1]); var x4x3 = robustDiff([x4], [x3]); var y2y1 = robustDiff([y2], [y1]); var y1y3 = robustDiff([y1], [y3]); var x1x3 = robustDiff([x1], [x3]); var denom = robustDiff(robustProduct(y4y3, x2x1), robustProduct(x4x3, y2y1)); var robustDenomComparison = robustCompare(denom, [0]); if (robustDenomComparison === 0) { return null; } var ua = robustDiff(robustProduct(x4x3, y1y3), robustProduct(y4y3, x1x3)); var ub = robustDiff(robustProduct(x2x1, y1y3), robustProduct(y2y1, x1x3)); var comparisonUaMin, comparisonUaMax, comparisonUbMin, comparisonUbMax; if (robustDenomComparison > 0) { comparisonUaMin = robustCompare(ua, [0]); comparisonUaMax = robustCompare(ua, denom); comparisonUbMin = robustCompare(ub, [0]); comparisonUbMax = robustCompare(ub, denom); } else { comparisonUaMin = robustCompare([0], ua); comparisonUaMax = robustCompare(denom, ua); comparisonUbMin = robustCompare([0], ub); comparisonUbMax = robustCompare(denom, ub); } var nonRobustDenom = robustSequenceToNumber(denom); // x and y are not exact numbers, but it is enough for the algo return { x: x1 + robustSequenceToNumber(robustProduct(ua, x2x1)) / nonRobustDenom, y: y1 + robustSequenceToNumber(robustProduct(ua, y2y1)) / nonRobustDenom, insideSegment1: comparisonUaMin > 0 && comparisonUaMax < 0, onEdgeSegment1: comparisonUaMin === 0 || comparisonUaMax === 0, insideSegment2: comparisonUbMin > 0 && comparisonUbMax < 0, onEdgeSegment2: comparisonUbMin === 0 || comparisonUbMax === 0 }; } /** * See http://paulbourke.net/geometry/pointlineplane/ * * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} line1 * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} line2 * @returns {{ x: number, y: number, insideSegment1: boolean, onEdgeSegment1: boolean, insideSegment2: boolean, onEdgeSegment2: boolean }} */ function lineIntersection(line1, line2) { if (robust) { return robustLineIntersection(line1, line2); } var _line1$a2 = line1.a, x1 = _line1$a2.x, y1 = _line1$a2.y, _line1$b2 = line1.b, x2 = _line1$b2.x, y2 = _line1$b2.y; var _line2$a2 = line2.a, x3 = _line2$a2.x, y3 = _line2$a2.y, _line2$b2 = line2.b, x4 = _line2$b2.x, y4 = _line2$b2.y; var denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if (Math.abs(denom) < EPSILON) { return null; } var ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom; var ub = ((x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)) / denom; return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1), insideSegment1: ua > 0 && ua < 1, onEdgeSegment1: ua === 0 || ua === 1, insideSegment2: ub > 0 && ub < 1, onEdgeSegment2: ub === 0 || ub === 1 }; } /** * Checks if the polygon is simple. * See https://en.wikipedia.org/wiki/Simple_polygon * * @param {{ x: number, y: number, id: number }[]} polygon * @returns {boolean} */ function isSimple(polygon) { if (polygon.length < 3) { return true; } var segments = []; for (var i = 0; i < polygon.length - 1; i++) { segments.push({ a: polygon[i], b: polygon[i + 1] }); } segments.push({ a: polygon[polygon.length - 1], b: polygon[0] }); return !segments.some(function (segment1) { return segments.some(function (segment2) { if (segment1 === segment2) { return false; } var intersection = lineIntersection(segment1, segment2); if (intersection === null) { return false; } var insideSegment1 = intersection.insideSegment1, insideSegment2 = intersection.insideSegment2; return insideSegment1 && insideSegment2; }); }); } /** * This is the MP1 procedure taken from "Algorithms for the decomposition of a polygon into convex polygons" by J. Fernandez et al. * The variable names (for the most part) are named according to the publication. * * @param {{ x: number, y: number, id: number }[]} P P is a polygon with clockwise ordered vertices. * @param {{ x: number, y: number, id: number }[]} initialLVertices The initial vertices for the polygon L * @returns {{ L: { x: number, y: number, id: number }[], end: boolean }} */ function MP1(P, initialLVertices) { if (P.length < 4) { return { L: _toConsumableArray(P), end: true }; } var L = _toConsumableArray(initialLVertices); if (L.length < 1) { L.push(P[0]); } if (L.length < 2) { L.push(nextVertex(L[0], P)); } var v1 = L[0]; var v2 = L[1]; var vim1 = v1; var vi = v2; var vip1 = nextVertex(vi, P); while (L.length < P.length) { if (orientation(vim1, vi, vip1) >= 0 && orientation(vi, vip1, v1) >= 0 && orientation(vip1, v1, v2) >= 0) { L.push(vip1); } else { break; } vim1 = vi; vi = vip1; vip1 = nextVertex(vi, P); } if (P.length === L.length) { return { L: L, end: true }; } else if (L.length < 2) { return { L: L, end: true }; } else { var _loop = function _loop() { var PmL = substractPolygons(P, L); // filter on L's bounding rectangle first ? var notches = getNotches(PmL).filter(function (point) { return !isAVertex(point, L); }).filter(function (point) { return inConvexPolygon(point, L); }); if (notches.length === 0) { return "break"; } var v1 = L[0]; var k = L.length; var vk = L[k - 1]; L = L.slice(0, -1); // L.filter(point => !vertexEquality(point, vk)); notches.forEach(function (notch) { var sideOfVk = Math.sign(sideOfLine(vk, v1, notch)); if (sideOfVk !== 0) { L = L.filter(function (point) { return Math.sign(sideOfLine(point, v1, notch)) !== sideOfVk; }); } }); }; while (L.length > 2) { var _ret = _loop(); if (_ret === "break") break; } return { L: L, end: false }; } } /** * This is the MP1' procedure taken from "Algorithms for the decomposition of a polygon into convex polygons" by J. Fernandez et al. * The variable names (for the most part) are named according to the publication. * * @param {{ x: number, y: number, id: number }[]} P P is a polygon with clockwise ordered vertices. * @param {{ x: number, y: number, id: number }[]} initialLVertices The initial vertices for the polygon L * @returns {{ L: { x: number, y: number, id: number }[], end: boolean }} */ function MP1Prime(P, initialLVertices) { if (P.length < 4) { return { L: _toConsumableArray(P), end: true }; } var L = _toConsumableArray(initialLVertices); if (L.length < 1) { L.unshift(P[0]); } if (L.length < 2) { L.unshift(previousVertex(L[0], P)); } var vk = L[L.length - 1]; var vkm1 = L[L.length - 2]; var vim1 = L[1]; var vi = L[0]; var vip1 = previousVertex(vi, P); while (L.length < P.length) { if (orientation(vim1, vi, vip1) <= 0 && orientation(vi, vip1, vk) <= 0 && orientation(vip1, vk, vkm1) <= 0) { L.unshift(vip1); } else { break; } vim1 = vi; vi = vip1; vip1 = previousVertex(vi, P); } if (P.length === L.length) { return { L: L, end: true }; } else if (L.length < 2) { return { L: L, end: true }; } else { var _loop2 = function _loop2() { var PmL = substractPolygons(P, L); var notches = getNotches(PmL).filter(function (point) { return !isAVertex(point, L); }).filter(function (point) { return inConvexPolygon(point, L); }); if (notches.length === 0) { return "break"; } // CCW order var v1 = L[L.length - 1]; var vk = L[0]; L = L.slice(1); // L.filter(point => !vertexEquality(point, vk)); notches.forEach(function (notch) { var sideOfVk = Math.sign(sideOfLine(vk, v1, notch)); if (sideOfVk !== 0) { L = L.filter(function (point) { return Math.sign(sideOfLine(point, v1, notch)) !== sideOfVk; }); } }); }; while (L.length > 2) { var _ret2 = _loop2(); if (_ret2 === "break") break; } return { L: L, end: false }; } } function rotateRight(arr, n) { var length = arr.length; return [].concat(_toConsumableArray(arr.slice(length - n)), _toConsumableArray(arr.slice(0, length - n))); } /** * Merges two polygons. * polygon1 and polygon2 should be convex and share an edge (= two consecutive vertices) * * @param {{ x: number, y: number, id: number }[]} polygon1 * @param {{ x: number, y: number, id: number }[]} polygon2 * @returns {{ x: number, y: number, id: number }[]} */ function mergePolygons(polygon1, polygon2) { // const sharedVertices = polygon1.map((v1, index) => [index, polygon2.findIndex(v2 => pointEquality(v1, v2))]).filter(([_, v2Index]) => v2Index > -1); // can be problematic when a point corresponds to several vertices var sharedVertices = polygon1.map(function (v1, index) { return [index, polygon2.findIndex(function (v2) { return vertexEqualityAfterAbsorption(v1, v2); })]; }).filter(function (_ref) { var _ref2 = _slicedToArray(_ref, 2), _ = _ref2[0], v2Index = _ref2[1]; return v2Index > -1; }); var polygon1Length = polygon1.length; if (sharedVertices.length !== 2) { throw new Error("sharedVertices length should be 2 : ".concat(JSON.stringify(sharedVertices))); } if ((sharedVertices[0][0] + 1) % polygon1Length === sharedVertices[1][0]) { return [].concat(_toConsumableArray(rotateRight(polygon1, polygon1Length - sharedVertices[1][0])), _toConsumableArray(rotateRight(polygon2, polygon2.length - sharedVertices[0][1]).slice(1, -1))); } else { return [].concat(_toConsumableArray(rotateRight(polygon1, polygon1Length - sharedVertices[0][0])), _toConsumableArray(rotateRight(polygon2, polygon2.length - sharedVertices[1][1]).slice(1, -1))); } } /** * Procedure to remove inessential diagonals from the partition of convex polygons. * * @params {{ x: number, y: number, id: number }[][]} polygons * @params {{ i2: { x: number, y: number, id: number }, j2: { x: number, y: number, id: number }, rightPolygon: { x: number, y: number, id: number }[][], leftPolygon: { x: number, y: number, id: number }[][] }[]} * @returns {{ x: number, y: number, id: number }[][]} */ function mergingAlgorithm(polygons, LLE) { if (polygons.length < 2) { return polygons; } // LDP[poly] = true means that the polygon `poly` is one of the definitive polygons of the partition after the merging process. var LDP = new Map(); // LUP[poly1] = poly2 means that the polygon `poly1` is part of the polygon `poly2`. var LUP = new Map(); polygons.forEach(function (poly) { LDP.set(poly, true); LUP.set(poly, poly); }); if (LLE.length + 1 !== polygons.length) { throw new Error('wtf ? LLE + 1 !== polygons.length (' + (LLE.length + 1) + ', ' + polygons.length + ')'); } var _loop = function _loop(j) { var _LLE$j = LLE[j], i2 = _LLE$j.i2, j2 = _LLE$j.j2, rightPolygon = _LLE$j.rightPolygon, leftPolygon = _LLE$j.leftPolygon; var Pj = LUP.get(leftPolygon); var Pu = LUP.get(rightPolygon); var PjLength = Pj.length; var PuLength = Pu.length; // custom nextVertex & previousVertex to take into account the originalId (for absHol) var i1 = Pu[(Pu.findIndex(function (v) { return vertexEqualityAfterAbsorption(v, i2); }) + PuLength - 1) % PuLength]; // previousVertex(i2, Pu); var i3 = Pj[(Pj.findIndex(function (v) { return vertexEqualityAfterAbsorption(v, i2); }) + 1) % PjLength]; // nextVertex(i2, Pj); var j1 = Pj[(Pj.findIndex(function (v) { return vertexEqualityAfterAbsorption(v, j2); }) + PjLength - 1) % PjLength]; // previousVertex(j2, Pj) var j3 = Pu[(Pu.findIndex(function (v) { return vertexEqualityAfterAbsorption(v, j2); }) + 1) % PuLength]; // nextVertex(j2, Pu); if (orientation(i1, i2, i3) >= 0 && orientation(j1, j2, j3) >= 0) { var P = mergePolygons(Pj, Pu); if (!isConvex(P)) { throw new Error('mergePolygons is not convex !'); } LDP.set(Pj, false); LDP.set(Pu, false); LDP.set(P, true); LUP.set(P, P); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = LUP.keys()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var poly = _step.value; if (LUP.get(poly) === Pj || LUP.get(poly) === Pu) { LUP.set(poly, P); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator["return"] != null) { _iterator["return"](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } } }; for (var j = 0; j < LLE.length; j++) { _loop(j); } return _toConsumableArray(LDP.entries()).filter(function (_ref3) { var _ref4 = _slicedToArray(_ref3, 2), _ = _ref4[0], inPartition = _ref4[1]; return inPartition; }).map(function (_ref5) { var _ref6 = _slicedToArray(_ref5, 1), polygon = _ref6[0]; return polygon; }); } function preprocessPolygon(polygon) { var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var polygonLength = polygon.length; return polygon.filter(function (vertex, index) { return !pointEquality(vertex, polygon[(index + 1) % polygonLength]); }).map(function (vertex, index) { return _objectSpread2({}, vertex, { id: index + offset }); }); } /** * This is the MP5 procedure taken from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" * * @param {{ x: number, y: number, id: number }[]} polygon * @param {{ x: number, y: number, id: number }} startingVertex * @returns {{ L: { x: number, y: number, id: number }[], end: boolean }} */ function MP5Procedure(polygon) { var startingVertex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : polygon[0]; if (polygon.length < 4) { return { convexPolygon: _toConsumableArray(polygon), end: true }; } var startingNotch = nextNotch(startingVertex, polygon); if (startingNotch === null) { // the polygon is convex if there is no notch in it return { convexPolygon: _toConsumableArray(polygon), end: true }; } var currentNotch = startingNotch; var _loop = function _loop() { var _MP = MP1(polygon, [currentNotch]), cwL = _MP.L, cwEnd = _MP.end; if (cwEnd) { return { v: { convexPolygon: cwL, end: true } }; } // MP1 + notch checking = MP3 if (cwL.length > 2 && getNotches(polygon).some(function (vertex) { return vertexEquality(vertex, cwL[0]) || vertexEquality(vertex, cwL[cwL.length - 1]); })) { return { v: { convexPolygon: cwL, end: false } }; } currentNotch = nextNotch(currentNotch, polygon); }; do { var _ret = _loop(); if (_typeof(_ret) === "object") return _ret.v; } while (!vertexEquality(currentNotch, startingNotch)); currentNotch = startingNotch; var _loop2 = function _loop2() { var _MP1Prime = MP1Prime(polygon, [currentNotch]), ccwL = _MP1Prime.L, ccwEnd = _MP1Prime.end; if (ccwEnd) { return { v: { convexPolygon: ccwL, end: true } }; } // MP1 + notch checking = MP3 if (ccwL.length > 2 && getNotches(polygon).some(function (vertex) { return vertexEquality(vertex, ccwL[0]) || vertexEquality(vertex, ccwL[ccwL.length - 1]); })) { return { v: { convexPolygon: ccwL, end: false } }; } currentNotch = previousNotch(currentNotch, polygon); }; do { var _ret2 = _loop2(); if (_typeof(_ret2) === "object") return _ret2.v; } while (!vertexEquality(currentNotch, startingNotch)); throw new Error('ERROR MP5Procedure 3'); } /** * This is the full MP5 algorithm taken from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" * * @param {{ x: number, y: number }[]} polygon * @returns {{ x: number, y: number }[][]} The partition of convex polygons */ function MP5(polygon) { if (!Array.isArray(polygon)) { throw new Error('MP5 can only take an array of points {x, y} as input'); } if (polygon.length <= 2) { return [polygon]; } if (!isClockwiseOrdered(polygon)) { throw new Error('MP5 can only work with clockwise ordered polygon'); } // L is containing the convex polygons. var L = []; // LLE is a list containing the diagonals of the partition. It will be used to merge inessential diagonals. var LLE = []; // Adds an id to each vertex. var P = preprocessPolygon(polygon); var _loop3 = function _loop3() { var _MP5Procedure = MP5Procedure(P), convexPolygon = _MP5Procedure.convexPolygon, end = _MP5Procedure.end; var diagonal = { a: convexPolygon[0], b: convexPolygon[convexPolygon.length - 1] }; L.push(convexPolygon); getEdges(convexPolygon).forEach(function (edge) { for (var i = 0; i < LLE.length; i++) { var _LLE$i = LLE[i], i2 = _LLE$i.i2, j2 = _LLE$i.j2; if (vertexEquality(i2, edge.b) && vertexEquality(j2, edge.a)) { LLE[i].leftPolygon = convexPolygon; break; } } }); if (end) { return "break"; } LLE.push({ i2: diagonal.b, j2: diagonal.a, rightPolygon: convexPolygon }); P = substractPolygons(P, convexPolygon); }; while (true) { var _ret3 = _loop3(); if (_ret3 === "break") break; } return mergingAlgorithm(L, LLE).map(function (poly) { return poly.map(function (_ref) { var x = _ref.x, y = _ref.y; return { x: x, y: y }; }); }); } /** * Checks if the given segment instersects the polygon * * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} segment * @param {{ x: number, y: number }[]} polygon * @returns {boolean} */ var segmentIntersectsPolygon = function segmentIntersectsPolygon(segment, polygon) { var polygonLength = polygon.length; for (var i = 0; i < polygonLength; i++) { var edge = { a: polygon[(i - 1 + polygonLength) % polygonLength], b: polygon[i] }; var intersection = lineIntersection(segment, edge); if (intersection === null) { continue; } var insideSegment1 = intersection.insideSegment1, insideSegment2 = intersection.insideSegment2, onEdgeSegment1 = intersection.onEdgeSegment1, onEdgeSegment2 = intersection.onEdgeSegment2; if ((insideSegment1 || onEdgeSegment1) && (insideSegment2 || onEdgeSegment2)) { return true; } } return false; }; /** * Returns all the edges of the given hole that intersects the given segment. * * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} segment * @param {{ x: number, y: number, id: number }[]} hole * @returns {{ x: number, y: number, edge: { a: { x: number, y: number }, b: { x: number, y: number }}, hole: { x: number, y: number, id: number }[] }[]} */ var getSegmentHoleIntersectionEdges = function getSegmentHoleIntersectionEdges(segment, hole) { var edges = []; var holeLength = hole.length; for (var i = 0; i < holeLength; i++) { var edge = { a: hole[(i - 1 + holeLength) % holeLength], b: hole[i] }; var intersection = lineIntersection(segment, edge); if (intersection === null) { continue; } var x = intersection.x, y = intersection.y, insideSegment1 = intersection.insideSegment1, insideSegment2 = intersection.insideSegment2, onEdgeSegment1 = intersection.onEdgeSegment1, onEdgeSegment2 = intersection.onEdgeSegment2; if ((insideSegment1 || onEdgeSegment1) && (insideSegment2 || onEdgeSegment2)) { edges.push({ x: x, y: y, edge: edge, hole: hole }); } } return edges; }; var rotateLeft = function rotateLeft(a) { var i = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 1; return [].concat(_toConsumableArray(a.slice(i)), _toConsumableArray(a.slice(0, i))); }; /** * This is the DrawTrueDiagonal procedure taken from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" * * @param {{ a: { x: number, y: number }, b: { x: number, y: number }}} diagonal * @param {{ x: number, y: number, id: number }[]} C * @param {{ x: number, y: number, id: number }[][]} holesInC * @returns {{ a: { x: number, y: number }, b: { x: number, y: number }, hole: { x: number, y: number, id: number }[]}} */ var drawTrueDiagonal = function drawTrueDiagonal(diagonal, C, holesInC) { var comparator = function comparator(a, b) { return squaredDistance(diagonal.a, a) - squaredDistance(diagonal.a, b); }; var edges = []; var holesInCLength = holesInC.length; for (var i = 0; i < holesInCLength; i++) { var _edges; (_edges = edges).push.apply(_edges, _toConsumableArray(getSegmentHoleIntersectionEdges(diagonal, holesInC[i]))); } var previousClosestVertexId = diagonal.b.id; while (edges.length > 0) { /* const closestEdge = edges.sort(comparator).find(({ edge }) => { return inConvexPolygon(edge.a, C) || inConvexPolygon(edge.b, C); }); */ var closestEdge = edges.sort(comparator)[0]; var closestVertex = Object.values(closestEdge.edge).filter(function (v) { return inConvexPolygon(v, C); }).sort(comparator)[0]; if (closestVertex.id === previousClosestVertexId) { return diagonal; } diagonal = { a: diagonal.a, b: closestVertex, hole: closestEdge.hole }; previousClosestVertexId = closestVertex.id; edges = []; for (var _i = 0; _i < holesInCLength; _i++) { var _edges2; (_edges2 = edges).push.apply(_edges2, _toConsumableArray(getSegmentHoleIntersectionEdges(diagonal, holesInC[_i]))); } } return diagonal; }; /** * This is the AbsHol algorithm taken from "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" * * @param {{ x: number, y: number, id: number }[]} P * @param {{ x: number, y: number, id: number }[][]} holes * @param {number} idOffset * @returns {{ LPCP: { x: number, y: number, id: number }[][], trueDiagonals: { a: { x: number, y: number, id: number }, b: { x: number, y: number, id: number } }[] , LLE: { i2: { x: number, y: number, id: number }, j2: { x: number, y: number, id: number }, rightPolygon: { x: number, y: number, id: number }[][], leftPolygon: { x: number, y: number, id: number }[][] }[] }} The partition of convex polygons */ function absHolProcedure(P, holes, idOffset) { var LLE = []; var trueDiagonals = []; var LPCP = []; var Q = _toConsumableArray(P); var _loop = function _loop() { var _MP5Procedure = MP5Procedure(Q), C = _MP5Procedure.convexPolygon, end = _MP5Procedure.end; var diagonal = { a: C[0], b: C[C.length - 1], hole: null }; var holesLength = holes.length; var diagonalIsCutByAHole = false; var holesInC = []; for (var i = 0; i < holesLength; i++) { var hole = holes[i]; if (!diagonalIsCutByAHole && segmentIntersectsPolygon(diagonal, hole)) { diagonalIsCutByAHole = true; diagonal.hole = hole; } if (containsPolygon(C, hole)) { holesInC.push(hole); } } if (diagonalIsCutByAHole || holesInC.length > 0) { if (!diagonalIsCutByAHole) { diagonal = { a: C[0], b: holesInC[0][0], hole: holesInC[0] }; } var _drawTrueDiagonal = drawTrueDiagonal(diagonal, C, holesInC), HPrime = _drawTrueDiagonal.hole, dPrime = _objectWithoutProperties(_drawTrueDiagonal, ["hole"]); trueDiagonals.push(dPrime); // Absorption of H' holes = holes.filter(function (hole) { return hole !== HPrime; }); var vi = C[0]; var id1 = ++idOffset; var id2 = ++idOffset; var rotatedHPrime = rotateLeft(HPrime, HPrime.findIndex(function (v) { return vertexEquality(v, dPrime.b); }) + 1).reverse(); var viIndexInQ = Q.findIndex(function (v) { return vertexEquality(v, vi); }); Q = [].concat(_toConsumableArray(Q.slice(0, viIndexInQ + 1)), _toConsumableArray(rotatedHPrime), [_objectSpread2({}, rotatedHPrime[0], { id: id1, originalId: rotatedHPrime[0].id.originalId || rotatedHPrime[0].id }), _objectSpread2({}, vi, { id: id2, originalId: vi.originalId || vi.id })], _toConsumableArray(Q.slice(viIndexInQ + 1))); } else { LPCP.push(C); getEdges(C).forEach(function (edge) { for (var _i2 = 0; _i2 < LLE.length; _i2++) { var _LLE$_i = LLE[_i2], diagonalA = _LLE$_i.i2, diagonalB = _LLE$_i.j2; if (vertexEqualityAfterAbsorption(diagonalA, edge.b) && vertexEqualityAfterAbsorption(diagonalB, edge.a)) { LLE[_i2].leftPolygon = C; break; } } }); if (end) { return "break"; } LLE.push({ i2: diagonal.b, j2: diagonal.a, rightPolygon: C }); Q = substractPolygons(Q, C); } }; while (true) { var _ret = _loop(); if (_ret === "break") break; } return { LPCP: LPCP, trueDiagonals: trueDiagonals, LLE: LLE }; } /** * An implementation of the algorithm presented in "A practical algorithm for decomposing polygonal domains into convex polygons by diagonals" * It will decompose a polygon (with or without holes) in a partition of convex polygons. * * @param {{ x: number, y: number }[]} polygon * @param {{ x: number, y: number }[][]} holes * @returns {{ x: number, y: number }[][]} The partition of convex polygons */ function absHol(polygon) { var