From 696b299020044f712a30056416a0a4c90a301fec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Fri, 30 Aug 2019 08:59:08 +0200 Subject: [PATCH] More robust orientation tests (#51) Closes #43. * test for issue #43 (broken hull) * Decide the triangle orientation with a symmetric formula (in this case a majority vote) Solves #43. * use error-bound orientation tests --- index.js | 15 +++++++++++++-- test/fixtures/issue43.json | 7 +++++++ test/test.js | 21 +++++++++++++++++++-- 3 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/issue43.json diff --git a/index.js b/index.js index baa9cde..2a84659 100644 --- a/index.js +++ b/index.js @@ -378,8 +378,19 @@ function dist(ax, ay, bx, by) { return dx * dx + dy * dy; } -function orient(px, py, qx, qy, rx, ry) { - return (qy - py) * (rx - qx) - (qx - px) * (ry - qy) < 0; +// return 2d orientation sign if we're confident in it through J. Shewchuk's error bound check +function orientIfSure(px, py, rx, ry, qx, qy) { + const l = (ry - py) * (qx - px); + const r = (rx - px) * (qy - py); + return Math.abs(l - r) >= 3.3306690738754716e-16 * Math.abs(l + r) ? l - r : 0; +} + +// a more robust orientation test that's stable in a given triangle (to fix robustness issues) +function orient(rx, ry, qx, qy, px, py) { + const sign = orientIfSure(px, py, rx, ry, qx, qy) || + orientIfSure(rx, ry, qx, qy, px, py) || + orientIfSure(qx, qy, px, py, rx, ry); + return sign < 0; } function inCircle(ax, ay, bx, by, cx, cy, px, py) { diff --git a/test/fixtures/issue43.json b/test/fixtures/issue43.json new file mode 100644 index 0000000..f4854b2 --- /dev/null +++ b/test/fixtures/issue43.json @@ -0,0 +1,7 @@ +[ + [-537.7739674441619, -122.26130468750004], + [-495.533967444162, -183.39195703125006], + [-453.29396744416204, -244.5226093750001], + [-411.0539674441621, -305.6532617187501], + [-164, -122] +] \ No newline at end of file diff --git a/test/test.js b/test/test.js index 25e978a..3354cc1 100644 --- a/test/test.js +++ b/test/test.js @@ -4,6 +4,7 @@ import Delaunator from '../index.js'; import points from './fixtures/ukraine.json'; import issue13 from './fixtures/issue13.json'; +import issue43 from './fixtures/issue43.json'; import issue44 from './fixtures/issue44.json'; import robustness1 from './fixtures/robustness1.json'; import robustness2 from './fixtures/robustness2.json'; @@ -59,13 +60,18 @@ test('issue #11', (t) => { t.end(); }); +test('issue #13', (t) => { + validate(t, issue13); + t.end(); +}); + test('issue #24', (t) => { validate(t, [[382, 302], [382, 328], [382, 205], [623, 175], [382, 188], [382, 284], [623, 87], [623, 341], [141, 227]]); t.end(); }); -test('issue #13', (t) => { - validate(t, issue13); +test('issue #43', (t) => { + validate(t, issue43); t.end(); }); @@ -114,6 +120,15 @@ test('supports custom point format', (t) => { t.end(); }); +function orient([px, py], [rx, ry], [qx, qy]) { + const l = (ry - py) * (qx - px); + const r = (rx - px) * (qy - py); + return Math.abs(l - r) >= 3.3306690738754716e-16 * Math.abs(l + r) ? l - r : 0; +} +function convex(r, q, p) { + return (orient(p, r, q) || orient(r, q, p) || orient(q, p, r)) >= 0; +} + function validate(t, points, d = Delaunator.from(points)) { // validate halfedges for (let i = 0; i < d.halfedges.length; i++) { @@ -130,6 +145,8 @@ function validate(t, points, d = Delaunator.from(points)) { const [x0, y0] = points[d.hull[j]]; const [x, y] = points[d.hull[i]]; hullAreas.push((x - x0) * (y + y0)); + const c = convex(points[d.hull[j]], points[d.hull[(j + 1) % d.hull.length]], points[d.hull[(j + 3) % d.hull.length]]); + if (!c) t.fail(`hull is not convex at ${j}`); } const hullArea = sum(hullAreas);