var geom = require('pex-geom');
var Vec3 = geom.Vec3;
3D Three data structure for fast spatial point indexing
var octree = new Octree(new Vec3(-1,-1,1), new Vec3(2,2,2));
octree.add(new Vec3(0.2, 0, 0));
octree.add(new Vec3(0.5, 0, 0));
octree.add(new Vec3(0, 0.12, 0));
octree.add(new Vec3(0, 0, -0.23));
octree.findNearestPoint(new Vec3(0, 0, 0));
var geom = require('pex-geom');
var Vec3 = geom.Vec3;
position
- far bottom left corner position of the octree bounding box { Vec3 }size
- size of the octree bounding box { Vec3 }accuracy
- precision at which two points are considered the same { Number/Float = 0 }
function Octree(position, size, accuracy) {
this.maxDistance = Math.max(size.x, Math.max(size.y, size.z));
this.accuracy = 0;
this.root = new Octree.Cell(this, position, size, 0);
}
Octree.fromBoundingBox = function (bbox) {
return new Octree(bbox.min.clone(), bbox.getSize().clone());
};
Octree.MaxLevel = 8;
Add point to octreep
- point to add { Vec3 }data
- optional data to attach to the point { Any }
Octree.prototype.add = function (p, data) {
this.root.add(p, data);
};
Octree.prototype.has = function (p) {
return this.root.has(p);
};
Finds closest point to the given onep
- point that we are searching around { Vec3 }o
- options { Object }
Available optionsincludeData
- return both point and it’s data { bool = false }maxDist
- don’t include points further than maxDist, defaults to { Number = Inifinity }notSelf
- return point only if different than submited point { bool = false }
Returns:
{ point:Vec3, data:Any } - object with both point and it’s data if includeData is true
Vec3 - just the point otherwise
Octree.prototype.findNearestPoint = function (p, options) {
options.includeData = options.includeData ? options.includeData : false;
options.bestDist = options.maxDist ? options.maxDist : Infinity;
options.notSelf = options.notSelf ? options.notSelf : false;
var result = this.root.findNearestPoint(p, options);
if (result) {
if (options.includeData) return result;
else return result.point;
}
else return null;
};
Finds nearby points to the given one within radius rp
- point that we are searching around { Vec3 }r
- search radius { Number }o
- options { Object }
Available optionsincludeData
- return both point and it’s data { bool = false }maxDist
- don’t include points further than maxDist, defaults to { Number = Inifinity }notSelf
- return point only if different than submited point { bool = false }
Returns:
{ points: Array of Vec3, data: Array of Any } - object with both point and it’s data if includeData is true
Octree.prototype.findNearbyPoints = function (p, r, options) {
options = options || { };
var result = { points: [], data: [] };
this.root.findNearbyPoints(p, r, result, options);
return result;
};
Return all octree cells at given levellevel
- level of cells to retrieve, e.g. root is 0 { Number/Int }
Note: the function parameter list is (cell, level, result) as it will be called recursively but the usage is simply getAllCellsAtLevel(n);
Returns { Array of Cell objects }, each cell has points property with all the points withing the cell
Octree.prototype.getAllCellsAtLevel = function (cell, level, result) {
if (typeof level == 'undefined') {
level = cell;
cell = this.root;
}
result = result || [];
if (cell.level == level) {
if (cell.points.length > 0) {
result.push(cell);
}
return result;
} else {
cell.children.forEach(function (child) {
this.getAllCellsAtLevel(child, level, result);
}.bind(this));
return result;
}
};
Octree.Cell = function (tree, position, size, level) {
this.tree = tree;
this.position = position;
this.size = size;
this.level = level;
this.points = [];
this.data = [];
this.temp = new Vec3(); //temp vector for distance calculation
this.children = [];
};
Octree.Cell.prototype.has = function (p) {
if (!this.contains(p))
return null;
if (this.children.length > 0) {
for (var i = 0; i < this.children.length; i++) {
var duplicate = this.children[i].has(p);
if (duplicate) {
return duplicate;
}
}
return null;
} else {
var minDistSqrt = this.tree.accuracy * this.tree.accuracy;
for (var i = 0; i < this.points.length; i++) {
var o = this.points[i];
var distSq = p.squareDistance(o);
if (distSq <= minDistSqrt) {
return o;
}
}
return null;
}
};
Octree.Cell.prototype.add = function (p, data) {
this.points.push(p);
this.data.push(data);
if (this.children.length > 0) {
this.addToChildren(p, data);
} else {
if (this.points.length > 1 && this.level < Octree.MaxLevel) {
this.split();
}
}
};
Octree.Cell.prototype.addToChildren = function (p, data) {
for (var i = 0; i < this.children.length; i++) {
if (this.children[i].contains(p)) {
this.children[i].add(p, data);
break;
}
}
};
Octree.Cell.prototype.contains = function (p) {
return p.x >= this.position.x - this.tree.accuracy
&& p.y >= this.position.y - this.tree.accuracy
&& p.z >= this.position.z - this.tree.accuracy
&& p.x < this.position.x + this.size.x + this.tree.accuracy
&& p.y < this.position.y + this.size.y + this.tree.accuracy
&& p.z < this.position.z + this.size.z + this.tree.accuracy;
};
1 2 3 4 5 6 7 8
Octree.Cell.prototype.split = function () {
var x = this.position.x;
var y = this.position.y;
var z = this.position.z;
var w2 = this.size.x / 2;
var h2 = this.size.y / 2;
var d2 = this.size.z / 2;
this.children.push(new Octree.Cell(this.tree, Vec3.create(x, y, z), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x + w2, y, z), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x, y, z + d2), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x + w2, y, z + d2), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x, y + h2, z), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x + w2, y + h2, z), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x, y + h2, z + d2), Vec3.create(w2, h2, d2), this.level + 1));
this.children.push(new Octree.Cell(this.tree, Vec3.create(x + w2, y + h2, z + d2), Vec3.create(w2, h2, d2), this.level + 1));
for (var i = 0; i < this.points.length; i++) {
this.addToChildren(this.points[i], this.data[i]);
}
};
Octree.Cell.prototype.squareDistanceToCenter = function(p) {
var dx = p.x - (this.position.x + this.size.x / 2);
var dy = p.y - (this.position.y + this.size.y / 2);
var dz = p.z - (this.position.z + this.size.z / 2);
return dx * dx + dy * dy + dz * dz;
}
Octree.Cell.prototype.findNearestPoint = function (p, options) {
var nearest = null;
var nearestData = null;
var bestDist = options.bestDist;
if (this.points.length > 0 && this.children.length == 0) {
for (var i = 0; i < this.points.length; i++) {
var dist = this.points[i].distance(p);
if (dist <= bestDist) {
if (dist == 0 && options.notSelf)
continue;
bestDist = dist;
nearest = this.points[i];
nearestData = this.data[i];
}
}
}
var children = this.children;
traverse children in order from closest to furthest
var children = this.children
.map(function(child) { return { child: child, dist: child.squareDistanceToCenter(p) } })
.sort(function(a, b) { return a.dist - b.dist; })
.map(function(c) { return c.child; });
if (children.length > 0) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.points.length > 0) {
if (p.x < child.position.x - bestDist || p.x > child.position.x + child.size.x + bestDist ||
p.y < child.position.y - bestDist || p.y > child.position.y + child.size.y + bestDist ||
p.z < child.position.z - bestDist || p.z > child.position.z + child.size.z + bestDist
) {
continue;
}
var childNearest = child.findNearestPoint(p, options);
if (!childNearest || !childNearest.point) {
continue;
}
var childNearestDist = childNearest.point.distance(p);
if (childNearestDist < bestDist) {
nearest = childNearest.point;
bestDist = childNearestDist;
nearestData = childNearest.data;
}
}
}
}
return {
point: nearest,
data: nearestData
}
};
Octree.Cell.prototype.findNearbyPoints = function (p, r, result, options) {
if (this.points.length > 0 && this.children.length == 0) {
for (var i = 0; i < this.points.length; i++) {
var dist = this.points[i].distance(p);
if (dist <= r) {
if (dist == 0 && options.notSelf)
continue;
result.points.push(this.points[i]);
if (options.includeData) result.data.push(this.data[i]);
}
}
}
children order doesn’t matter
var children = this.children;
if (children.length > 0) {
for (var i = 0; i < children.length; i++) {
var child = children[i];
if (child.points.length > 0) {
if (p.x < child.position.x - r || p.x > child.position.x + child.size.x + r ||
p.y < child.position.y - r || p.y > child.position.y + child.size.y + r ||
p.z < child.position.z - r || p.z > child.position.z + child.size.z + r
) {
continue;
}
child.findNearbyPoints(p, r, result, options);
}
}
}
};
module.exports = Octree;