UNPKG

poly2tri

Version:

A 2D constrained Delaunay triangulation library

835 lines (733 loc) 25.6 kB
/* * Poly2Tri Copyright (c) 2009-2014, Poly2Tri Contributors * http://code.google.com/p/poly2tri/ * * poly2tri.js (JavaScript port) (c) 2009-2014, Poly2Tri Contributors * https://github.com/r3mi/poly2tri.js * * All rights reserved. * * Distributed under the 3-clause BSD License, see LICENSE.txt */ /* jshint latedef:nofunc, maxcomplexity:9 */ "use strict"; /** * This 'Sweep' module is present in order to keep this JavaScript version * as close as possible to the reference C++ version, even though almost all * functions could be declared as methods on the {@linkcode module:sweepcontext~SweepContext} object. * @module * @private */ /* * Note * ==== * the structure of this JavaScript version of poly2tri intentionally follows * as closely as possible the structure of the reference C++ version, to make it * easier to keep the 2 versions in sync. */ var assert = require('./assert'); var PointError = require('./pointerror'); var Triangle = require('./triangle'); var Node = require('./advancingfront').Node; // ------------------------------------------------------------------------utils var utils = require('./utils'); /** @const */ var EPSILON = utils.EPSILON; /** @const */ var Orientation = utils.Orientation; /** @const */ var orient2d = utils.orient2d; /** @const */ var inScanArea = utils.inScanArea; /** @const */ var isAngleObtuse = utils.isAngleObtuse; // ------------------------------------------------------------------------Sweep /** * Triangulate the polygon with holes and Steiner points. * Do this AFTER you've added the polyline, holes, and Steiner points * @private * @param {!SweepContext} tcx - SweepContext object */ function triangulate(tcx) { tcx.initTriangulation(); tcx.createAdvancingFront(); // Sweep points; build mesh sweepPoints(tcx); // Clean up finalizationPolygon(tcx); } /** * Start sweeping the Y-sorted point set from bottom to top * @param {!SweepContext} tcx - SweepContext object */ function sweepPoints(tcx) { var i, len = tcx.pointCount(); for (i = 1; i < len; ++i) { var point = tcx.getPoint(i); var node = pointEvent(tcx, point); var edges = point._p2t_edge_list; for (var j = 0; edges && j < edges.length; ++j) { edgeEventByEdge(tcx, edges[j], node); } } } /** * @param {!SweepContext} tcx - SweepContext object */ function finalizationPolygon(tcx) { // Get an Internal triangle to start with var t = tcx.front().head().next.triangle; var p = tcx.front().head().next.point; while (!t.getConstrainedEdgeCW(p)) { t = t.neighborCCW(p); } // Collect interior triangles constrained by edges tcx.meshClean(t); } /** * Find closes node to the left of the new point and * create a new triangle. If needed new holes and basins * will be filled to. * @param {!SweepContext} tcx - SweepContext object * @param {!XY} point Point */ function pointEvent(tcx, point) { var node = tcx.locateNode(point); var new_node = newFrontTriangle(tcx, point, node); // Only need to check +epsilon since point never have smaller // x value than node due to how we fetch nodes from the front if (point.x <= node.point.x + (EPSILON)) { fill(tcx, node); } //tcx.AddNode(new_node); fillAdvancingFront(tcx, new_node); return new_node; } function edgeEventByEdge(tcx, edge, node) { tcx.edge_event.constrained_edge = edge; tcx.edge_event.right = (edge.p.x > edge.q.x); if (isEdgeSideOfTriangle(node.triangle, edge.p, edge.q)) { return; } // For now we will do all needed filling // TODO: integrate with flip process might give some better performance // but for now this avoid the issue with cases that needs both flips and fills fillEdgeEvent(tcx, edge, node); edgeEventByPoints(tcx, edge.p, edge.q, node.triangle, edge.q); } function edgeEventByPoints(tcx, ep, eq, triangle, point) { if (isEdgeSideOfTriangle(triangle, ep, eq)) { return; } var p1 = triangle.pointCCW(point); var o1 = orient2d(eq, p1, ep); if (o1 === Orientation.COLLINEAR) { // TODO integrate here changes from C++ version // (C++ repo revision 09880a869095 dated March 8, 2011) throw new PointError('poly2tri EdgeEvent: Collinear not supported!', [eq, p1, ep]); } var p2 = triangle.pointCW(point); var o2 = orient2d(eq, p2, ep); if (o2 === Orientation.COLLINEAR) { // TODO integrate here changes from C++ version // (C++ repo revision 09880a869095 dated March 8, 2011) throw new PointError('poly2tri EdgeEvent: Collinear not supported!', [eq, p2, ep]); } if (o1 === o2) { // Need to decide if we are rotating CW or CCW to get to a triangle // that will cross edge if (o1 === Orientation.CW) { triangle = triangle.neighborCCW(point); } else { triangle = triangle.neighborCW(point); } edgeEventByPoints(tcx, ep, eq, triangle, point); } else { // This triangle crosses constraint so lets flippin start! flipEdgeEvent(tcx, ep, eq, triangle, point); } } function isEdgeSideOfTriangle(triangle, ep, eq) { var index = triangle.edgeIndex(ep, eq); if (index !== -1) { triangle.markConstrainedEdgeByIndex(index); var t = triangle.getNeighbor(index); if (t) { t.markConstrainedEdgeByPoints(ep, eq); } return true; } return false; } /** * Creates a new front triangle and legalize it * @param {!SweepContext} tcx - SweepContext object */ function newFrontTriangle(tcx, point, node) { var triangle = new Triangle(point, node.point, node.next.point); triangle.markNeighbor(node.triangle); tcx.addToMap(triangle); var new_node = new Node(point); new_node.next = node.next; new_node.prev = node; node.next.prev = new_node; node.next = new_node; if (!legalize(tcx, triangle)) { tcx.mapTriangleToNodes(triangle); } return new_node; } /** * Adds a triangle to the advancing front to fill a hole. * @param {!SweepContext} tcx - SweepContext object * @param node - middle node, that is the bottom of the hole */ function fill(tcx, node) { var triangle = new Triangle(node.prev.point, node.point, node.next.point); // TODO: should copy the constrained_edge value from neighbor triangles // for now constrained_edge values are copied during the legalize triangle.markNeighbor(node.prev.triangle); triangle.markNeighbor(node.triangle); tcx.addToMap(triangle); // Update the advancing front node.prev.next = node.next; node.next.prev = node.prev; // If it was legalized the triangle has already been mapped if (!legalize(tcx, triangle)) { tcx.mapTriangleToNodes(triangle); } //tcx.removeNode(node); } /** * Fills holes in the Advancing Front * @param {!SweepContext} tcx - SweepContext object */ function fillAdvancingFront(tcx, n) { // Fill right holes var node = n.next; while (node.next) { // TODO integrate here changes from C++ version // (C++ repo revision acf81f1f1764 dated April 7, 2012) if (isAngleObtuse(node.point, node.next.point, node.prev.point)) { break; } fill(tcx, node); node = node.next; } // Fill left holes node = n.prev; while (node.prev) { // TODO integrate here changes from C++ version // (C++ repo revision acf81f1f1764 dated April 7, 2012) if (isAngleObtuse(node.point, node.next.point, node.prev.point)) { break; } fill(tcx, node); node = node.prev; } // Fill right basins if (n.next && n.next.next) { if (isBasinAngleRight(n)) { fillBasin(tcx, n); } } } /** * The basin angle is decided against the horizontal line [1,0]. * @param {Node} node * @return {boolean} true if angle < 3*π/4 */ function isBasinAngleRight(node) { var ax = node.point.x - node.next.next.point.x; var ay = node.point.y - node.next.next.point.y; assert(ay >= 0, "unordered y"); return (ax >= 0 || Math.abs(ax) < ay); } /** * Returns true if triangle was legalized * @param {!SweepContext} tcx - SweepContext object * @return {boolean} */ function legalize(tcx, t) { // To legalize a triangle we start by finding if any of the three edges // violate the Delaunay condition for (var i = 0; i < 3; ++i) { if (t.delaunay_edge[i]) { continue; } var ot = t.getNeighbor(i); if (ot) { var p = t.getPoint(i); var op = ot.oppositePoint(t, p); var oi = ot.index(op); // If this is a Constrained Edge or a Delaunay Edge(only during recursive legalization) // then we should not try to legalize if (ot.constrained_edge[oi] || ot.delaunay_edge[oi]) { t.constrained_edge[i] = ot.constrained_edge[oi]; continue; } var inside = inCircle(p, t.pointCCW(p), t.pointCW(p), op); if (inside) { // Lets mark this shared edge as Delaunay t.delaunay_edge[i] = true; ot.delaunay_edge[oi] = true; // Lets rotate shared edge one vertex CW to legalize it rotateTrianglePair(t, p, ot, op); // We now got one valid Delaunay Edge shared by two triangles // This gives us 4 new edges to check for Delaunay // Make sure that triangle to node mapping is done only one time for a specific triangle var not_legalized = !legalize(tcx, t); if (not_legalized) { tcx.mapTriangleToNodes(t); } not_legalized = !legalize(tcx, ot); if (not_legalized) { tcx.mapTriangleToNodes(ot); } // Reset the Delaunay edges, since they only are valid Delaunay edges // until we add a new triangle or point. // XXX: need to think about this. Can these edges be tried after we // return to previous recursive level? t.delaunay_edge[i] = false; ot.delaunay_edge[oi] = false; // If triangle have been legalized no need to check the other edges since // the recursive legalization will handles those so we can end here. return true; } } } return false; } /** * <b>Requirement</b>:<br> * 1. a,b and c form a triangle.<br> * 2. a and d is know to be on opposite side of bc<br> * <pre> * a * + * / \ * / \ * b/ \c * +-------+ * / d \ * / \ * </pre> * <b>Fact</b>: d has to be in area B to have a chance to be inside the circle formed by * a,b and c<br> * d is outside B if orient2d(a,b,d) or orient2d(c,a,d) is CW<br> * This preknowledge gives us a way to optimize the incircle test * @param pa - triangle point, opposite d * @param pb - triangle point * @param pc - triangle point * @param pd - point opposite a * @return {boolean} true if d is inside circle, false if on circle edge */ function inCircle(pa, pb, pc, pd) { var adx = pa.x - pd.x; var ady = pa.y - pd.y; var bdx = pb.x - pd.x; var bdy = pb.y - pd.y; var adxbdy = adx * bdy; var bdxady = bdx * ady; var oabd = adxbdy - bdxady; if (oabd <= 0) { return false; } var cdx = pc.x - pd.x; var cdy = pc.y - pd.y; var cdxady = cdx * ady; var adxcdy = adx * cdy; var ocad = cdxady - adxcdy; if (ocad <= 0) { return false; } var bdxcdy = bdx * cdy; var cdxbdy = cdx * bdy; var alift = adx * adx + ady * ady; var blift = bdx * bdx + bdy * bdy; var clift = cdx * cdx + cdy * cdy; var det = alift * (bdxcdy - cdxbdy) + blift * ocad + clift * oabd; return det > 0; } /** * Rotates a triangle pair one vertex CW *<pre> * n2 n2 * P +-----+ P +-----+ * | t /| |\ t | * | / | | \ | * n1| / |n3 n1| \ |n3 * | / | after CW | \ | * |/ oT | | oT \| * +-----+ oP +-----+ * n4 n4 * </pre> */ function rotateTrianglePair(t, p, ot, op) { var n1, n2, n3, n4; n1 = t.neighborCCW(p); n2 = t.neighborCW(p); n3 = ot.neighborCCW(op); n4 = ot.neighborCW(op); var ce1, ce2, ce3, ce4; ce1 = t.getConstrainedEdgeCCW(p); ce2 = t.getConstrainedEdgeCW(p); ce3 = ot.getConstrainedEdgeCCW(op); ce4 = ot.getConstrainedEdgeCW(op); var de1, de2, de3, de4; de1 = t.getDelaunayEdgeCCW(p); de2 = t.getDelaunayEdgeCW(p); de3 = ot.getDelaunayEdgeCCW(op); de4 = ot.getDelaunayEdgeCW(op); t.legalize(p, op); ot.legalize(op, p); // Remap delaunay_edge ot.setDelaunayEdgeCCW(p, de1); t.setDelaunayEdgeCW(p, de2); t.setDelaunayEdgeCCW(op, de3); ot.setDelaunayEdgeCW(op, de4); // Remap constrained_edge ot.setConstrainedEdgeCCW(p, ce1); t.setConstrainedEdgeCW(p, ce2); t.setConstrainedEdgeCCW(op, ce3); ot.setConstrainedEdgeCW(op, ce4); // Remap neighbors // XXX: might optimize the markNeighbor by keeping track of // what side should be assigned to what neighbor after the // rotation. Now mark neighbor does lots of testing to find // the right side. t.clearNeighbors(); ot.clearNeighbors(); if (n1) { ot.markNeighbor(n1); } if (n2) { t.markNeighbor(n2); } if (n3) { t.markNeighbor(n3); } if (n4) { ot.markNeighbor(n4); } t.markNeighbor(ot); } /** * Fills a basin that has formed on the Advancing Front to the right * of given node.<br> * First we decide a left,bottom and right node that forms the * boundaries of the basin. Then we do a reqursive fill. * * @param {!SweepContext} tcx - SweepContext object * @param node - starting node, this or next node will be left node */ function fillBasin(tcx, node) { if (orient2d(node.point, node.next.point, node.next.next.point) === Orientation.CCW) { tcx.basin.left_node = node.next.next; } else { tcx.basin.left_node = node.next; } // Find the bottom and right node tcx.basin.bottom_node = tcx.basin.left_node; while (tcx.basin.bottom_node.next && tcx.basin.bottom_node.point.y >= tcx.basin.bottom_node.next.point.y) { tcx.basin.bottom_node = tcx.basin.bottom_node.next; } if (tcx.basin.bottom_node === tcx.basin.left_node) { // No valid basin return; } tcx.basin.right_node = tcx.basin.bottom_node; while (tcx.basin.right_node.next && tcx.basin.right_node.point.y < tcx.basin.right_node.next.point.y) { tcx.basin.right_node = tcx.basin.right_node.next; } if (tcx.basin.right_node === tcx.basin.bottom_node) { // No valid basins return; } tcx.basin.width = tcx.basin.right_node.point.x - tcx.basin.left_node.point.x; tcx.basin.left_highest = tcx.basin.left_node.point.y > tcx.basin.right_node.point.y; fillBasinReq(tcx, tcx.basin.bottom_node); } /** * Recursive algorithm to fill a Basin with triangles * * @param {!SweepContext} tcx - SweepContext object * @param node - bottom_node */ function fillBasinReq(tcx, node) { // if shallow stop filling if (isShallow(tcx, node)) { return; } fill(tcx, node); var o; if (node.prev === tcx.basin.left_node && node.next === tcx.basin.right_node) { return; } else if (node.prev === tcx.basin.left_node) { o = orient2d(node.point, node.next.point, node.next.next.point); if (o === Orientation.CW) { return; } node = node.next; } else if (node.next === tcx.basin.right_node) { o = orient2d(node.point, node.prev.point, node.prev.prev.point); if (o === Orientation.CCW) { return; } node = node.prev; } else { // Continue with the neighbor node with lowest Y value if (node.prev.point.y < node.next.point.y) { node = node.prev; } else { node = node.next; } } fillBasinReq(tcx, node); } function isShallow(tcx, node) { var height; if (tcx.basin.left_highest) { height = tcx.basin.left_node.point.y - node.point.y; } else { height = tcx.basin.right_node.point.y - node.point.y; } // if shallow stop filling if (tcx.basin.width > height) { return true; } return false; } function fillEdgeEvent(tcx, edge, node) { if (tcx.edge_event.right) { fillRightAboveEdgeEvent(tcx, edge, node); } else { fillLeftAboveEdgeEvent(tcx, edge, node); } } function fillRightAboveEdgeEvent(tcx, edge, node) { while (node.next.point.x < edge.p.x) { // Check if next node is below the edge if (orient2d(edge.q, node.next.point, edge.p) === Orientation.CCW) { fillRightBelowEdgeEvent(tcx, edge, node); } else { node = node.next; } } } function fillRightBelowEdgeEvent(tcx, edge, node) { if (node.point.x < edge.p.x) { if (orient2d(node.point, node.next.point, node.next.next.point) === Orientation.CCW) { // Concave fillRightConcaveEdgeEvent(tcx, edge, node); } else { // Convex fillRightConvexEdgeEvent(tcx, edge, node); // Retry this one fillRightBelowEdgeEvent(tcx, edge, node); } } } function fillRightConcaveEdgeEvent(tcx, edge, node) { fill(tcx, node.next); if (node.next.point !== edge.p) { // Next above or below edge? if (orient2d(edge.q, node.next.point, edge.p) === Orientation.CCW) { // Below if (orient2d(node.point, node.next.point, node.next.next.point) === Orientation.CCW) { // Next is concave fillRightConcaveEdgeEvent(tcx, edge, node); } else { // Next is convex /* jshint noempty:false */ } } } } function fillRightConvexEdgeEvent(tcx, edge, node) { // Next concave or convex? if (orient2d(node.next.point, node.next.next.point, node.next.next.next.point) === Orientation.CCW) { // Concave fillRightConcaveEdgeEvent(tcx, edge, node.next); } else { // Convex // Next above or below edge? if (orient2d(edge.q, node.next.next.point, edge.p) === Orientation.CCW) { // Below fillRightConvexEdgeEvent(tcx, edge, node.next); } else { // Above /* jshint noempty:false */ } } } function fillLeftAboveEdgeEvent(tcx, edge, node) { while (node.prev.point.x > edge.p.x) { // Check if next node is below the edge if (orient2d(edge.q, node.prev.point, edge.p) === Orientation.CW) { fillLeftBelowEdgeEvent(tcx, edge, node); } else { node = node.prev; } } } function fillLeftBelowEdgeEvent(tcx, edge, node) { if (node.point.x > edge.p.x) { if (orient2d(node.point, node.prev.point, node.prev.prev.point) === Orientation.CW) { // Concave fillLeftConcaveEdgeEvent(tcx, edge, node); } else { // Convex fillLeftConvexEdgeEvent(tcx, edge, node); // Retry this one fillLeftBelowEdgeEvent(tcx, edge, node); } } } function fillLeftConvexEdgeEvent(tcx, edge, node) { // Next concave or convex? if (orient2d(node.prev.point, node.prev.prev.point, node.prev.prev.prev.point) === Orientation.CW) { // Concave fillLeftConcaveEdgeEvent(tcx, edge, node.prev); } else { // Convex // Next above or below edge? if (orient2d(edge.q, node.prev.prev.point, edge.p) === Orientation.CW) { // Below fillLeftConvexEdgeEvent(tcx, edge, node.prev); } else { // Above /* jshint noempty:false */ } } } function fillLeftConcaveEdgeEvent(tcx, edge, node) { fill(tcx, node.prev); if (node.prev.point !== edge.p) { // Next above or below edge? if (orient2d(edge.q, node.prev.point, edge.p) === Orientation.CW) { // Below if (orient2d(node.point, node.prev.point, node.prev.prev.point) === Orientation.CW) { // Next is concave fillLeftConcaveEdgeEvent(tcx, edge, node); } else { // Next is convex /* jshint noempty:false */ } } } } function flipEdgeEvent(tcx, ep, eq, t, p) { var ot = t.neighborAcross(p); assert(ot, "FLIP failed due to missing triangle!"); var op = ot.oppositePoint(t, p); // Additional check from Java version (see issue #88) if (t.getConstrainedEdgeAcross(p)) { var index = t.index(p); throw new PointError("poly2tri Intersecting Constraints", [p, op, t.getPoint((index + 1) % 3), t.getPoint((index + 2) % 3)]); } if (inScanArea(p, t.pointCCW(p), t.pointCW(p), op)) { // Lets rotate shared edge one vertex CW rotateTrianglePair(t, p, ot, op); tcx.mapTriangleToNodes(t); tcx.mapTriangleToNodes(ot); // XXX: in the original C++ code for the next 2 lines, we are // comparing point values (and not pointers). In this JavaScript // code, we are comparing point references (pointers). This works // because we can't have 2 different points with the same values. // But to be really equivalent, we should use "Point.equals" here. if (p === eq && op === ep) { if (eq === tcx.edge_event.constrained_edge.q && ep === tcx.edge_event.constrained_edge.p) { t.markConstrainedEdgeByPoints(ep, eq); ot.markConstrainedEdgeByPoints(ep, eq); legalize(tcx, t); legalize(tcx, ot); } else { // XXX: I think one of the triangles should be legalized here? /* jshint noempty:false */ } } else { var o = orient2d(eq, op, ep); t = nextFlipTriangle(tcx, o, t, ot, p, op); flipEdgeEvent(tcx, ep, eq, t, p); } } else { var newP = nextFlipPoint(ep, eq, ot, op); flipScanEdgeEvent(tcx, ep, eq, t, ot, newP); edgeEventByPoints(tcx, ep, eq, t, p); } } /** * After a flip we have two triangles and know that only one will still be * intersecting the edge. So decide which to contiune with and legalize the other * * @param {!SweepContext} tcx - SweepContext object * @param o - should be the result of an orient2d( eq, op, ep ) * @param t - triangle 1 * @param ot - triangle 2 * @param p - a point shared by both triangles * @param op - another point shared by both triangles * @return returns the triangle still intersecting the edge */ function nextFlipTriangle(tcx, o, t, ot, p, op) { var edge_index; if (o === Orientation.CCW) { // ot is not crossing edge after flip edge_index = ot.edgeIndex(p, op); ot.delaunay_edge[edge_index] = true; legalize(tcx, ot); ot.clearDelaunayEdges(); return t; } // t is not crossing edge after flip edge_index = t.edgeIndex(p, op); t.delaunay_edge[edge_index] = true; legalize(tcx, t); t.clearDelaunayEdges(); return ot; } /** * When we need to traverse from one triangle to the next we need * the point in current triangle that is the opposite point to the next * triangle. */ function nextFlipPoint(ep, eq, ot, op) { var o2d = orient2d(eq, op, ep); if (o2d === Orientation.CW) { // Right return ot.pointCCW(op); } else if (o2d === Orientation.CCW) { // Left return ot.pointCW(op); } else { throw new PointError("poly2tri [Unsupported] nextFlipPoint: opposing point on constrained edge!", [eq, op, ep]); } } /** * Scan part of the FlipScan algorithm<br> * When a triangle pair isn't flippable we will scan for the next * point that is inside the flip triangle scan area. When found * we generate a new flipEdgeEvent * * @param {!SweepContext} tcx - SweepContext object * @param ep - last point on the edge we are traversing * @param eq - first point on the edge we are traversing * @param {!Triangle} flip_triangle - the current triangle sharing the point eq with edge * @param t * @param p */ function flipScanEdgeEvent(tcx, ep, eq, flip_triangle, t, p) { var ot = t.neighborAcross(p); assert(ot, "FLIP failed due to missing triangle"); var op = ot.oppositePoint(t, p); if (inScanArea(eq, flip_triangle.pointCCW(eq), flip_triangle.pointCW(eq), op)) { // flip with new edge op.eq flipEdgeEvent(tcx, eq, op, ot, op); } else { var newP = nextFlipPoint(ep, eq, ot, op); flipScanEdgeEvent(tcx, ep, eq, flip_triangle, ot, newP); } } // ----------------------------------------------------------------------Exports exports.triangulate = triangulate;