Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I had to make this tweak to the benchmarks to get them to run

main:

typical OSM building (15 vertices): x 2,751,261 ops/sec ±0.50% (96 runs sampled)
dude shape (94 vertices): x 76,587 ops/sec ±0.42% (100 runs sampled)
dude shape with holes (104 vertices): x 61,820 ops/sec ±0.32% (100 runs sampled)
complex OSM water (2523 vertices): x 1,024 ops/sec ±0.49% (97 runs sampled)

this branch:

typical OSM building (15 vertices): x 2,644,854 ops/sec ±0.46% (92 runs sampled)
dude shape (94 vertices): x 76,872 ops/sec ±0.31% (100 runs sampled)
dude shape with holes (104 vertices): x 61,708 ops/sec ±0.28% (100 runs sampled)
complex OSM water (2523 vertices): x 1,040 ops/sec ±0.22% (98 runs sampled)

import Benchmark from 'benchmark';
import {readFileSync} from 'fs';

Expand Down
31 changes: 23 additions & 8 deletions src/earcut.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ function isEar(ear) {
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) &&
!equals(a, p) &&
area(p.prev, p, p.next) >= 0) return false;
p = p.next;
}
Expand Down Expand Up @@ -182,25 +183,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;
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y, true) && !equals(a, p) && 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;
pointInTriangle(ax, ay, bx, by, cx, cy, n.x, n.y, true) && !equals(a, n) && 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;
pointInTriangle(ax, ay, bx, by, cx, cy, p.x, p.y, true) && !equals(a, p) && 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;
pointInTriangle(ax, ay, bx, by, cx, cy, n.x, n.y, true) && !equals(a, n) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}

Expand Down Expand Up @@ -268,7 +269,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 +279,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 +318,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
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
Copy Markdown
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.