// Ear clipping polygon triangulation. // https://www.geometrictools.com/Documentation/TriangulationByEarClipping.pdf // http://www.cosy.sbg.ac.at/~held/projects/triang/triang.html // Z Order Hash ? import LinkedList from 'claygl/src/core/LinkedList'; // From x,y point cast a ray to right. and intersect with edge x0, y0, x1, y1; // Return x value of intersect point function intersectEdge(x0, y0, x1, y1, x, y) { if (y > y0 && y > y1 || y < y0 && y < y1) { return -Infinity; } // Ignore horizontal line if (y1 === y0) { return -Infinity; } var dir = y1 < y0 ? 1 : -1; var t = (y - y0) / (y1 - y0); // Avoid winding error when intersection point is the connect point of two line of polygon if (t === 1 || t === 0) { dir = y1 < y0 ? 0.5 : -0.5; } var x_ = t * (x1 - x0) + x0; return x_; } ; function triangleArea(x0, y0, x1, y1, x2, y2) { return (x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1); } function isPointInTriangle(x0, y0, x1, y1, x2, y2, xi, yi) { return !(triangleArea(x0, y0, x2, y2, xi, yi) <= 0 || triangleArea(x0, y0, xi, yi, x1, y1) <= 0 || triangleArea(xi, yi, x2, y2, x1, y1) <= 0); } function area(points) { // Signed polygon area var n = points.length / 2; if (n < 3) { return 0; } var area = 0; for (var i = (n - 1) * 2, j = 0; j < n * 2;) { var x0 = points[i]; var y0 = points[i + 1]; var x1 = points[j]; var y1 = points[j + 1]; i = j; j += 2; area += x0 * y1 - x1 * y0; } return area; } function reverse(points, stride) { var n = points.length / stride; for (var i = 0; i < Math.floor(n / 2); i++) { for (var j = 0; j < stride; j++) { var a = i * stride + j; var b = (n - i - 1) * stride + j; var tmp = points[a]; points[a] = points[b]; points[b] = tmp; } } return points; } var VERTEX_TYPE_CONVEX = 1; var VERTEX_TYPE_REFLEX = 2; var VERTEX_COUNT_NEEDS_GRID = 50; function Point(idx) { this.idx = idx; } var TriangulationContext = function () { this.points = []; this.triangles = []; this.maxGridNumber = 50; this.minGridNumber = 4; this._gridNumber = 20; this._boundingBox = [[Infinity, Infinity], [-Infinity, -Infinity]]; this._nPoints = 0; this._pointsTypes = []; this._grids = []; this._gridWidth = 0; this._gridHeight = 0; this._candidates = null; }; /** * @param {Array.} exterior. Exterior points * exterior should be clockwise order. (When y is from bottom to top) * @param {Array.} holes. holes should be counter clockwise order. */ TriangulationContext.prototype.triangulate = function (exterior, holes) { this._nPoints = exterior.length / 2; if (this._nPoints < 3) { return; } // PENDING Dynamic grid number or fixed grid number ? this._gridNumber = Math.ceil(Math.sqrt(this._nPoints) / 2); this._gridNumber = Math.max(Math.min(this._gridNumber, this.maxGridNumber), this.minGridNumber); this.points = exterior; this._needsGreed = this._nPoints > VERTEX_COUNT_NEEDS_GRID; if (area(this.points) > 0) { // Don't konw why, but use slice is more faster than new Float32Array(this.points). this.points = this.points.slice(); reverse(this.points, 2); } this.holes = (holes || []).map(function (hole) { if (area(hole) < 0) { hole = hole.slice(); reverse(hole, 2); } return hole; }); this._reset(); this._prepare(); this._earClipping(); }; TriangulationContext.prototype._reset = function () { this._candidates = new LinkedList(); this.triangles = []; this._boundingBox[0][0] = this._boundingBox[0][1] = Infinity; this._boundingBox[1][0] = this._boundingBox[1][1] = -Infinity; // Initialize grid var nGrids = this._gridNumber * this._gridNumber; for (var i = 0; i < nGrids; i++) { this._grids[i] = []; } this._grids.length = nGrids; }; // Prepare points TriangulationContext.prototype._prepare = function () { var bb = this._boundingBox; var n = this._nPoints; var points = this.points; this._pointsTypes = []; // Update bounding box and determine point type is reflex or convex for (var i = 0, j = n - 1; i < n;) { var k = (i + 1) % n; var x0 = points[j * 2]; var y0 = points[j * 2 + 1]; var x1 = points[i * 2]; var y1 = points[i * 2 + 1]; var x2 = points[k * 2]; var y2 = points[k * 2 + 1]; if (this._needsGreed) { if (x1 < bb[0][0]) { bb[0][0] = x1; } if (y1 < bb[0][1]) { bb[0][1] = y1; } if (x1 > bb[1][0]) { bb[1][0] = x1; } if (y1 > bb[1][1]) { bb[1][1] = y1; } // Make the bounding box a litte bigger // Avoid the geometry hashing will touching the bound of the bounding box bb[0][0] -= 0.1; bb[0][1] -= 0.1; bb[1][0] += 0.1; bb[1][1] += 0.1; } var area = triangleArea(x0, y0, x1, y1, x2, y2); // Including 0. this._pointsTypes[i] = area <= 0 ? VERTEX_TYPE_CONVEX : VERTEX_TYPE_REFLEX; j = i; i++; } this._cutHoles(); // points may be changed after cutHoles. n = this._nPoints; points = this.points; // Init candidates. for (var i = 0; i < n; i++) { this._candidates.insert(new Point(i)); } // Put the points in the grids if (this._needsGreed) { this._gridWidth = (bb[1][0] - bb[0][0]) / this._gridNumber; this._gridHeight = (bb[1][1] - bb[0][1]) / this._gridNumber; for (var i = 0; i < n; i++) { var x = points[i * 2]; var y = points[i * 2 + 1]; if (this._pointsTypes[i] == VERTEX_TYPE_REFLEX) { var key = this._getPointHash(x, y); this._grids[key].push(i); } } } }; // Finding Mutually Visible Vertices and cut the polygon to remove holes. TriangulationContext.prototype._cutHoles = function () { var holes = this.holes; if (!holes.length) { return; } holes = holes.slice(); var xMaxOfHoles = []; var xMaxIndicesOfHoles = []; for (var i = 0; i < holes.length; i++) { var hole = holes[i]; var holeMaxX = -Infinity; var holeMaxXIndex = 0; // Find index of xMax in the hole. for (var k = 0; k < hole.length; k += 2) { var x = hole[k * 2]; if (x > holeMaxX) { holeMaxXIndex = k / 2; holeMaxX = x; } } xMaxOfHoles.push(holeMaxX); xMaxIndicesOfHoles.push(holeMaxXIndex); } var self = this; function cutHole() { var points = self.points; var nPoints = self._nPoints; var holeMaxX = -Infinity; var holeMaxXIndex = 0; var holeIndex = 0; // Find hole which xMax is rightest for (var i = 0; i < xMaxOfHoles.length; i++) { if (xMaxOfHoles[i] > holeMaxX) { holeMaxX = xMaxOfHoles[i]; holeMaxXIndex = xMaxIndicesOfHoles[i]; holeIndex = i; } } var holePoints = holes[holeIndex]; xMaxOfHoles.splice(holeIndex, 1); xMaxIndicesOfHoles.splice(holeIndex, 1); holes.splice(holeIndex, 1); var holePointX = holePoints[holeMaxXIndex * 2]; var holePointY = holePoints[holeMaxXIndex * 2 + 1]; var minRayX = Infinity; var edgeStartPointIndex = -1; // Find nearest intersected line for (var i = 0, j = points.length - 2; i < points.length; i += 2) { var x0 = points[j], y0 = points[j + 1]; var x1 = points[i], y1 = points[i + 1]; var rayX = intersectEdge(x0, y0, x1, y1, holePointX, holePointY); if (rayX >= holePointX) { // Intersected. if (rayX < minRayX) { minRayX = rayX; edgeStartPointIndex = j / 2; } } j = i; } // Didn't find if (edgeStartPointIndex < 0) { if (process.env.NODE_ENV !== 'production') { console.warn('Hole must be inside exterior.'); } return; } var edgeEndPointIndex = (edgeStartPointIndex + 1) % (points.length / 2); // Point of seam edge/ var seamPointIndex = points[edgeStartPointIndex * 2] > points[edgeEndPointIndex * 2] ? edgeStartPointIndex : edgeEndPointIndex; // Use maximum x of edge var seamX = points[seamPointIndex * 2]; var seamY = points[seamPointIndex * 2 + 1]; var minimumAngleCos = Infinity; // And figure out if any of reflex points is in the triangle, // if has, use the reflex point with minimum angle with (1, 0) for (var i = 0; i < nPoints; i++) { if (self._pointsTypes[i] === VERTEX_TYPE_REFLEX) { var xi = points[i * 2]; var yi = points[i * 2 + 1]; if (isPointInTriangle(holePointX, holePointY, minRayX, holePointY, seamX, seamY, xi, yi)) { // Use dot product with (1, 0) as angle var dx = xi - holePointX; var dy = yi - holePointY; var len = Math.sqrt(dx * dx + dy * dy); dx /= len; dy /= len; var angleCos = dx * dx; if (angleCos < minimumAngleCos) { minimumAngleCos = angleCos; // Replaced seam. seamPointIndex = i; } } } } // TODO Use splice to add maybe slow var newPointsCount = nPoints + holePoints.length / 2 + 2; var newPoints = new Float32Array(newPointsCount * 2); var newPointsTypes = new Uint8Array(newPointsCount); seamX = points[seamPointIndex * 2]; seamY = points[seamPointIndex * 2 + 1]; var offPt = 0; var offType = 0; // x, y, prevX, prevY, nextX, nextY is used for point type. var x, y; var prevX, prevY, nextX, nextY; function copyPoints(idx, source) { prevX = x; prevY = y; x = newPoints[offPt++] = source[idx * 2]; y = newPoints[offPt++] = source[idx * 2 + 1]; } function guessAndAddPointType() { var type = triangleArea(prevX, prevY, x, y, nextX, nextY) < 0 ? VERTEX_TYPE_CONVEX : VERTEX_TYPE_REFLEX; newPointsTypes[offType++] = type; } for (var i = 0; i < seamPointIndex; i++) { copyPoints(i, points); newPointsTypes[offType++] = self._pointsTypes[i]; } copyPoints(seamPointIndex, points); if (0 === seamPointIndex) { // In case first point is seam. prevX = points[nPoints * 2 - 2]; prevY = points[nPoints * 2 - 1]; } nextX = holePoints[holeMaxXIndex * 2]; nextY = holePoints[holeMaxXIndex * 2 + 1]; guessAndAddPointType(); // Add hole for (var i = 0, holePointsCount = holePoints.length / 2; i < holePointsCount; i++) { var idx = (i + holeMaxXIndex) % holePointsCount; copyPoints(idx, holePoints); var nextIdx = (idx + 1) % holePointsCount; nextX = holePoints[nextIdx * 2]; nextY = holePoints[nextIdx * 2 + 1]; guessAndAddPointType(); } // Add another seam. copyPoints(holeMaxXIndex, holePoints); nextX = seamX; nextY = seamY; guessAndAddPointType(); copyPoints(seamPointIndex, points); var nextIdx = (seamPointIndex + 1) % nPoints; nextX = points[nextIdx * 2]; nextY = points[nextIdx * 2 + 1]; guessAndAddPointType(); // Add rest for (var i = seamPointIndex + 1; i < nPoints; i++) { copyPoints(i, points); newPointsTypes[offType++] = self._pointsTypes[i]; } // Update points and pointsTypes self.points = newPoints; self._pointsTypes = newPointsTypes; self._nPoints = newPointsCount; } var count = holes.length; while (count--) { cutHole(); } }; TriangulationContext.prototype._earClipping = function () { var candidates = this._candidates; while (candidates.length() > 2) { var isDesperate = true; var entry = candidates.head; while (entry && candidates.length() > 2) { if (this._isEar(entry)) { entry = this._clipEar(entry); isDesperate = false; } else { entry = entry.next; } } if (isDesperate && candidates.length() > 2) { // var entry = candidates.head; // console.log('------'); // while (entry) { // var idx = entry.value.idx; // var xi = this.points[idx * 2]; // var yi = this.points[idx * 2 + 1]; // console.log([xi, yi]); // entry = entry.next; // } // Random pick a convex vertex when there is no more ear // can be clipped and there are more than 3 points left // After clip the random picked vertex, go on finding ears again // So it can be extremely slow in worst case // TODO this._clipEar(candidates.head); } } }; TriangulationContext.prototype._isEar = function (pointEntry) { if (this._pointsTypes[pointEntry.value.idx] === VERTEX_TYPE_REFLEX) { return; } var points = this.points; var prevPointEntry = pointEntry.prev || this._candidates.tail; var nextPointEntry = pointEntry.next || this._candidates.head; var p0 = prevPointEntry.value.idx; var p1 = pointEntry.value.idx; var p2 = nextPointEntry.value.idx; p0 *= 2; p1 *= 2; p2 *= 2; var x0 = points[p0]; var y0 = points[p0 + 1]; var x1 = points[p1]; var y1 = points[p1 + 1]; var x2 = points[p2]; var y2 = points[p2 + 1]; // Clipped the tiny triangles directly if (Math.abs(triangleArea(x0, y0, x1, y1, x2, y2)) < Number.EPSILON) { return true; } if (this._needsGreed) { var range = this._getTriangleGrids(x0, y0, x1, y1, x2, y2); // Find all the points in the grids covered by the triangle // And figure out if any of them is in the triangle for (var j = range[0][1]; j <= range[1][1]; j++) { for (var i = range[0][0]; i <= range[1][0]; i++) { var gridIdx = j * this._gridNumber + i; var gridPoints = this._grids[gridIdx]; for (var k = 0; k < gridPoints.length; k++) { var idx = gridPoints[k]; if (this._pointsTypes[idx] == VERTEX_TYPE_REFLEX) { var xi = points[idx * 2]; var yi = points[idx * 2 + 1]; if (isPointInTriangle(x0, y0, x1, y1, x2, y2, xi, yi)) { return false; } } } } } } else { var entry = this._candidates.head; while (entry) { var idx = entry.value.idx; var xi = points[idx * 2]; var yi = points[idx * 2 + 1]; if (this._pointsTypes[idx] == VERTEX_TYPE_REFLEX) { if (isPointInTriangle(x0, y0, x1, y1, x2, y2, xi, yi)) { return false; } } entry = entry.next; } } return true; }; TriangulationContext.prototype._clipEar = function (pointEntry) { var candidates = this._candidates; var prevPointEntry = pointEntry.prev || candidates.tail; var nextPointEntry = pointEntry.next || candidates.head; var p0 = prevPointEntry.value.idx; var p1 = pointEntry.value.idx; var p2 = nextPointEntry.value.idx; var triangles = this.triangles; // FIXME e0 may same with e1 triangles.push(p0); triangles.push(p1); triangles.push(p2); // PENDING // The index in the grids also needs to be removed // But because it needs `splice` and `indexOf` // may cost too much candidates.remove(pointEntry); if (candidates.length() === 3) { triangles.push(p0); triangles.push(p2); triangles.push((nextPointEntry.next || candidates.head).value.idx); return; } var nextNextPointEntry = nextPointEntry.next || candidates.head; var prevPrevPointEntry = prevPointEntry.prev || candidates.tail; var p0 = prevPrevPointEntry.value.idx; var p1 = prevPointEntry.value.idx; var p2 = nextPointEntry.value.idx; var p3 = nextNextPointEntry.value.idx; // Update p1, p2, vertex type. // New candidate after clipping (convex vertex) this._pointsTypes[p1] = this.isTriangleConvex2(p0, p1, p2) ? VERTEX_TYPE_CONVEX : VERTEX_TYPE_REFLEX; this._pointsTypes[p2] = this.isTriangleConvex2(p1, p2, p3) ? VERTEX_TYPE_CONVEX : VERTEX_TYPE_REFLEX; return prevPointEntry; }; // Get geometric hash of point // Actually it will find the grid index by giving the point (x y) TriangulationContext.prototype._getPointHash = function (x, y) { var bb = this._boundingBox; return Math.floor((y - bb[0][1]) / this._gridHeight) * this._gridNumber + Math.floor((x - bb[0][0]) / this._gridWidth); }; // Get the grid range covered by the triangle TriangulationContext.prototype._getTriangleGrids = function () { var range = [[-1, -1], [-1, -1]]; var minX, minY, maxX, maxY; return function (x0, y0, x1, y1, x2, y2) { var bb = this._boundingBox; minX = maxX = x0; minY = maxY = y0; if (x1 < minX) { minX = x1; } if (y1 < minY) { minY = y1; } if (x1 > maxX) { maxX = x1; } if (y1 > maxY) { maxY = y1; } if (x2 < minX) { minX = x2; } if (y2 < minY) { minY = y2; } if (x2 > maxX) { maxX = x2; } if (y2 > maxY) { maxY = y2; } range[0][0] = Math.floor((minX - bb[0][0]) / this._gridWidth); range[1][0] = Math.floor((maxX - bb[0][0]) / this._gridWidth); range[0][1] = Math.floor((minY - bb[0][1]) / this._gridHeight); range[1][1] = Math.floor((maxY - bb[0][1]) / this._gridHeight); return range; }; }(); TriangulationContext.prototype.isTriangleConvex2 = function (p0, p1, p2) { // Including 0 return this.triangleArea(p0, p1, p2) <= 0; }; TriangulationContext.prototype.triangleArea = function (p0, p1, p2) { var x0 = this.points[p0 * 2]; var y0 = this.points[p0 * 2 + 1]; var x1 = this.points[p1 * 2]; var y1 = this.points[p1 * 2 + 1]; var x2 = this.points[p2 * 2]; var y2 = this.points[p2 * 2 + 1]; return (x1 - x0) * (y2 - y1) - (y1 - y0) * (x2 - x1); }; export default TriangulationContext;