// loaders.gl // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors // ISC License // Copyright(c) 2019, Michael Fogleman, Vladimir Agafonkin // @ts-nocheck /* eslint-disable complexity, max-params, max-statements, max-depth, no-constant-condition */ export default class Delatin { constructor(data, width, height = width) { this.data = data; // height data this.width = width; this.height = height; this.coords = []; // vertex coordinates (x, y) this.triangles = []; // mesh triangle indices // additional triangle data this._halfedges = []; this._candidates = []; this._queueIndices = []; this._queue = []; // queue of added triangles this._errors = []; this._rms = []; this._pending = []; // triangles pending addition to queue this._pendingLen = 0; this._rmsSum = 0; const x1 = width - 1; const y1 = height - 1; const p0 = this._addPoint(0, 0); const p1 = this._addPoint(x1, 0); const p2 = this._addPoint(0, y1); const p3 = this._addPoint(x1, y1); // add initial two triangles const t0 = this._addTriangle(p3, p0, p2, -1, -1, -1); this._addTriangle(p0, p3, p1, t0, -1, -1); this._flush(); } // refine the mesh until its maximum error gets below the given one run(maxError = 1) { while (this.getMaxError() > maxError) { this.refine(); } } // refine the mesh with a single point refine() { this._step(); this._flush(); } // max error of the current mesh getMaxError() { return this._errors[0]; } // root-mean-square deviation of the current mesh getRMSD() { return this._rmsSum > 0 ? Math.sqrt(this._rmsSum / (this.width * this.height)) : 0; } // height value at a given position heightAt(x, y) { return this.data[this.width * y + x]; } // rasterize and queue all triangles that got added or updated in _step _flush() { const coords = this.coords; for (let i = 0; i < this._pendingLen; i++) { const t = this._pending[i]; // rasterize triangle to find maximum pixel error const a = 2 * this.triangles[t * 3 + 0]; const b = 2 * this.triangles[t * 3 + 1]; const c = 2 * this.triangles[t * 3 + 2]; this._findCandidate( coords[a], coords[a + 1], coords[b], coords[b + 1], coords[c], coords[c + 1], t ); } this._pendingLen = 0; } // rasterize a triangle, find its max error, and queue it for processing _findCandidate(p0x, p0y, p1x, p1y, p2x, p2y, t) { // triangle bounding box const minX = Math.min(p0x, p1x, p2x); const minY = Math.min(p0y, p1y, p2y); const maxX = Math.max(p0x, p1x, p2x); const maxY = Math.max(p0y, p1y, p2y); // forward differencing variables let w00 = orient(p1x, p1y, p2x, p2y, minX, minY); let w01 = orient(p2x, p2y, p0x, p0y, minX, minY); let w02 = orient(p0x, p0y, p1x, p1y, minX, minY); const a01 = p1y - p0y; const b01 = p0x - p1x; const a12 = p2y - p1y; const b12 = p1x - p2x; const a20 = p0y - p2y; const b20 = p2x - p0x; // pre-multiplied z values at vertices const a = orient(p0x, p0y, p1x, p1y, p2x, p2y); const z0 = this.heightAt(p0x, p0y) / a; const z1 = this.heightAt(p1x, p1y) / a; const z2 = this.heightAt(p2x, p2y) / a; // iterate over pixels in bounding box let maxError = 0; let mx = 0; let my = 0; let rms = 0; for (let y = minY; y <= maxY; y++) { // compute starting offset let dx = 0; if (w00 < 0 && a12 !== 0) { dx = Math.max(dx, Math.floor(-w00 / a12)); } if (w01 < 0 && a20 !== 0) { dx = Math.max(dx, Math.floor(-w01 / a20)); } if (w02 < 0 && a01 !== 0) { dx = Math.max(dx, Math.floor(-w02 / a01)); } let w0 = w00 + a12 * dx; let w1 = w01 + a20 * dx; let w2 = w02 + a01 * dx; let wasInside = false; for (let x = minX + dx; x <= maxX; x++) { // check if inside triangle if (w0 >= 0 && w1 >= 0 && w2 >= 0) { wasInside = true; // compute z using barycentric coordinates const z = z0 * w0 + z1 * w1 + z2 * w2; const dz = Math.abs(z - this.heightAt(x, y)); rms += dz * dz; if (dz > maxError) { maxError = dz; mx = x; my = y; } } else if (wasInside) { break; } w0 += a12; w1 += a20; w2 += a01; } w00 += b12; w01 += b20; w02 += b01; } if ((mx === p0x && my === p0y) || (mx === p1x && my === p1y) || (mx === p2x && my === p2y)) { maxError = 0; } // update triangle metadata this._candidates[2 * t] = mx; this._candidates[2 * t + 1] = my; this._rms[t] = rms; // add triangle to priority queue this._queuePush(t, maxError, rms); } // process the next triangle in the queue, splitting it with a new point _step() { // pop triangle with highest error from priority queue const t = this._queuePop(); const e0 = t * 3 + 0; const e1 = t * 3 + 1; const e2 = t * 3 + 2; const p0 = this.triangles[e0]; const p1 = this.triangles[e1]; const p2 = this.triangles[e2]; const ax = this.coords[2 * p0]; const ay = this.coords[2 * p0 + 1]; const bx = this.coords[2 * p1]; const by = this.coords[2 * p1 + 1]; const cx = this.coords[2 * p2]; const cy = this.coords[2 * p2 + 1]; const px = this._candidates[2 * t]; const py = this._candidates[2 * t + 1]; const pn = this._addPoint(px, py); if (orient(ax, ay, bx, by, px, py) === 0) { this._handleCollinear(pn, e0); } else if (orient(bx, by, cx, cy, px, py) === 0) { this._handleCollinear(pn, e1); } else if (orient(cx, cy, ax, ay, px, py) === 0) { this._handleCollinear(pn, e2); } else { const h0 = this._halfedges[e0]; const h1 = this._halfedges[e1]; const h2 = this._halfedges[e2]; const t0 = this._addTriangle(p0, p1, pn, h0, -1, -1, e0); const t1 = this._addTriangle(p1, p2, pn, h1, -1, t0 + 1); const t2 = this._addTriangle(p2, p0, pn, h2, t0 + 2, t1 + 1); this._legalize(t0); this._legalize(t1); this._legalize(t2); } } // add coordinates for a new vertex _addPoint(x, y) { const i = this.coords.length >> 1; this.coords.push(x, y); return i; } // add or update a triangle in the mesh _addTriangle(a, b, c, ab, bc, ca, e = this.triangles.length) { const t = e / 3; // new triangle index // add triangle vertices this.triangles[e + 0] = a; this.triangles[e + 1] = b; this.triangles[e + 2] = c; // add triangle halfedges this._halfedges[e + 0] = ab; this._halfedges[e + 1] = bc; this._halfedges[e + 2] = ca; // link neighboring halfedges if (ab >= 0) { this._halfedges[ab] = e + 0; } if (bc >= 0) { this._halfedges[bc] = e + 1; } if (ca >= 0) { this._halfedges[ca] = e + 2; } // init triangle metadata this._candidates[2 * t + 0] = 0; this._candidates[2 * t + 1] = 0; this._queueIndices[t] = -1; this._rms[t] = 0; // add triangle to pending queue for later rasterization this._pending[this._pendingLen++] = t; // return first halfedge index return e; } _legalize(a) { // if the pair of triangles doesn't satisfy the Delaunay condition // (p1 is inside the circumcircle of [p0, pl, pr]), flip them, // then do the same check/flip recursively for the new pair of triangles // // pl pl // /||\ / \ // al/ || \bl al/ \a // / || \ / \ // / a||b \ flip /___ar___\ // p0\ || /p1 => p0\---bl---/p1 // \ || / \ / // ar\ || /br b\ /br // \||/ \ / // pr pr const b = this._halfedges[a]; if (b < 0) { return; } const a0 = a - (a % 3); const b0 = b - (b % 3); const al = a0 + ((a + 1) % 3); const ar = a0 + ((a + 2) % 3); const bl = b0 + ((b + 2) % 3); const br = b0 + ((b + 1) % 3); const p0 = this.triangles[ar]; const pr = this.triangles[a]; const pl = this.triangles[al]; const p1 = this.triangles[bl]; const coords = this.coords; if ( !inCircle( coords[2 * p0], coords[2 * p0 + 1], coords[2 * pr], coords[2 * pr + 1], coords[2 * pl], coords[2 * pl + 1], coords[2 * p1], coords[2 * p1 + 1] ) ) { return; } const hal = this._halfedges[al]; const har = this._halfedges[ar]; const hbl = this._halfedges[bl]; const hbr = this._halfedges[br]; this._queueRemove(a0 / 3); this._queueRemove(b0 / 3); const t0 = this._addTriangle(p0, p1, pl, -1, hbl, hal, a0); const t1 = this._addTriangle(p1, p0, pr, t0, har, hbr, b0); this._legalize(t0 + 1); this._legalize(t1 + 2); } // handle a case where new vertex is on the edge of a triangle _handleCollinear(pn, a) { const a0 = a - (a % 3); const al = a0 + ((a + 1) % 3); const ar = a0 + ((a + 2) % 3); const p0 = this.triangles[ar]; const pr = this.triangles[a]; const pl = this.triangles[al]; const hal = this._halfedges[al]; const har = this._halfedges[ar]; const b = this._halfedges[a]; if (b < 0) { const t0 = this._addTriangle(pn, p0, pr, -1, har, -1, a0); const t1 = this._addTriangle(p0, pn, pl, t0, -1, hal); this._legalize(t0 + 1); this._legalize(t1 + 2); return; } const b0 = b - (b % 3); const bl = b0 + ((b + 2) % 3); const br = b0 + ((b + 1) % 3); const p1 = this.triangles[bl]; const hbl = this._halfedges[bl]; const hbr = this._halfedges[br]; this._queueRemove(b0 / 3); const t0 = this._addTriangle(p0, pr, pn, har, -1, -1, a0); const t1 = this._addTriangle(pr, p1, pn, hbr, -1, t0 + 1, b0); const t2 = this._addTriangle(p1, pl, pn, hbl, -1, t1 + 1); const t3 = this._addTriangle(pl, p0, pn, hal, t0 + 2, t2 + 1); this._legalize(t0); this._legalize(t1); this._legalize(t2); this._legalize(t3); } // priority queue methods _queuePush(t, error, rms) { const i = this._queue.length; this._queueIndices[t] = i; this._queue.push(t); this._errors.push(error); this._rmsSum += rms; this._queueUp(i); } _queuePop() { const n = this._queue.length - 1; this._queueSwap(0, n); this._queueDown(0, n); return this._queuePopBack(); } _queuePopBack() { const t = this._queue.pop(); this._errors.pop(); this._rmsSum -= this._rms[t]; this._queueIndices[t] = -1; return t; } _queueRemove(t) { const i = this._queueIndices[t]; if (i < 0) { const it = this._pending.indexOf(t); if (it !== -1) { this._pending[it] = this._pending[--this._pendingLen]; } else { throw new Error('Broken triangulation (something went wrong).'); } return; } const n = this._queue.length - 1; if (n !== i) { this._queueSwap(i, n); if (!this._queueDown(i, n)) { this._queueUp(i); } } this._queuePopBack(); } _queueLess(i, j) { return this._errors[i] > this._errors[j]; } _queueSwap(i, j) { const pi = this._queue[i]; const pj = this._queue[j]; this._queue[i] = pj; this._queue[j] = pi; this._queueIndices[pi] = j; this._queueIndices[pj] = i; const e = this._errors[i]; this._errors[i] = this._errors[j]; this._errors[j] = e; } _queueUp(j0) { let j = j0; while (true) { const i = (j - 1) >> 1; if (i === j || !this._queueLess(j, i)) { break; } this._queueSwap(i, j); j = i; } } _queueDown(i0, n) { let i = i0; while (true) { const j1 = 2 * i + 1; if (j1 >= n || j1 < 0) { break; } const j2 = j1 + 1; let j = j1; if (j2 < n && this._queueLess(j2, j1)) { j = j2; } if (!this._queueLess(j, i)) { break; } this._queueSwap(i, j); i = j; } return i > i0; } } function orient(ax, ay, bx, by, cx, cy) { return (bx - cx) * (ay - cy) - (by - cy) * (ax - cx); } function inCircle(ax, ay, bx, by, cx, cy, px, py) { const dx = ax - px; const dy = ay - py; const ex = bx - px; const ey = by - py; const fx = cx - px; const fy = cy - py; const ap = dx * dx + dy * dy; const bp = ex * ex + ey * ey; const cp = fx * fx + fy * fy; return dx * (ey * cp - bp * fy) - dy * (ex * cp - bp * fx) + ap * (ex * fy - ey * fx) < 0; }