UNPKG

@maplat/edgeruler

Version:

A small library for dual-constraining a Delaunator triangulation

430 lines (369 loc) 11.6 kB
/* Originally based on @kninnug/constrainautor 4.0.0 https://github.com/kninnug/Constrainautor/ */ import {orient2d, incircle} from 'robust-predicates'; import {BitSet8} from '../common/bitset'; import {nextEdge, prevEdge, Base, DelaunatorLike, intersectSegments as baseIntersectSegments} from '../common/base'; import type {BitSet} from '../common/bitset'; class Constrain extends Base { vertMap: Uint32Array; flips: BitSet; consd: BitSet; /** * Make a Constrain. * * @param del The triangulation output from Delaunator. * @param edges If provided, constrain these edges as by constrainAll. */ constructor(del: DelaunatorLike, edges?: readonly [number, number][]) { if(!del || typeof del !== 'object' || !del.triangles || !del.halfedges || !del.coords) { throw new Error("Expected an object with Delaunator output"); } if(del.triangles.length % 3 || del.halfedges.length !== del.triangles.length || del.coords.length % 2) { throw new Error("Delaunator output appears inconsistent"); } if(del.triangles.length < 3) { throw new Error("No edges in triangulation"); } super(del); const U32NIL = 2**32 - 1; const numPoints = del.coords.length >> 1; const numEdges = del.triangles.length; // Map every vertex id to the right-most edge that points to that vertex. this.vertMap = new Uint32Array(numPoints).fill(U32NIL); // Keep track of edges flipped while constraining this.flips = new BitSet8(numEdges); // Keep track of constrained edges this.consd = new BitSet8(numEdges); for(let e = 0; e < numEdges; e++) { const v = del.triangles[e]; if(this.vertMap[v] === U32NIL) { this.updateVert(e); } } if(edges) { this.constrainAll(edges); } } /** * Constrain the triangulation such that there is an edge between p1 and p2. */ constrainOne(segP1: number, segP2: number) { const {triangles, halfedges} = this.del; const start = this.vertMap[segP1]; // Loop over the edges touching segP1 let edg = start; do { const p4 = triangles[edg], nxt = nextEdge(edg); // already constrained, but in reverse order if(p4 === segP2) { return this.protect(edg); } // The edge opposite segP1 const opp = prevEdge(edg), p3 = triangles[opp]; // already constrained if(p3 === segP2) { this.protect(nxt); return nxt; } // edge opposite segP1 intersects constraint if(this.intersectSegments(segP1, segP2, p3, p4)) { edg = opp; break; } const adj = halfedges[nxt]; // The next edge pointing to segP1 edg = adj; } while(edg !== -1 && edg !== start); let conEdge = edg; // Walk through the triangulation looking for further intersecting // edges and flip them. If an intersecting edge cannot be flipped, // assign its id to `rescan` and restart from there, until there are // no more intersects. let rescan = -1; while(edg !== -1) { const adj = halfedges[edg], bot = prevEdge(edg), top = prevEdge(adj), rgt = nextEdge(adj); if(adj === -1) { throw new Error("Constraining edge exited the hull"); } if(this.consd.has(edg)) { throw new Error("Edge intersects already constrained edge"); } if(this.isCollinear(segP1, segP2, triangles[edg]) || this.isCollinear(segP1, segP2, triangles[adj])) { throw new Error("Constraining edge intersects point"); } const convex = this.intersectSegments( triangles[edg], triangles[adj], triangles[bot], triangles[top] ); if(!convex) { if(rescan === -1) { rescan = edg; } if(triangles[top] === segP2) { if(edg === rescan) { throw new Error("Infinite loop: non-convex quadrilateral"); } edg = rescan; rescan = -1; continue; } if(this.intersectSegments(segP1, segP2, triangles[top], triangles[adj])) { edg = top; } else if(this.intersectSegments(segP1, segP2, triangles[rgt], triangles[top])) { edg = rgt; } else if(rescan === edg) { throw new Error("Infinite loop: no further intersect after non-convex"); } continue; } this.flipDiagonal(edg); if(this.intersectSegments(segP1, segP2, triangles[bot], triangles[top])) { if(rescan === -1) { rescan = bot; } if(rescan === bot) { throw new Error("Infinite loop: flipped diagonal still intersects"); } } if(triangles[top] === segP2) { conEdge = top; edg = rescan; rescan = -1; } else if(this.intersectSegments(segP1, segP2, triangles[rgt], triangles[top])) { edg = rgt; } } this.protect(conEdge); this.delaunify(true); return this.findEdge(segP1, segP2); } /** * Fix the Delaunay condition. */ delaunify(deep = false) { const {halfedges} = this.del; const flips = this.flips; const consd = this.consd; const len = halfedges.length; do { var flipped = 0; for(let edg = 0; edg < len; edg++) { if(consd.has(edg)) { continue; } flips.delete(edg); const adj = halfedges[edg]; if(adj === -1) { continue; } flips.delete(adj); if(!this.isDelaunay(edg)) { this.flipDiagonal(edg); flipped++; } } } while(deep && flipped > 0); return this; } /** * Call constrainOne on each edge */ constrainAll(edges: readonly [number, number][]) { const len = edges.length; for(let i = 0; i < len; i++) { const e = edges[i]; this.constrainOne(e[0], e[1]); } return this; } /** * Whether an edge is constrained */ isConstrained(edg: number) { return this.consd.has(edg); } /** * Find the edge that points from p1 -> p2. If there is only an edge from * p2 -> p1 (i.e. it is on the hull), returns the negative id of it. */ findEdge(p1: number, p2: number) { const start1 = this.vertMap[p2]; const {triangles, halfedges} = this.del; let edg = start1, prv = -1; // Walk around p2, iterating over the edges pointing to it do { if(triangles[edg] === p1) { return edg; } prv = nextEdge(edg); edg = halfedges[prv]; } while(edg !== -1 && edg !== start1); // Did not find p1 -> p2, the only option is that it is on the hull on // the 'left-hand' side, pointing p2 -> p1 (or there is no edge) if(triangles[nextEdge(prv)] === p1) { return -prv; } return Infinity; } /** * Mark an edge as constrained, i.e. should not be touched by `delaunify`. */ private protect(edg: number) { const adj = this.del.halfedges[edg]; const flips = this.flips; const consd = this.consd; flips.delete(edg); consd.add(edg); if(adj !== -1) { flips.delete(adj); consd.add(adj); return adj; } return -edg; } /** * Mark an edge as flipped unless constrained. */ private markFlip(edg: number) { const halfedges = this.del.halfedges; const flips = this.flips; const consd = this.consd; if(consd.has(edg)) { return false; } const adj = halfedges[edg]; if(adj !== -1) { flips.add(edg); flips.add(adj); } return true; } /** * Flip the edge shared by two triangles. */ private flipDiagonal(edg: number) { const {triangles, halfedges} = this.del; const flips = this.flips; const consd = this.consd; const adj = halfedges[edg]; const bot = prevEdge(edg); const lft = nextEdge(edg); const top = prevEdge(adj); const rgt = nextEdge(adj); const adjBot = halfedges[bot]; const adjTop = halfedges[top]; if(consd.has(edg)) { throw new Error("Trying to flip a constrained edge"); } triangles[edg] = triangles[top]; halfedges[edg] = adjTop; if(!flips.set(edg, flips.has(top))) { consd.set(edg, consd.has(top)); } if(adjTop !== -1) { halfedges[adjTop] = edg; } halfedges[bot] = top; triangles[adj] = triangles[bot]; halfedges[adj] = adjBot; if(!flips.set(adj, flips.has(bot))) { consd.set(adj, consd.has(bot)); } if(adjBot !== -1) { halfedges[adjBot] = adj; } halfedges[top] = bot; this.markFlip(edg); this.markFlip(lft); this.markFlip(adj); this.markFlip(rgt); flips.add(bot); consd.delete(bot); flips.add(top); consd.delete(top); this.updateVert(edg); this.updateVert(lft); this.updateVert(adj); this.updateVert(rgt); return bot; } /** * Whether point p1, p2, and p are collinear */ private isCollinear(p1: number, p2: number, p: number) { const pts = this.del.coords; return orient2d( pts[p1 * 2], pts[p1 * 2 + 1], pts[p2 * 2], pts[p2 * 2 + 1], pts[p * 2], pts[p * 2 + 1] ) === 0.0; } /** * Whether px is in the circumcircle of the triangle formed by p1, p2, p3 */ private inCircle(p1: number, p2: number, p3: number, px: number) { const pts = this.del.coords; return incircle( pts[p1 * 2], pts[p1 * 2 + 1], pts[p2 * 2], pts[p2 * 2 + 1], pts[p3 * 2], pts[p3 * 2 + 1], pts[px * 2], pts[px * 2 + 1] ) < 0.0; } /** * Whether the triangles sharing edg conform to the Delaunay condition */ private isDelaunay(edg: number) { const {triangles, halfedges} = this.del; const adj = halfedges[edg]; if(adj === -1) { return true; } const p1 = triangles[prevEdge(edg)], p2 = triangles[edg], p3 = triangles[nextEdge(edg)], px = triangles[prevEdge(adj)]; return !this.inCircle(p1, p2, p3, px); } /** * Update the vertex -> incoming edge map */ private updateVert(start: number) { const {triangles, halfedges} = this.del; const vm = this.vertMap; const v = triangles[start]; let inc = prevEdge(start); let adj = halfedges[inc]; while(adj !== -1 && adj !== start) { inc = prevEdge(adj); adj = halfedges[inc]; } vm[v] = inc; return inc; } /** * Whether the segments between vertices intersect */ protected intersectSegments(p1: number, p2: number, p3: number, p4: number) { const pts = this.del.coords; if(p1 === p3 || p1 === p4 || p2 === p3 || p2 === p4) { return false; } return baseIntersectSegments( pts[p1 * 2], pts[p1 * 2 + 1], pts[p2 * 2], pts[p2 * 2 + 1], pts[p3 * 2], pts[p3 * 2 + 1], pts[p4 * 2], pts[p4 * 2 + 1] ); } static intersectSegments = baseIntersectSegments; } export default Constrain;