Skip to content

Commit 2ea4d90

Browse files
committed
initial commit
1 parent a58c41a commit 2ea4d90

File tree

3 files changed

+319
-1
lines changed

3 files changed

+319
-1
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
kdtree
22
======
33

4-
Node.js KDTree implementation with lazy indexing for sparse object collision detection
4+
Node.js KDTree implementation with lazy indexing for sparse object collision detection. See [this fiddle](http://jsfiddle.net/0x0539/vShP4/12/embedded/result/) for a running example.

index.js

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
var ShapeSet = function () {
2+
this.shapes = {};
3+
this.length = 0;
4+
};
5+
6+
ShapeSet.prototype.add = function (shape) {
7+
if (!(shape.id in this.shapes)) {
8+
this.shapes[shape.id] = shape;
9+
this.length++;
10+
return true;
11+
}
12+
return false;
13+
};
14+
15+
ShapeSet.prototype.remove = function (shape) {
16+
if (shape.id in this.shapes) {
17+
delete this.shapes[shape.id];
18+
this.length--;
19+
return true;
20+
}
21+
return false;
22+
};
23+
24+
ShapeSet.prototype.has = function (shape) {
25+
return shape.id in this.shapes;
26+
}
27+
28+
var Partition = function (dimension, shapeAry, shapeSet) {
29+
this.dimension = dimension;
30+
31+
this.shapeSet = shapeSet;
32+
this.shapeAry = shapeAry;
33+
this.lShapeSet = new ShapeSet();
34+
this.rShapeSet = new ShapeSet();
35+
36+
// Explicitly declaring fields without initial values.
37+
this.split = null;
38+
this.score = null;
39+
};
40+
41+
Partition.prototype.getLShapeAry = function (splitDimension) {
42+
return splitDimension ? new Ary(this.shapeAry.array, this.shapeAry.aryL, this.split) : this.shapeAry;
43+
};
44+
45+
Partition.prototype.getRShapeAry = function (splitDimension) {
46+
return splitDimension ? new Ary(this.shapeAry.array, this.split, this.shapeAry.aryR) : this.shapeAry;
47+
};
48+
49+
Partition.prototype.update = function () {
50+
var lastShape = null,
51+
prior = null,
52+
score = null,
53+
split = null,
54+
// Rightmost point of all shapes seen so far.
55+
F = null,
56+
// Number of shapes to the left if we were to split before the current shape.
57+
L = 0,
58+
// Number of shapes to the right if we were to split before the current shape.
59+
R = this.shapeSet.length;
60+
61+
// Get bounds of iteration, some subarray of this.shapeAry.
62+
var i1 = this.shapeAry.getAryL(),
63+
i2 = this.shapeAry.getAryR();
64+
65+
for (var i = i1; i < i2; i++) {
66+
var shape = this.shapeAry.array[i];
67+
68+
// Check that the current shape is actually part of this partition.
69+
if (this.shapeSet.has(shape)) {
70+
var s = shape.coords[this.dimension][0],
71+
f = shape.coords[this.dimension][1];
72+
73+
// Split before the current shape only if the current shape
74+
// is completely to the right of previous shapes.
75+
if (F != null && s > F) {
76+
var nextScore = L * L + R * R;
77+
if (score == null || nextScore < score) {
78+
// Split between lastShape and i.
79+
prior = lastShape;
80+
split = i;
81+
score = nextScore;
82+
} else {
83+
// Stop because scores will only increase from here.
84+
break;
85+
}
86+
}
87+
88+
L++;
89+
R--;
90+
91+
if (F == null || f > F) {
92+
F = f;
93+
lastShape = i;
94+
}
95+
}
96+
}
97+
98+
this.prior = prior;
99+
this.split = split;
100+
this.score = score;
101+
this.dirty = false;
102+
103+
// Mark the dirty bit if no valid split exists.
104+
if (this.score == null)
105+
this.dirty = true;
106+
107+
if (this.split != null) {
108+
// Get the physical location of the split.
109+
var splitLocation = this.shapeAry.array[this.split].coords[this.dimension][0];
110+
111+
// For each shape in the partition, either add to the left shape set or the right shape set.
112+
for (var id in this.shapeSet.shapes) {
113+
var shape = this.shapeSet.shapes[id],
114+
s = shape.coords[this.dimension][0];
115+
116+
// Compare physical location relative to the split.
117+
if (s < splitLocation) {
118+
if (this.lShapeSet.add(shape)) this.dirty = true;
119+
if (this.rShapeSet.remove(shape)) this.dirty = true;
120+
} else {
121+
if (this.rShapeSet.add(shape)) this.dirty = true;
122+
if (this.lShapeSet.remove(shape)) this.dirty = true;
123+
}
124+
}
125+
}
126+
};
127+
128+
var makeId = (function () {
129+
var nextId = 0;
130+
return function () {
131+
return nextId++;
132+
};
133+
}());
134+
135+
var Shape = function () {
136+
this.coords = [];
137+
for (var i = 0; i < arguments.length; i += 2)
138+
this.coords.push([arguments[i], arguments[i + 1]])
139+
this.id = makeId();
140+
};
141+
142+
Shape.prototype.overlaps = function (otherShape) {
143+
for (var i = 0; i < this.coords.length; i++) {
144+
var coords1 = this.coords[i],
145+
coords2 = otherShape.coords[i];
146+
if (coords1[0] > coords2[1] || coords1[1] < coords2[0])
147+
return false;
148+
}
149+
return true;
150+
};
151+
152+
var Ary = function (array, aryL, aryR) {
153+
this.array = array;
154+
this.aryL = aryL;
155+
this.aryR = aryR;
156+
};
157+
158+
Ary.prototype.getAryL = function () {
159+
return this.aryL;
160+
};
161+
162+
Ary.prototype.getAryR = function () {
163+
return this.aryR == null ? this.array.length : this.aryR;
164+
};
165+
166+
var Tree = function (shapeArys, shapeSet) {
167+
this.dimensions = shapeArys.length;
168+
this.partitions = [];
169+
for (var i = 0; i < shapeArys.length; i++)
170+
this.partitions.push(new Partition(i, shapeArys[i], shapeSet));
171+
this.shapeSet = shapeSet;
172+
this.step = null;
173+
};
174+
175+
Tree.prototype.query = function (queryShape, resultSet, step) {
176+
if (this.step != step) {
177+
this.update();
178+
this.step = step;
179+
}
180+
if (this.partition == null) {
181+
for (var shapeId in this.shapeSet.shapes) {
182+
var shape = this.shapeSet.shapes[shapeId];
183+
if (queryShape.overlaps(shape)) {
184+
resultSet.add(shape);
185+
}
186+
}
187+
} else {
188+
var splitDimension = this.partition.dimension,
189+
lBoundingShape = this.partition.shapeAry.array[this.partition.prior],
190+
rBoundingShape = this.partition.shapeAry.array[this.partition.split],
191+
lBoundary = lBoundingShape.coords[splitDimension][1],
192+
rBoundary = rBoundingShape.coords[splitDimension][0];
193+
194+
if (queryShape.coords[splitDimension][0] < lBoundary) {
195+
this.lTree.query(queryShape, resultSet, step);
196+
}
197+
198+
if (queryShape.coords[splitDimension][1] > rBoundary) {
199+
this.rTree.query(queryShape, resultSet, step);
200+
}
201+
}
202+
return resultSet;
203+
};
204+
205+
Tree.prototype.update = function () {
206+
if (this.partition != null) {
207+
this.partition.update();
208+
// Keep current partition because it hasn't changed.
209+
if (!this.partition.dirty)
210+
return;
211+
}
212+
213+
// Update partitions in case of new elements/reorderings.
214+
for (var i = 0; i < this.partitions.length; i++)
215+
if (this.partitions[i] != this.partition)
216+
this.partitions[i].update();
217+
218+
if (this.partition == null || this.partition.dirty) {
219+
var bestPartition = this.getBestPartition();
220+
221+
if (bestPartition == null) {
222+
delete this.partition;
223+
delete this.lTree;
224+
delete this.rTree;
225+
} else {
226+
this.partition = bestPartition;
227+
228+
var lShapeArys = [];
229+
var rShapeArys = [];
230+
231+
for (var i = 0; i < this.partitions.length; i++) {
232+
lShapeArys.push(this.partitions[i].getLShapeAry(i == this.partition.dimension));
233+
rShapeArys.push(this.partitions[i].getRShapeAry(i == this.partition.dimension));
234+
}
235+
236+
this.lTree = new Tree(lShapeArys, this.partition.lShapeSet);
237+
this.rTree = new Tree(rShapeArys, this.partition.rShapeSet);
238+
}
239+
}
240+
};
241+
242+
Tree.prototype.getBestPartition = function () {
243+
var best = null
244+
for (var i = 0; i < this.partitions.length; i++) {
245+
if (this.partitions[i] == null || this.partitions[i].score == null)
246+
continue;
247+
if (best == null || this.partitions[i].score < best.score)
248+
best = this.partitions[i];
249+
}
250+
return best;
251+
};
252+
253+
var Index = function (dimensions) {
254+
this.dimensions = dimensions;
255+
this.shapeArys = [];
256+
for (var i = 0; i < dimensions; i++)
257+
this.shapeArys.push(new Ary([], 0, null));
258+
this.shapeSet = new ShapeSet();
259+
this.tree = new Tree(this.shapeArys, this.shapeSet);
260+
this.step = 0;
261+
};
262+
263+
Index.prototype.add = function (shape) {
264+
if (shape.coords.length != this.dimensions)
265+
throw new Error("shape has the wrong number of dimensions");
266+
for (var i = 0; i < this.shapeArys.length; i++)
267+
this.shapeArys[i].array.push(shape);
268+
this.shapeSet.add(shape);
269+
};
270+
271+
Index.prototype.update = function () {
272+
for (var i = 0; i < this.shapeArys.length; i++)
273+
this.sort(this.shapeArys[i].array, i);
274+
this.step++;
275+
};
276+
277+
Index.prototype.query = function (shape) {
278+
if (shape.coords.length != this.dimensions)
279+
throw new Error("query shape has the wrong number of dimensions");
280+
return this.tree.query(shape, new ShapeSet(), this.step);
281+
};
282+
283+
Index.prototype.sort = function (ary, dim) {
284+
for (var i = 1; i < ary.length; i++) {
285+
var iShape = ary[i],
286+
Si = iShape.coords[dim][0];
287+
for (var j = i - 1; j >= 0; j--) {
288+
var jShape = ary[j],
289+
Sj = jShape.coords[dim][0];
290+
if (Si >= Sj)
291+
break;
292+
var temp = ary[j];
293+
ary[j] = ary[j + 1];
294+
ary[j + 1] = temp;
295+
}
296+
}
297+
};
298+
299+
exports.Index = Index;
300+
exports.BoundingBox = Shape;

package.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "kdtree",
3+
"version": "0.0.0-0",
4+
"main": "kdtree",
5+
"description": "A Node.js KDTree implementation with lazy indexing for sparse object collision detection.",
6+
"author": {
7+
"name": "Sebastian Goodman"
8+
},
9+
"license": "MIT",
10+
"repository": {
11+
"type": "git",
12+
"url": "git@github.com/0x0539/kdtree"
13+
},
14+
"engines": {
15+
"node": "0.8.x"
16+
},
17+
"dependencies": {}
18+
}

0 commit comments

Comments
 (0)