delaunay
Version:
Delaunay triangulation in arbitrary dimensions
362 lines (294 loc) • 10.5 kB
JavaScript
/* This is a JavaScript implementation of Delaunay triangulation. Ideally, it
* would function in any number of dimensions; the only restriction is in
* calculating the circumsphere of a simplex, and I can't seem to find the
* algorithm to do it. As such, this code currently just works in 2 or 3
* dimensions.
*
* The theory behind Delaunay triangulation can be found here:
*
* http://paulbourke.net/papers/triangulate/ */
var Delaunay;
(function() {
"use strict";
var M = typeof Matrix !== "undefined" ? Matrix : require("./matrix"),
Simplex = function(indices, vertices, n) {
var list = new Array(indices.length),
i;
for(i = list.length; i--; )
list[i] = vertices[indices[i]];
this.vertices = indices;
this.center = Delaunay.circumcenter(list, n);
this.radius = Delaunay.distanceSquared(this.center, list[0], n);
};
Simplex.prototype = {
passed: function(vertex, n) {
var d = vertex[0] - this.center[0];
return d > 0.0 && d * d > this.radius;
},
contains: function(vertex, n) {
return Delaunay.distanceSquared(this.center, vertex, n) <= this.radius;
},
/* FIXME: This can be done more efficiently, since the vertices are already
* in sorted order, there's no need to do another sort. Just iterate every
* array formed by removing one vertex. */
addEdges: function(n, edges) {
var edge, i, j;
for(i = n + 1; i--; ) {
edge = new Array(n);
for(j = n; j--; )
edge[j] = this.vertices[(i + j) % this.vertices.length];
edge.sort();
edges.push(edge);
}
}
};
Delaunay = {
resolve: function(obj, key) {
var i;
if(key !== undefined) {
if(Array.isArray(key))
for(i = 0; obj !== undefined && i !== key.length; ++i)
obj = obj[key[i]];
else if(obj !== undefined)
obj = obj[key];
}
return obj;
},
dimensions: function(vertices) {
var d = 0,
i = vertices.length;
if(i) {
d = vertices[--i].length;
while(i--)
if(vertices[i].length < d)
d = vertices[i].length;
}
return d;
},
/* Return the bounding box (two vertices) enclosing each given vertex. */
boundingBox: function(vertices, n) {
var min = new Array(n),
max = new Array(n),
i, j, pos;
/* Given some objects, find the bounding box of those objects. */
if(vertices.length) {
for(j = n; j--; ) {
min[j] = Number.POSITIVE_INFINITY;
max[j] = Number.NEGATIVE_INFINITY;
}
for(i = vertices.length; i--; ) {
pos = vertices[i];
for(j = n; j--; ) {
if(pos[j] < min[j])
min[j] = pos[j];
if(pos[j] > max[j])
max[j] = pos[j];
}
}
}
/* No points? Well then, you get a degenerate bounding box. Dumbface. */
else
for(j = n; j--; ) {
min[j] = 0;
max[j] = 0;
}
return [min, max];
},
boundingSimplex: function(vertices, n) {
var box = Delaunay.boundingBox(vertices, n),
min = box[0],
max = box[1],
simplex = new Array(n + 1),
i, j, w, pos;
/* Scale up the bounding box. FIXME: This is something of a fudge, in
* order to make all triangles formed against the super triangle super
* long and skinny, so that the triangles will be formed against the hull
* of shapes, instead. That's kludgy. It'd be better to make the
* algorithm as a whole robust against that kind of silliness. */
for(j = n; j--; ) {
w = 2048 + max[j] - min[j];
min[j] -= w * 1;
max[j] += w * 3;
}
/* The first vertex is just the minimum vertex of the bounding box. */
simplex[0] = min;
/* Every subsequent vertex is the max along that axis. */
for(i = n; i--; ) {
pos = simplex[1 + i] = new Array(n);
for(j = n; j--; )
pos[j] = (i !== j ? min : max)[j];
}
/* Return the simplex. */
return simplex;
},
bisectors: function(vertices, n) {
var m = n + 1,
matrix = new Array(m * n),
i, j, a, b, c;
for(i = n; i--; ) {
a = vertices[i + 0];
b = vertices[i + 1];
c = 0;
for(j = n; j--; )
c += (a[j] + b[j]) * (matrix[i * m + j] = a[j] - b[j]);
matrix[i * m + n] = c * -0.5;
}
return matrix;
},
cross: function(matrix, n) {
if(matrix.length !== n * n + n)
throw new Error("Invalid matrix shape.");
var m = n + 1,
sgn = 1,
vec = new Array(m),
sq = new Array(n * n),
i, j, k;
for(i = 0; i !== m; ++i) {
/* If it's the first time through the loop, initialize the square
* matrix to hold every column but the first. */
if(i === 0)
for(j = n; j--; )
for(k = n; k--; )
sq[j * n + k] = matrix[j * m + k + 1];
/* Every other time, just replace the one column that's no longer
* relevant. */
else {
k = i - 1;
for(j = n; j--; )
sq[j * n + k] = matrix[j * m + k];
}
vec[i] = sgn * M.determinant(sq);
sgn = -sgn;
}
return vec;
},
/* FIXME: This should probably test the points for collinearity and use a
* different algorithm in that case, in order to improve robustness. */
circumcenter: function(vertices, n) {
if(vertices.length !== n + 1)
throw new Error("A " + n + "-simplex requires " + (n + 1) + " vertices.");
/* Find the position of the circumcenter in homogeneous coordinates. */
var matrix = Delaunay.bisectors(vertices, n),
center = Delaunay.cross(matrix, n),
j;
/* Convert into Euclidean coordinates. */
for(j = n; j--; )
center[j] /= center[n];
center.length = n;
/* Return the results. */
return center;
},
distanceSquared: function(a, b, n) {
var d = 0,
i, t;
for(i = n; i--; ) {
t = b[i] - a[i];
d += t * t;
}
return d;
},
distance: function(a, b, n) {
return Math.sqrt(Delaunay.distanceSquared(a, b, n));
},
isSameEdge: function(a, b) {
var i;
if(a.length !== b.length)
return false;
for(i = a.length; i--; )
if(a[i] !== b[i])
return false;
return true;
},
/* FIXME: By using a set data structure, this can be made O(n log n). */
removeDuplicateEdges: function(edges) {
var i, j;
for(i = edges.length; i--; )
for(j = i; j--; )
if(Delaunay.isSameEdge(edges[i], edges[j])) {
edges.splice(i, 1);
edges.splice(j, 1);
--i;
break;
}
},
triangulate: function(objects, key) {
var v = objects.length,
i, j;
/* Resolve all objects, so we never have to do it again. */
objects = objects.slice(0);
for(i = objects.length; i--; )
objects[i] = Delaunay.resolve(objects[i], key);
/* Get the dimensionality of the objects. */
var n = Delaunay.dimensions(objects);
if(n < 2 || n > 3)
throw new Error("The Delaunay module currently only supports 2D or 3D data.");
/* Sort the objects on an axis so we can get O(n log n) behavior. Sadly,
* we also need to keep track of their original position in the array, so
* we wrap the objects to track that and then unwrap them again. */
for(i = objects.length; i--; )
objects[i] = {index: i, position: objects[i]};
/* FIXME: It'd be better to sort on the longest axis, rather than on an
* arbitrary axis, since it'll lower the constant factor. */
objects.sort(function(a, b) { return b.position[0] - a.position[0]; });
var indices = new Array(objects.length);
for(i = objects.length; i--; ) {
indices[i] = objects[i].index;
objects[i] = objects[i].position;
}
/* Add the vertices of the bounding simplex to the object list. It's okay
* that these vertices aren't sorted like the others, since they're never
* going to be iterated over. */
Array.prototype.push.apply(
objects,
Delaunay.boundingSimplex(objects, n)
);
/* Initialize the simplex list to the bounding simplex. */
var list = new Array(n + 1);
for(i = list.length; i--; )
list[i] = v + i;
var open = [new Simplex(list, objects, n)],
closed = [],
edges = [];
for(i = v; i--; edges.length = 0) {
for(j = open.length; j--; ) {
/* If this vertex is past the simplex, then we're never going to
* intersect it again, so remove it from the open list and move it to
* the closed list. */
if(open[j].passed(objects[i], n)) {
closed.push(open[j]);
open.splice(j, 1);
}
/* Otherwise, if the simplex contains the vertex, it needs to get
* split apart. */
else if(open[j].contains(objects[i], n)) {
open[j].addEdges(n, edges);
open.splice(j, 1);
}
}
Delaunay.removeDuplicateEdges(edges);
for(j = edges.length; j--; ) {
edges[j].unshift(i);
open.push(new Simplex(edges[j], objects, n));
}
}
/* Move all open simplices into the closed list. */
Array.prototype.push.apply(closed, open);
open.length = 0;
/* Build and return the final list of simplex vertex indices. */
list.length = 0;
simplex: for(i = closed.length; i--; ) {
/* If any of the vertices are from the bounding simplex, skip adding
* this simplex to the output list. */
for(j = closed[i].vertices.length; j--; )
if(closed[i].vertices[j] >= v)
continue simplex;
for(j = 0; j < closed[i].vertices.length; j++)
list.push(indices[closed[i].vertices[j]]);
}
return list;
}
};
/* If we're in Node, export our module as a Node module. */
if(typeof module !== "undefined")
module.exports = Delaunay;
}());