poly2tri
Version:
A 2D constrained Delaunay triangulation library
835 lines (733 loc) • 25.6 kB
JavaScript
/*
* 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;