hefeihvac_java/node_modules/echarts-gl/lib/util/Triangulation.js

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;