642 lines
17 KiB
JavaScript
642 lines
17 KiB
JavaScript
|
|
// 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.<number>} exterior. Exterior points
|
||
|
|
* exterior should be clockwise order. (When y is from bottom to top)
|
||
|
|
* @param {Array.<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;
|