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

647 lines (593 loc) 22.9 kB
import robustCompare from 'robust-compare'; import robustCompress from 'robust-compress'; import robustOrientation from 'robust-orientation'; import robustProduct from 'robust-product'; import robustDiff from 'robust-subtract'; /** * Useful to avoid floating point problems. */ export const EPSILON = 0.00000001; let robust = true; export const setRobustness = (bool) => { robust = bool; }; export const getRobustness = () => 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} */ export 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} */ export 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} */ export 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} */ export 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} */ export function isFlat (polygon) { const polygonLength = polygon.length; return polygon.every((point, index) => { const previousPoint = polygon[(index - 1 + polygonLength) % polygonLength]; const 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} */ export function isClockwiseOrdered (polygon) { if (!isSimple(polygon)) { throw new Error('The isClockwiseOrdered method only works with simple polygons'); } const polygonLength = polygon.length; // find bottom right vertex let bottomRightVertexIndex = 0; let bottomRightVertex = polygon[0]; for (let i = 1; i < polygonLength; i++) { const 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}[]} */ export function orderClockwise (polygon) { if (!isClockwiseOrdered(polygon)) { return [...polygon].reverse(); } return [...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} */ export function orientation (point1, point2, point3) { if (robust) { const 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} */ export function sideOfLine (point1, point2, point3) { return orientation(point2, point3, point1); } const VERTEX_CODE = 100; const 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) { const polygonPoint = polygon[0]; if (polygonPoint.x === point.x && polygonPoint.y === point.y) { return VERTEX_CODE; } const polygonLength = polygon.length; let wn = 0; for (let i = 0; i < polygonLength; i++) { const polygonPoint = polygon[i]; const 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 { const 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) { const 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) { const polygonPoint = polygon[0]; if (polygonPoint.x === point.x && polygonPoint.y === point.y) { return VERTEX_CODE; } const polygonLength = polygon.length; let wn = 0; for (let i = 0; i < polygonLength; i++) { const polygonPoint = polygon[i]; const 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) | 0) - 1; wn += nextPolygonPoint.y > polygonPoint.y ? 1 : -1; } else { const det = robustDiff( robustProduct(robustDiff([polygonPoint.x], [point.x]), robustDiff([nextPolygonPoint.y], [point.y])), robustProduct(robustDiff([nextPolygonPoint.x], [point.x]), robustDiff([polygonPoint.y], [point.y])) ); const detComparison = robustCompare(det, [0]); if (detComparison === 0) { return EDGE_CODE; } else if ((detComparison > 0) === (nextPolygonPoint.y > polygonPoint.y)) { // right_crossing // wn += 2 * ((nextPolygonPoint.y > polygonPoint.y) | 0) - 1; wn += nextPolygonPoint.y > polygonPoint.y ? 1 : -1; } } } else { if (nextPolygonPoint.x > point.x) { const det = robustDiff( robustProduct(robustDiff([polygonPoint.x], [point.x]), robustDiff([nextPolygonPoint.y], [point.y])), robustProduct(robustDiff([nextPolygonPoint.x], [point.x]), robustDiff([polygonPoint.y], [point.y])) ); const detComparison = robustCompare(det, [0]); if (detComparison === 0) { return EDGE_CODE; } else if ((detComparison > 0) === (nextPolygonPoint.y > polygonPoint.y)) { // right_crossing // wn += 2 * ((nextPolygonPoint.y > polygonPoint.y) | 0) - 1; wn += nextPolygonPoint.y > polygonPoint.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} */ export 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} */ export function inConvexPolygon (point, convexPolygon) { const polygonLength = convexPolygon.length; return convexPolygon.every((previousPoint, index) => { const 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} */ export function containsPolygon (polygon1, polygon2) { return polygon2.some(vertex => 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} */ export function containsEntirePolygon (polygon1, polygon2) { return polygon2.every(vertex => 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 }} */ export function nextVertex (vertex, polygon) { const polygonLength = polygon.length; const vertexIndex = polygon.findIndex(v => 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 }} */ export function previousVertex (vertex, polygon) { const polygonLength = polygon.length; const vertexIndex = polygon.findIndex(v => 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} */ export function isAVertex (point, polygon) { return polygon.some(v => pointEquality(v, point)); } /** * Checks if a point is a notch of a polygon. * The vertices of a polygon displaying a reflex angle, that is, greater than 180° are called notches. * * @param {{ x: number, y: number }} vertex * @param {{ x: number, y: number, id: number }[]} polygon * @returns {boolean} */ export function isANotch (vertex, polygon) { const polygonLength = polygon.length; const vertexIndex = polygon.findIndex(v => vertexEquality(vertex, v)); return orientation(polygon[(vertexIndex - 1 + polygonLength) % polygonLength], vertex, polygon[(vertexIndex + 1) % polygonLength]) < 0; } /** * Returns all the notches of a given polygon. * * @param {{ x: number, y: number, id: number }[]} polygon * @returns {{ x: number, y: number, id: number }[]} */ export function getNotches (polygon) { const polygonLength = polygon.length; return polygon.filter((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 }}[]} */ export function getEdges (polygon) { const edges = []; const polygonLength = polygon.length; for (let 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)} */ export function nextNotch (vertex, polygon) { const polygonLength = polygon.length; const vertexIndex = polygon.findIndex(v => vertexEquality(vertex, v)); let 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)} */ export function previousNotch (vertex, polygon) { const polygonLength = polygon.length; const vertexIndex = polygon.findIndex(v => vertexEquality(vertex, v)); let 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 }[]} */ export function substractPolygons (polygon1, polygon2) { // const firstIndex = polygon1.findIndex(p => pointEquality(p, polygon2[0])); // const lastIndex = polygon1.findIndex(p => pointEquality(p, polygon2[polygon2.length - 1])); const firstIndex = polygon1.findIndex(p => vertexEquality(p, polygon2[0])); const lastIndex = polygon1.findIndex(p => vertexEquality(p, polygon2[polygon2.length - 1])); if (firstIndex < lastIndex) { return [...polygon1.slice(0, firstIndex), polygon1[firstIndex], ...polygon1.slice(lastIndex)]; } else { return [...polygon1.slice(lastIndex, firstIndex + 1)]; } } /** * @param {{{ x: number, y: number, id: number }[]}} polygon * @return {boolean} */ export function isConvex (polygon) { const polygonLength = polygon.length; return polygon.every((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) { const compressedSequence = robustCompress([...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 }} */ export function robustLineIntersection (line1, line2) { const { a: { x: x1, y: y1 }, b: { x: x2, y: y2 } } = line1; const { a: { x: x3, y: y3 }, b: { x: x4, y: y4 } } = line2; const y4y3 = robustDiff([y4], [y3]); const x2x1 = robustDiff([x2], [x1]); const x4x3 = robustDiff([x4], [x3]); const y2y1 = robustDiff([y2], [y1]); const y1y3 = robustDiff([y1], [y3]); const x1x3 = robustDiff([x1], [x3]); const denom = robustDiff(robustProduct(y4y3, x2x1), robustProduct(x4x3, y2y1)); const robustDenomComparison = robustCompare(denom, [0]); if (robustDenomComparison === 0) { return null; } const ua = robustDiff(robustProduct(x4x3, y1y3), robustProduct(y4y3, x1x3)); const ub = robustDiff(robustProduct(x2x1, y1y3), robustProduct(y2y1, x1x3)); let 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); } const 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 }} */ export function lineIntersection (line1, line2) { if (robust) { return robustLineIntersection(line1, line2); } const { a: { x: x1, y: y1 }, b: { x: x2, y: y2 } } = line1; const { a: { x: x3, y: y3 }, b: { x: x4, y: y4 } } = line2; const denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); if (Math.abs(denom) < EPSILON) { return null; } const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denom; const 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} */ export function isSimple (polygon) { if (polygon.length < 3) { return true; } const segments = []; for (let 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((segment1) => { return segments.some((segment2) => { if (segment1 === segment2) { return false; } const intersection = lineIntersection(segment1, segment2); if (intersection === null) { return false; } const { insideSegment1, insideSegment2 } = intersection; return insideSegment1 && insideSegment2; }); }); }