Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Touching inner rings #177

Merged
merged 9 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion bench/basic.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {earcut, flatten} from '../src/earcut.js';
import earcut, {flatten} from '../src/earcut.js';
import {readFileSync} from 'fs';

const data = JSON.parse(readFileSync(new URL('../test/fixtures/building.json', import.meta.url)));
Expand Down
2 changes: 1 addition & 1 deletion bench/bench.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {earcut, flatten} from '../src/earcut.js';
import earcut, {flatten} from '../src/earcut.js';
import Benchmark from 'benchmark';
import {readFileSync} from 'fs';

Expand Down
57 changes: 38 additions & 19 deletions src/earcut.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,16 +140,16 @@ function isEar(ear) {
// now make sure we don't have other points inside the potential ear
const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

// triangle bbox; min & max are calculated like this for speed
const x0 = ax < bx ? (ax < cx ? ax : cx) : (bx < cx ? bx : cx),
y0 = ay < by ? (ay < cy ? ay : cy) : (by < cy ? by : cy),
x1 = ax > bx ? (ax > cx ? ax : cx) : (bx > cx ? bx : cx),
y1 = ay > by ? (ay > cy ? ay : cy) : (by > cy ? by : cy);
// triangle bbox
const x0 = Math.min(ax, bx, cx),
y0 = Math.min(ay, by, cy),
x1 = Math.max(ax, bx, cx),
y1 = Math.max(ay, by, cy);

let p = c.next;
while (p !== a) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
area(p.prev, p, p.next) >= 0) return false;
p = p.next;
}
Expand All @@ -166,11 +166,11 @@ function isEarHashed(ear, minX, minY, invSize) {

const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;

// triangle bbox; min & max are calculated like this for speed
const x0 = ax < bx ? (ax < cx ? ax : cx) : (bx < cx ? bx : cx),
y0 = ay < by ? (ay < cy ? ay : cy) : (by < cy ? by : cy),
x1 = ax > bx ? (ax > cx ? ax : cx) : (bx > cx ? bx : cx),
y1 = ay > by ? (ay > cy ? ay : cy) : (by > cy ? by : cy);
// triangle bbox
const x0 = Math.min(ax, bx, cx),
y0 = Math.min(ay, by, cy),
x1 = Math.max(ax, bx, cx),
y1 = Math.max(ay, by, cy);

// z-order range for the current triangle bbox;
const minZ = zOrder(x0, y0, minX, minY, invSize),
Expand All @@ -182,25 +182,25 @@ function isEarHashed(ear, minX, minY, invSize) {
// look for points inside the triangle in both directions
while (p && p.z >= minZ && n && n.z <= maxZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;

if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangle(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}

// look for remaining points in decreasing z-order
while (p && p.z >= minZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
}

// look for remaining points in increasing z-order
while (n && n.z <= maxZ) {
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangle(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}

Expand Down Expand Up @@ -268,7 +268,7 @@ function eliminateHoles(data, holeIndices, outerNode, dim) {
queue.push(getLeftmost(list));
}

queue.sort(compareX);
queue.sort(compareXYSlope);

// process holes from left to right
for (let i = 0; i < queue.length; i++) {
Expand All @@ -278,8 +278,19 @@ function eliminateHoles(data, holeIndices, outerNode, dim) {
return outerNode;
}

function compareX(a, b) {
return a.x - b.x;
function compareXYSlope(a, b) {
let result = a.x - b.x;
// when the left-most point of 2 holes meet at a vertex, sort the holes counterclockwise so that when we find
// the bridge to the outer shell is always the point that they meet at.
if (result === 0) {
result = a.y - b.y;
if (result === 0) {
const aSlope = (a.next.y - a.y) / (a.next.x - a.x);
const bSlope = (b.next.y - b.y) / (b.next.x - b.x);
result = aSlope - bSlope;
}
}
return result;
}

// find a bridge between vertices that connects hole with an outer ring and and link it
Expand All @@ -306,8 +317,11 @@ function findHoleBridge(hole, outerNode) {

// find a segment intersected by a ray from the hole's leftmost point to the left;
// segment's endpoint with lesser x will be potential connection point
// unless they intersect at a vertex, then choose the vertex
if (equals(hole, p)) return p;
do {
if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
if (equals(hole, p.next)) return p.next;
else if (hy <= p.y && hy >= p.next.y && p.next.y !== p.y) {
const x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y);
if (x <= hx && x > qx) {
qx = x;
Expand Down Expand Up @@ -463,6 +477,11 @@ function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
(bx - px) * (cy - py) >= (cx - px) * (by - py);
}

// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}

// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
function isValidDiagonal(a, b) {
return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
Expand Down
27 changes: 19 additions & 8 deletions test/expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"water3": 197,
"water3b": 25,
"water4": 705,
"water-huge": 5177,
"water-huge": 5176,
"water-huge2": 4462,
"degenerate": 0,
"bad-hole": 42,
Expand All @@ -22,6 +22,11 @@
"outside-ring": 64,
"simplified-us-border": 120,
"touching-holes": 57,
"touching-holes2": 10,
"touching-holes3": 82,
"touching-holes4": 55,
"touching-holes5": 133,
"touching-holes6": 3098,
"hole-touching-outer": 77,
"hilbert": 1024,
"issue45": 10,
Expand All @@ -32,32 +37,38 @@
"bad-diagonals": 7,
"issue83": 0,
"issue107": 0,
"issue111": 19,
"boxy": 57,
"issue111": 18,
"boxy": 58,
"collinear-diagonal": 14,
"issue119": 18,
"hourglass": 2,
"touching2": 8,
"touching3": 15,
"touching4": 20,
"touching4": 19,
"rain": 2681,
"issue131": 12,
"infinite-loop-jhl" : 0,
"filtered-bridge-jhl" : 25,
"infinite-loop-jhl": 0,
"filtered-bridge-jhl": 25,
"issue149": 2,
"issue142": 4
},
"errors": {
"dude": 2e-15,
"water": 0.0008,
"water-huge": 0.0011,
"water-huge2": 0.0028,
"water-huge2": 0.004,
"bad-hole": 0.019,
"issue16": 4e-16,
"issue17": 2e-16,
"issue29": 2e-15,
"self-touching": 2e-13,
"eberly-6": 2e-14,
"issue142": 0.13
},
"errors-with-rotation": {
"water-huge": 0.0035,
"water-huge2": 0.061,
"bad-hole": 0.04,
"issue16": 8e-16
}
}
}
1 change: 1 addition & 0 deletions test/fixtures/touching-holes2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[0,0],[20,0],[20,25],[0,25],[0,0]],[[3,3],[2,12],[9,15],[3,3]],[[9,21],[2,12],[7,22],[9,21]]]
1 change: 1 addition & 0 deletions test/fixtures/touching-holes3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[0,0],[20,0],[20,25],[0,25],[0,0]],[[2,12],[4,23],[5,23],[2,12]],[[2,12],[6,23],[7,23],[2,12]],[[2,12],[8,23],[9,23],[2,12]],[[2,12],[10,23],[11,23],[2,12]],[[2,12],[12,23],[13,23],[2,12]],[[2,12],[14,23],[15,23],[2,12]],[[2,12],[16,23],[17,23],[2,12]],[[2,12],[18,23],[18,22],[2,12]],[[2,12],[18,21],[18,20],[2,12]],[[2,12],[18,19],[18,18],[2,12]],[[2,12],[18,17],[18,16],[2,12]],[[2,12],[18,15],[18,14],[2,12]],[[2,12],[18,13],[18,12],[2,12]],[[2,12],[18,11],[18,10],[2,12]],[[2,12],[18,9],[18,8],[2,12]],[[2,12],[18,7],[18,6],[2,12]],[[2,12],[18,5],[18,4],[2,12]],[[2,12],[18,3],[18,2],[2,12]],[[2,12],[18,1],[17,1],[2,12]],[[2,12],[16,1],[15,1],[2,12]],[[2,12],[14,1],[13,1],[2,12]],[[2,12],[12,1],[11,1],[2,12]],[[2,12],[10,1],[9,1],[2,12]],[[2,12],[8,1],[7,1],[2,12]],[[2,12],[6,1],[5,1],[2,12]],[[2,12],[4,1],[3,1],[2,12]]]
1 change: 1 addition & 0 deletions test/fixtures/touching-holes4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[-20,0],[20,0],[20,25],[-20,25],[-20,0]],[[2,12],[-1,23],[0,23],[2,12]],[[2,12],[-3,23],[-2,23],[2,12]],[[2,12],[-5,23],[-4,23],[2,12]],[[2,12],[-7,23],[-6,23],[2,12]],[[2,12],[-9,23],[-8,23],[2,12]],[[2,12],[-11,23],[-10,23],[2,12]],[[2,12],[-13,23],[-12,23],[2,12]],[[2,12],[-14,22],[-14,23],[2,12]],[[2,12],[-14,20],[-14,21],[2,12]],[[2,12],[-14,18],[-14,19],[2,12]],[[2,12],[-14,16],[-14,17],[2,12]],[[2,12],[-14,14],[-14,15],[2,12]],[[2,12],[-14,12],[-14,13],[2,12]],[[2,12],[-14,10],[-14,11],[2,12]],[[2,12],[-14,8],[-14,9],[2,12]],[[2,12],[-14,6],[-14,7],[2,12]],[[2,12],[-14,4],[-14,5],[2,12]],[[2,12],[-14,2],[-14,3],[2,12]],[[2,12],[-13,1],[-14,1],[2,12]],[[2,12],[-11,1],[-12,1],[2,12]],[[2,12],[-9,1],[-10,1],[2,12]],[[2,12],[-7,1],[-8,1],[2,12]],[[2,12],[-5,1],[-6,1],[2,12]],[[2,12],[-3,1],[-4,1],[2,12]],[[2,12],[-1,1],[-2,1],[2,12]],[[2,12],[1,1],[0,1],[2,12]]]
1 change: 1 addition & 0 deletions test/fixtures/touching-holes5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[[[-20,0],[20,0],[20,25],[-20,25],[-20,0]],[[2,12],[4,23],[5,23],[2,12]],[[2,12],[6,23],[7,23],[2,12]],[[2,12],[8,23],[9,23],[2,12]],[[2,12],[10,23],[11,23],[2,12]],[[2,12],[12,23],[13,23],[2,12]],[[2,12],[14,23],[15,23],[2,12]],[[2,12],[16,23],[17,23],[2,12]],[[2,12],[18,23],[18,22],[2,12]],[[2,12],[18,21],[18,20],[2,12]],[[2,12],[18,19],[18,18],[2,12]],[[2,12],[18,17],[18,16],[2,12]],[[2,12],[18,15],[18,14],[2,12]],[[2,12],[18,13],[18,12],[2,12]],[[2,12],[18,11],[18,10],[2,12]],[[2,12],[18,9],[18,8],[2,12]],[[2,12],[18,7],[18,6],[2,12]],[[2,12],[18,5],[18,4],[2,12]],[[2,12],[18,3],[18,2],[2,12]],[[2,12],[18,1],[17,1],[2,12]],[[2,12],[16,1],[15,1],[2,12]],[[2,12],[14,1],[13,1],[2,12]],[[2,12],[12,1],[11,1],[2,12]],[[2,12],[10,1],[9,1],[2,12]],[[2,12],[8,1],[7,1],[2,12]],[[2,12],[6,1],[5,1],[2,12]],[[2,12],[4,1],[3,1],[2,12]],[[2,12],[-1,23],[0,23],[2,12]],[[2,12],[-3,23],[-2,23],[2,12]],[[2,12],[-5,23],[-4,23],[2,12]],[[2,12],[-7,23],[-6,23],[2,12]],[[2,12],[-9,23],[-8,23],[2,12]],[[2,12],[-11,23],[-10,23],[2,12]],[[2,12],[-13,23],[-12,23],[2,12]],[[2,12],[-14,22],[-14,23],[2,12]],[[2,12],[-14,20],[-14,21],[2,12]],[[2,12],[-14,18],[-14,19],[2,12]],[[2,12],[-14,16],[-14,17],[2,12]],[[2,12],[-14,14],[-14,15],[2,12]],[[2,12],[-14,12],[-14,13],[2,12]],[[2,12],[-14,10],[-14,11],[2,12]],[[2,12],[-14,8],[-14,9],[2,12]],[[2,12],[-14,6],[-14,7],[2,12]],[[2,12],[-14,4],[-14,5],[2,12]],[[2,12],[-14,2],[-14,3],[2,12]],[[2,12],[-13,1],[-14,1],[2,12]],[[2,12],[-11,1],[-12,1],[2,12]],[[2,12],[-9,1],[-10,1],[2,12]],[[2,12],[-7,1],[-8,1],[2,12]],[[2,12],[-5,1],[-6,1],[2,12]],[[2,12],[-3,1],[-4,1],[2,12]],[[2,12],[-1,1],[-2,1],[2,12]],[[2,12],[1,1],[0,1],[2,12]]]
1 change: 1 addition & 0 deletions test/fixtures/touching-holes6.json

Large diffs are not rendered by default.

47 changes: 33 additions & 14 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,39 @@ test('empty', () => {

for (const id of Object.keys(expected.triangles)) {

test(id, () => {
const data = flatten(JSON.parse(fs.readFileSync(new URL(`fixtures/${id}.json`, import.meta.url)))),
indices = earcut(data.vertices, data.holes, data.dimensions),
err = deviation(data.vertices, data.holes, data.dimensions, indices),
expectedTriangles = expected.triangles[id],
expectedDeviation = expected.errors[id] || 0;

const numTriangles = indices.length / 3;
assert.ok(numTriangles === expectedTriangles, `${numTriangles} triangles when expected ${expectedTriangles}`);

if (expectedTriangles > 0) {
assert.ok(err <= expectedDeviation, `deviation ${err} <= ${expectedDeviation}`);
}
});
for (const rotation of [0, 90, 180, 270]) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added this while debugging to test some adjacent cases, I can roll it it back if you don't want it.

test(`${id} rotation ${rotation}`, () => {
const coords = JSON.parse(fs.readFileSync(new URL(`fixtures/${id}.json`, import.meta.url)));
const theta = rotation * Math.PI / 180;
const xx = Math.round(Math.cos(theta));
const xy = Math.round(-Math.sin(theta));
const yx = Math.round(Math.sin(theta));
const yy = Math.round(Math.cos(theta));
if (rotation) {
for (const ring of coords) {
for (const coord of ring) {
const [x, y] = coord;
coord[0] = xx * x + xy * y;
coord[1] = yx * x + yy * y;
}
}
}
const data = flatten(coords),
indices = earcut(data.vertices, data.holes, data.dimensions),
err = deviation(data.vertices, data.holes, data.dimensions, indices),
expectedTriangles = expected.triangles[id],
expectedDeviation = (rotation !== 0 && expected['errors-with-rotation'][id]) || expected.errors[id] || 0;

const numTriangles = indices.length / 3;
if (rotation === 0) {
assert.ok(numTriangles === expectedTriangles, `${numTriangles} triangles when expected ${expectedTriangles}`);
}

if (expectedTriangles > 0) {
assert.ok(err <= expectedDeviation, `deviation ${err} <= ${expectedDeviation}`);
}
});
}
}

test('infinite-loop', () => {
Expand Down
247 changes: 127 additions & 120 deletions viz/viz.js

Large diffs are not rendered by default.

Loading