voronator
Version:
Compute the Voronoi diagram of a set of two-dimensional points.
193 lines (191 loc) • 7.65 kB
JavaScript
import Cell, {containsFinite, containsInfinite} from "./cell";
export default class Voronoi {
constructor(cells, circumcenters, delaunay, xmin, ymin, xmax, ymax) {
if (!((xmax = +xmax) >= (xmin = +xmin)) || !((ymax = +ymax) >= (ymin = +ymin))) throw new Error("invalid bounds");
this.cells = cells;
this.circumcenters = circumcenters;
this.delaunay = delaunay;
this.xmax = xmax, this.xmin = xmin;
this.ymax = ymax, this.ymin = ymin;
}
find(x, y) {
return this.cells[this.findIndex(x, y)];
}
findIndex(x, y) {
const {cells, delaunay: {halfedges, points, triangles}} = this;
if (cells.length === 0 || (x = +x, x !== x) || (y = +y, y !== y)) return -1;
let c = 0, c2 = (x - points[0]) ** 2 + (y - points[1]) ** 2;
while (true) {
let d = c, d2 = c2;
for (let T = cells[c].triangles, i = 0, n = T.length; i < n; ++i) {
let k = T[i] * 3;
switch (c) {
case triangles[k]: k = triangles[k + 1]; break;
case triangles[k + 1]: k = triangles[k + 2]; break;
case triangles[k + 2]: k = triangles[k]; break;
}
let k2 = (x - points[k * 2]) ** 2 + (y - points[k * 2 + 1]) ** 2;
if (k2 < d2) d2 = k2, d = k;
}
if (d === c) return d;
c = d, c2 = d2;
}
}
render(context) {
const {cells, circumcenters, delaunay: {halfedges, hull}} = this;
for (let i = 0, n = halfedges.length; i < n; ++i) {
const j = halfedges[i];
if (j < i) continue;
const ti = Math.floor(i / 3) * 2;
const tj = Math.floor(j / 3) * 2;
context.moveTo(circumcenters[ti], circumcenters[ti + 1]);
context.lineTo(circumcenters[tj], circumcenters[tj + 1]);
}
let node = hull;
do {
const t = Math.floor(node.t / 3) * 2;
const x = circumcenters[t];
const y = circumcenters[t + 1];
const p = this._project(x, y, cells[node.i].vn);
if (p) {
context.moveTo(x, y);
context.lineTo(p[0], p[1]);
}
} while ((node = node.next) !== hull);
}
renderBounds(context) {
context.rect(this.xmin, this.ymin, this.xmax - this.xmin, this.ymax - this.ymin);
}
_clip(points, v0, vn) {
return v0 ? this._clipInfinite(points, v0, vn) : this._clipFinite(points);
}
_clipFinite(points) {
const n = points.length;
let P = null, S;
let x0, y0, x1 = points[n - 2], y1 = points[n - 1];
let c0, c1 = this._regioncode(x1, y1);
let e0, e1;
for (let i = 0; i < n; i += 2) {
x0 = x1, y0 = y1, x1 = points[i], y1 = points[i + 1];
c0 = c1, c1 = this._regioncode(x1, y1);
if (c0 === 0 && c1 === 0) {
e0 = e1, e1 = 0;
if (P) P.push(x1, y1);
else P = [x1, y1];
} else if (S = this._clipSegment(x0, y0, x1, y1, c0, c1)) {
const [sx0, sy0, sx1, sy1] = S;
if (c0) {
e0 = e1, e1 = this._edgecode(sx0, sy0);
if (e0 && e1) this._edge(points, e0, e1, P);
if (P) P.push(sx0, sy0);
else P = [sx0, sy0];
}
e0 = e1, e1 = this._edgecode(sx1, sy1);
if (e0 && e1) this._edge(points, e0, e1, P);
if (P) P.push(sx1, sy1);
else P = [sx1, sy1];
}
}
if (P) {
e0 = e1, e1 = this._edgecode(P[0], P[1]);
if (e0 && e1) this._edge(points, e0, e1, P);
} else if (containsFinite(points, (this.xmin + this.xmax) / 2, (this.ymin + this.ymax) / 2)) {
return [this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax, this.xmin, this.ymin];
}
return P;
}
_clipSegment(x0, y0, x1, y1, c0, c1) {
while (true) {
if (c0 === 0 && c1 === 0) return [x0, y0, x1, y1];
if (c0 & c1) return;
let x, y, c = c0 || c1;
if (c & 0b1000) x = x0 + (x1 - x0) * (this.ymax - y0) / (y1 - y0), y = this.ymax, c ^= 0b1000;
else if (c & 0b0100) x = x0 + (x1 - x0) * (this.ymin - y0) / (y1 - y0), y = this.ymin, c ^= 0b0100;
else if (c & 0b0010) y = y0 + (y1 - y0) * (this.xmax - x0) / (x1 - x0), x = this.xmax, c ^= 0b0010;
else y = y0 + (y1 - y0) * (this.xmin - x0) / (x1 - x0), x = this.xmin, c ^= 0b0001;
if (c0) x0 = x, y0 = y, c0 = c;
else x1 = x, y1 = y, c1 = c;
}
}
// TODO Consolidate corner traversal code using edge?
_clipInfinite(points, v0, vn) {
let P = Array.from(points), p;
if (p = this._project(P[0], P[1], v0)) P.unshift(p[0], p[1]);
if (p = this._project(P[P.length - 2], P[P.length - 1], vn)) P.unshift(p[0], p[1]);
if (P = this._clipFinite(P)) {
for (let i = 0, n = P.length, c0, c1 = this._edgecode(P[n - 2], P[n - 1]); i < n; i += 2) {
c0 = c1, c1 = this._edgecode(P[i], P[i + 1]);
if (c0 && c1) {
while (c0 !== c1) {
let cx, cy;
switch (c0) {
case 0b0101: c0 = 0b0100; continue; // top-left
case 0b0100: c0 = 0b0110, cx = this.xmax, cy = this.ymin; break; // top
case 0b0110: c0 = 0b0010; continue; // top-right
case 0b0010: c0 = 0b1010, cx = this.xmax, cy = this.ymax; break; // right
case 0b1010: c0 = 0b1000; continue; // bottom-right
case 0b1000: c0 = 0b1001, cx = this.xmin, cy = this.ymax; break; // bottom
case 0b1001: c0 = 0b0001; continue; // bottom-left
case 0b0001: c0 = 0b0101, cx = this.xmin, cy = this.ymin; break; // left
}
if (containsInfinite(points, v0, vn, cx, cy)) {
P.splice(i, 0, cx, cy), n += 2, i += 2;
}
}
}
}
} else if (containsInfinite(points, v0, vn, (this.xmin + this.xmax) / 2, (this.ymin + this.ymax) / 2)) {
P.push(this.xmin, this.ymin, this.xmax, this.ymin, this.xmax, this.ymax, this.xmin, this.ymax);
}
return P;
}
// TODO Allow containsInfinite instead of contains for clipInfinite?
_edge(points, e0, e1, P) {
while (e0 !== e1) {
let cx, cy;
switch (e0) {
case 0b0101: e0 = 0b0100; continue; // top-left
case 0b0100: e0 = 0b0110, cx = this.xmax, cy = this.ymin; break; // top
case 0b0110: e0 = 0b0010; continue; // top-right
case 0b0010: e0 = 0b1010, cx = this.xmax, cy = this.ymax; break; // right
case 0b1010: e0 = 0b1000; continue; // bottom-right
case 0b1000: e0 = 0b1001, cx = this.xmin, cy = this.ymax; break; // bottom
case 0b1001: e0 = 0b0001; continue; // bottom-left
case 0b0001: e0 = 0b0101, cx = this.xmin, cy = this.ymin; break; // left
}
if (containsFinite(points, cx, cy)) {
P.push(cx, cy);
}
}
}
_project(x0, y0, [vx, vy]) {
let t = Infinity, c, x, y;
if (vy < 0) { // top
if (y0 <= this.ymin) return;
if ((c = (this.ymin - y0) / vy) < t) y = this.ymin, x = x0 + (t = c) * vx;
} else if (vy > 0) { // bottom
if (y0 >= this.ymax) return;
if ((c = (this.ymax - y0) / vy) < t) y = this.ymax, x = x0 + (t = c) * vx;
}
if (vx > 0) { // right
if (x0 >= this.xmax) return;
if ((c = (this.xmax - x0) / vx) < t) x = this.xmax, y = y0 + (t = c) * vy;
} else if (vx < 0) { // left
if (x0 <= this.xmin) return;
if ((c = (this.xmin - x0) / vx) < t) x = this.xmin, y = y0 + (t = c) * vy;
}
return [x, y];
}
_edgecode(x, y) {
return (x === this.xmin ? 0b0001
: x === this.xmax ? 0b0010 : 0b0000)
| (y === this.ymin ? 0b0100
: y === this.ymax ? 0b1000 : 0b0000);
}
_regioncode(x, y) {
return (x < this.xmin ? 0b0001
: x > this.xmax ? 0b0010 : 0b0000)
| (y < this.ymin ? 0b0100
: y > this.ymax ? 0b1000 : 0b0000);
}
}