diff --git a/source/geometry/box3d.js b/source/geometry/box3d.js new file mode 100644 index 0000000..bea3d16 --- /dev/null +++ b/source/geometry/box3d.js @@ -0,0 +1,27 @@ +OV.Box3D = class +{ + constructor (min, max) + { + this.min = min; + this.max = max; + } + + GetMin () + { + return this.min; + } + + GetMax () + { + return this.max; + } + + GetCenter () + { + return new OV.Coord3D ( + (this.min.x + this.max.x) / 2.0, + (this.min.y + this.max.y) / 2.0, + (this.min.z + this.max.z) / 2.0 + ); + } +}; diff --git a/source/geometry/geometry.js b/source/geometry/geometry.js index b4893be..161813b 100644 --- a/source/geometry/geometry.js +++ b/source/geometry/geometry.js @@ -17,6 +17,16 @@ OV.IsGreater = function (a, b) return a - b > OV.Eps; }; +OV.IsLowerOrEqual = function (a, b) +{ + return b - a > -OV.Eps; +}; + +OV.IsGreaterOrEqual = function (a, b) +{ + return a - b > -OV.Eps; +}; + OV.IsEqual = function (a, b) { return Math.abs (b - a) < OV.Eps; diff --git a/source/geometry/octree.js b/source/geometry/octree.js new file mode 100644 index 0000000..cb18b90 --- /dev/null +++ b/source/geometry/octree.js @@ -0,0 +1,156 @@ +OV.OctreeNode = class +{ + constructor (boundingBox, level) + { + this.boundingBox = boundingBox; + this.level = level; + this.pointItems = []; + this.childNodes = []; + } + + AddPoint (point, data, options) + { + let node = this.FindNodeForPoint (point); + if (node === null) { + return false; + } + + if (node.FindPointDirectly (point) !== null) { + return false; + } + + if (node.pointItems.length < options.maxPointsPerNode || node.level >= options.maxTreeDepth) { + node.AddPointDirectly (point, data); + return true; + } else { + node.CreateChildNodes (); + let oldPointItems = node.pointItems; + node.pointItems = []; + for (let i = 0; i < oldPointItems.length; i++) { + let pointItem = oldPointItems[i]; + if (!node.AddPoint (pointItem.point, pointItem.data, options)) { + return false; + } + } + return node.AddPoint (point, data, options); + } + } + + FindPoint (point) + { + let node = this.FindNodeForPoint (point); + if (node === null) { + return null; + } + return node.FindPointDirectly (point); + } + + AddPointDirectly (point, data) + { + this.pointItems.push ({ + point : point, + data : data + }); + } + + FindPointDirectly (point) + { + for (let i = 0; i < this.pointItems.length; i++) { + let pointItem = this.pointItems[i]; + if (OV.CoordIsEqual3D (point, pointItem.point)) { + return pointItem.data; + } + } + return null; + } + + FindNodeForPoint (point) + { + if (!this.IsPointInBounds (point)) { + return null; + } + + if (this.childNodes.length === 0) { + return this; + } + + for (let i = 0; i < this.childNodes.length; i++) { + let childNode = this.childNodes[i]; + let foundNode = childNode.FindNodeForPoint (point); + if (foundNode !== null) { + return foundNode; + } + } + + return null; + } + + CreateChildNodes () + { + function AddChildNode (node, minX, minY, minZ, sizeX, sizeY, sizeZ) + { + let box = new OV.Box3D ( + new OV.Coord3D (minX, minY, minZ), + new OV.Coord3D (minX + sizeX, minY + sizeY, minZ + sizeZ) + ); + node.childNodes.push (new OV.OctreeNode (box, node.level + 1, node.options)); + } + + let min = this.boundingBox.min; + let center = this.boundingBox.GetCenter (); + let sizeX = (this.boundingBox.max.x - this.boundingBox.min.x) / 2.0; + let sizeY = (this.boundingBox.max.y - this.boundingBox.min.y) / 2.0; + let sizeZ = (this.boundingBox.max.z - this.boundingBox.min.z) / 2.0; + + AddChildNode (this, min.x, min.y, min.z, sizeX, sizeY, sizeZ); + AddChildNode (this, center.x, min.y, min.z, sizeX, sizeY, sizeZ); + AddChildNode (this, min.x, center.y, min.z, sizeX, sizeY, sizeZ); + AddChildNode (this, center.x, center.y, min.z, sizeX, sizeY, sizeZ); + AddChildNode (this, min.x, min.y, center.z, sizeX, sizeY, sizeZ); + AddChildNode (this, center.x, min.y, center.z, sizeX, sizeY, sizeZ); + AddChildNode (this, min.x, center.y, center.z, sizeX, sizeY, sizeZ); + AddChildNode (this, center.x, center.y, center.z, sizeX, sizeY, sizeZ); + } + + IsPointInBounds (point) + { + let isEqual = + OV.IsGreaterOrEqual (point.x, this.boundingBox.min.x) && + OV.IsGreaterOrEqual (point.y, this.boundingBox.min.y) && + OV.IsGreaterOrEqual (point.z, this.boundingBox.min.z) && + OV.IsLowerOrEqual (point.x, this.boundingBox.max.x) && + OV.IsLowerOrEqual (point.y, this.boundingBox.max.y) && + OV.IsLowerOrEqual (point.z, this.boundingBox.max.z); + return isEqual; + } +}; + +OV.Octree = class +{ + constructor (boundingBox, options) + { + this.options = { + maxPointsPerNode : 10, + maxTreeDepth : 10 + }; + if (options !== undefined) { + if (options.maxPointsPerNode !== undefined) { + this.options.maxPointsPerNode = options.maxPointsPerNode; + } + if (options.maxTreeDepth !== undefined) { + this.options.maxTreeDepth = options.maxTreeDepth; + } + } + this.rootNode = new OV.OctreeNode (boundingBox, 0, this.options); + } + + AddPoint (point, data) + { + return this.rootNode.AddPoint (point, data, this.options); + } + + FindPoint (point) + { + return this.rootNode.FindPoint (point); + } +}; diff --git a/test/tests/geometry_test.js b/test/tests/geometry_test.js index fe949cb..d990b2d 100644 --- a/test/tests/geometry_test.js +++ b/test/tests/geometry_test.js @@ -1,5 +1,11 @@ var assert = require ('assert'); +function SeededRandom (from, to, seed) +{ + var random = ((seed * 9301 + 49297) % 233280) / 233280; + return random * (to - from) + from; +} + function CreateYRot90Quaternion () { let angle = Math.PI / 2.0; @@ -15,6 +21,37 @@ function CreateYRot90Quaternion () return quaternion; } +describe ('Comparison', function () { + it ('IsGreater', function () { + assert (OV.IsEqual (1.0, 1.0)); + assert (OV.IsEqual (1.0, 1.000000001)); + assert (!OV.IsEqual (1.0, 1.0001)); + }); + + it ('IsGreater', function () { + assert (OV.IsGreater (1.0, 0.0)); + assert (OV.IsGreater (1.0001, 1.0)); + assert (!OV.IsGreater (1.000000001, 1.0)); + + assert (OV.IsGreaterOrEqual (1.0001, 1.0)); + assert (OV.IsGreaterOrEqual (1.000000001, 1.0)); + assert (OV.IsGreaterOrEqual (0.999999999, 1.0)); + assert (!OV.IsGreaterOrEqual (0.999, 1.0)); + }); + + it ('IsLower', function () { + assert (OV.IsLower (0.0, 1.0)); + assert (OV.IsLower (1.0, 1.0001)); + assert (!OV.IsLower (1.0, 1.000000001)); + + assert (OV.IsLowerOrEqual (1.0, 1.0001)); + assert (OV.IsLowerOrEqual (1.0, 1.000000001)); + assert (OV.IsLowerOrEqual (1.0, 0.999999999)); + assert (!OV.IsLowerOrEqual (1.0, 0.999)); + }); +}); + + describe ('Coord', function () { it ('Length', function () { var c = new OV.Coord3D (2.0, 0.0, 0.0); @@ -144,3 +181,101 @@ describe ('Tween', function() { assert (OV.CoordIsEqual3D (segments[segments.length - 1], end)); }); }); + +describe ('Octree', function() { + it ('Add Point', function () { + let octree = new OV.Octree (new OV.Box3D ( + new OV.Coord3D (-10.0, -10.0, -10.0), + new OV.Coord3D (10.0, 10.0, 10.0) + )); + + let p1 = new OV.Coord3D (0.0, 0.0, 0.0); + assert (octree.AddPoint (p1, 'p1')); + assert (!octree.AddPoint (p1, 'p2')); + assert.strictEqual (octree.FindPoint (p1), 'p1') + }); + + it ('Add Points', function () { + let octree = new OV.Octree (new OV.Box3D ( + new OV.Coord3D (-10.0, -10.0, -10.0), + new OV.Coord3D (10.0, 10.0, 10.0) + )); + + let p1 = new OV.Coord3D (0.0, 0.0, 0.0); + let p2 = new OV.Coord3D (1.0, 1.0, 1.0); + let p3 = new OV.Coord3D (-1.0, 1.0, 1.0); + let p4 = new OV.Coord3D (-1.0, -1.0, 1.0); + let p5 = new OV.Coord3D (-1.0, -1.0, -1.0); + let p6 = new OV.Coord3D (2.0, 2.0, 2.0); + let p7 = new OV.Coord3D (-2.0, 2.0, 2.0); + let p8 = new OV.Coord3D (-2.0, -2.0, 2.0); + let p9 = new OV.Coord3D (-2.0, -2.0, -2.0); + + assert (octree.AddPoint (p1, 'p1')); + assert (octree.AddPoint (p2, 'p2')); + assert (octree.AddPoint (p3, 'p3')); + assert (octree.AddPoint (p4, 'p4')); + assert (octree.AddPoint (p5, 'p5')); + assert (octree.AddPoint (p6, 'p6')); + assert (octree.AddPoint (p7, 'p7')); + assert (octree.AddPoint (p8, 'p8')); + assert (octree.AddPoint (p9, 'p9')); + + assert.strictEqual (octree.FindPoint (p1), 'p1'); + assert.strictEqual (octree.FindPoint (p2), 'p2'); + assert.strictEqual (octree.FindPoint (p3), 'p3'); + assert.strictEqual (octree.FindPoint (p4), 'p4'); + assert.strictEqual (octree.FindPoint (p5), 'p5'); + assert.strictEqual (octree.FindPoint (p6), 'p6'); + assert.strictEqual (octree.FindPoint (p7), 'p7'); + assert.strictEqual (octree.FindPoint (p8), 'p8'); + assert.strictEqual (octree.FindPoint (p9), 'p9'); + }); + + it ('Add Points On Boundaries', function () { + let octree = new OV.Octree (new OV.Box3D ( + new OV.Coord3D (-10.0, -10.0, -10.0), + new OV.Coord3D (10.0, 10.0, 10.0) + )); + + let p1 = new OV.Coord3D (10.0, 10.0, 10.0); + let p2 = new OV.Coord3D (-10.0, -10.0, -10.0); + let p3 = new OV.Coord3D (20.0, 20.0, 20.0); + + assert (octree.AddPoint (p1, 'p1')); + assert (octree.AddPoint (p2, 'p2')); + assert (!octree.AddPoint (p3, 'p3')); + + assert.strictEqual (octree.FindPoint (p1), 'p1'); + assert.strictEqual (octree.FindPoint (p2), 'p2'); + assert.strictEqual (octree.FindPoint (p3), null); + }); + + it ('Stress Test', function () { + let box = new OV.Box3D ( + new OV.Coord3D (-10.0, -10.0, -10.0), + new OV.Coord3D (10.0, 10.0, 10.0) + ); + + let octree = new OV.Octree (box); + + let count = 1000; + let seed = 1; + let points = []; + for (let i = 0; i < count; i++) { + let x = SeededRandom (-10.0, 10.0, seed++); + let y = SeededRandom (-10.0, 10.0, seed++); + let z = SeededRandom (-10.0, 10.0, seed++); + let point = new OV.Coord3D (x, y, z); + points.push (point); + } + for (let i = 0; i < count; i++) { + let point = points[i]; + assert (octree.AddPoint (point, i.toString ())); + } + for (let i = 0; i < count; i++) { + let point = points[i]; + assert.strictEqual (octree.FindPoint (point), i.toString ()); + } + }); +}); diff --git a/tools/config.json b/tools/config.json index 470cb49..310d571 100644 --- a/tools/config.json +++ b/tools/config.json @@ -10,6 +10,8 @@ "source/geometry/geometry.js", "source/geometry/coord2d.js", "source/geometry/coord3d.js", + "source/geometry/box3d.js", + "source/geometry/octree.js", "source/geometry/matrix.js", "source/geometry/transformation.js", "source/geometry/tween.js",