UNPKG

@thi.ng/geom-voronoi

Version:

Fast, incremental 2D Delaunay & Voronoi mesh implementation

312 lines (311 loc) 9 kB
import { defBitField } from "@thi.ng/bitfield/bitfield"; import { isNumber } from "@thi.ng/checks/is-number"; import { liangBarsky2 } from "@thi.ng/geom-clip-line/liang-barsky"; import { sutherlandHodgeman } from "@thi.ng/geom-clip-poly"; import { pointInCircumCircle, pointInPolygon2, pointInSegment } from "@thi.ng/geom-isec/point"; import { centroid } from "@thi.ng/geom-poly-utils/centroid"; import { circumCenter2 } from "@thi.ng/geom-poly-utils/circumcenter"; import { EPS } from "@thi.ng/math/api"; import { defEdge } from "@thi.ng/quad-edge"; import { ZERO2 } from "@thi.ng/vectors/api"; import { eqDelta2 } from "@thi.ng/vectors/eqdelta"; import { signedArea2 } from "@thi.ng/vectors/signed-area"; class DVMesh { first; nextEID; nextVID; constructor(pts, size = 1e5) { const a = { pos: [0, -size], id: 0 }; const b = { pos: [size, size], id: 1 }; const c = { pos: [-size, size], id: 2 }; const eab = defEdge(0, a, b); const ebc = defEdge(4, b, c); const eca = defEdge(8, c, a); eab.sym.splice(ebc); ebc.sym.splice(eca); eca.sym.splice(eab); this.first = eab; this.nextEID = 12; this.nextVID = 3; if (pts?.length) { isNumber(pts[0][0]) ? this.addKeys(pts) : this.addAll(pts); } else { this.computeDual(); } } /** * Adds a single new point `p` w/ optional value `val` to the mesh, unless * there already is another point existing within radius `eps`. If `update` * is true (default), the mesh dual will be automatically updated using * {@link DVMesh.computeDual}. * * @remarks * If adding multiple points, ensure `computeDual` will only be called * for/after the last point insertion to avoid computational overhead. * * @param p - * @param val - * @param eps - * @param update - */ add(p, val, eps = EPS, update = true) { let [e, exists] = this.locate(p, eps); if (exists) return false; if (pointInSegment(p, e.origin.pos, e.dest.pos)) { e = e.oprev; e.onext.remove(); } let base = defEdge(this.nextEID, e.origin, { pos: p, id: this.nextVID++, val }); base.splice(e); this.nextEID += 4; const first = base; do { base = e.connect(base.sym, this.nextEID); e = base.oprev; this.nextEID += 4; } while (e.lnext !== first); do { const t = e.oprev; if (__isRightOf(t.dest.pos, e) && pointInCircumCircle(p, e.origin.pos, t.dest.pos, e.dest.pos)) { e.swap(); e = e.oprev; } else if (e.onext !== first) { e = e.onext.lprev; } else { break; } } while (true); update && this.computeDual(); return true; } addKeys(pts, eps) { for (const p of pts) { this.add(p, void 0, eps, false); } this.computeDual(); } addAll(pairs, eps) { for (const p of pairs) { this.add(p[0], p[1], eps, false); } this.computeDual(); } /** * Returns tuple of the edge related to `p` and a boolean to indicate if * `p` already exists in this triangulation (true if already present). * * @param p - query point */ locate(p, eps = EPS) { let e = this.first; while (true) { if (eqDelta2(p, e.origin.pos, eps) || eqDelta2(p, e.dest.pos, eps)) { return [e, true]; } else if (__isRightOf(p, e)) { e = e.sym; } else if (!__isRightOf(p, e.onext)) { e = e.onext; } else if (!__isRightOf(p, e.dprev)) { e = e.dprev; } else { return [e, false]; } } } /** * Syncronize / update / add dual faces (i.e. Voronoi) for current * primary mesh (i.e. Delaunay). */ computeDual() { const work = [this.first.rot]; const visitedEdges = {}; const visitedVerts = {}; while (work.length) { const e = work.pop(); if (visitedEdges[e.id]) continue; visitedEdges[e.id] = true; if (!e.origin || !visitedVerts[e.origin.id]) { let t = e.rot; const a = t.origin.pos; let isBoundary = t.origin.id < 3; t = t.lnext; const b = t.origin.pos; isBoundary = isBoundary && t.origin.id < 3; t = t.lnext; const c = t.origin.pos; isBoundary = isBoundary && t.origin.id < 3; const id = this.nextVID++; e.origin = { pos: !isBoundary ? circumCenter2(a, b, c) : ZERO2, id }; visitedVerts[id] = true; } work.push(e.sym, e.onext, e.lnext); } } delaunay(bounds) { const cells = []; const usedEdges = defBitField(this.nextEID); const bc = bounds && centroid(bounds); this.traverse((eab) => { if (!usedEdges.at(eab.id)) { const ebc = eab.lnext; const eca = ebc.lnext; const va = eab.origin.pos; const vb = ebc.origin.pos; const vc = eca.origin.pos; let verts = [va, vb, vc]; if (bounds && !(pointInPolygon2(va, bounds) && pointInPolygon2(vb, bounds) && pointInPolygon2(vc, bounds))) { verts = sutherlandHodgeman(verts, bounds, bc); if (verts.length > 2) { cells.push(verts); } } else { cells.push(verts); } usedEdges.setAt(eab.id); usedEdges.setAt(ebc.id); usedEdges.setAt(eca.id); } }); return cells; } /** * Advanced use only. Returns Delaunay triangles as arrays of raw * [thi.ng/quad-edge * Edges](https://docs.thi.ng/umbrella/quad-edge/classes/Edge.html). * * @remarks * The actual vertex position associated with each edge can be obtained via * `e.origin.pos`. Each edge (and each associated {@link Vertex}) also has a * unique ID, accessible via `e.id` and `e.origin.id`. */ delaunayQE() { const cells = []; const usedEdges = defBitField(this.nextEID); this.traverse((eab) => { if (!usedEdges.at(eab.id)) { const ebc = eab.lnext; const eca = ebc.lnext; cells.push([eab, ebc, eca]); usedEdges.setAt(eab.id); usedEdges.setAt(ebc.id); usedEdges.setAt(eca.id); } }); return cells; } voronoi(bounds) { const cells = []; const bc = bounds && centroid(bounds); this.traverse( bounds ? (e) => { const first = e = e.rot; let verts = []; let needsClip = false; let p; do { p = e.origin.pos; verts.push(p); needsClip = needsClip || !pointInPolygon2(p, bounds); } while ((e = e.lnext) !== first); if (needsClip) { verts = sutherlandHodgeman(verts, bounds, bc); if (verts.length < 3) return; } cells.push(verts); } : (e) => { const first = e = e.rot; const verts = []; do { verts.push(e.origin.pos); } while ((e = e.lnext) !== first); cells.push(verts); }, false ); return cells; } /** * Advanced use only. Returns Voronoi cells as arrays of raw * [thi.ng/quad-edge * Edges](https://docs.thi.ng/umbrella/quad-edge/classes/Edge.html). * * @remarks * The actual vertex position associated with each edge can be obtained via * `e.origin.pos`. Each edge (and each associated {@link Vertex}) also has a * unique ID, accessible via `e.id` and `e.origin.id`. */ voronoiQE() { const cells = []; this.traverse((e) => { const first = e = e.rot; const cell = []; do { cell.push(e); } while ((e = e.lnext) !== first); cells.push(cell); }, false); return cells; } edges(voronoi = false, boundsMinMax) { const edges = []; this.traverse( (e, visitedEdges) => { if (visitedEdges.at(e.sym.id)) return; if (e.origin.id > 2 && e.dest.id > 2) { const a = e.origin.pos; const b = e.dest.pos; if (boundsMinMax) { const clip = liangBarsky2( a, b, boundsMinMax[0], boundsMinMax[1] ); clip && edges.push([clip[0], clip[1]]); } else { edges.push([a, b]); } } visitedEdges.setAt(e.id); }, true, voronoi ? this.first.rot : this.first ); return edges; } traverse(proc, edges = true, e = this.first) { const work = [e]; const visitedEdges = defBitField(this.nextEID); const visitedVerts = defBitField(this.nextVID); while (work.length) { e = work.pop(); if (visitedEdges.at(e.id)) continue; visitedEdges.setAt(e.id); const eoID = e.origin.id; if (eoID > 2 && e.rot.origin.id > 2) { if (edges || !visitedVerts.at(eoID)) { visitedVerts.setAt(eoID); proc(e, visitedEdges, visitedVerts); } } work.push(e.sym, e.onext, e.lnext); } } } const __isRightOf = (p, e) => signedArea2(p, e.dest.pos, e.origin.pos) > 0; export { DVMesh };