UNPKG

16.8 kBJavaScriptView Raw
1const {_CSGDEBUG, EPS} = require('./constants')
2const Vertex = require('./math/Vertex3')
3const Polygon = require('./math/Polygon3')
4
5// Returns object:
6// .type:
7// 0: coplanar-front
8// 1: coplanar-back
9// 2: front
10// 3: back
11// 4: spanning
12// In case the polygon is spanning, returns:
13// .front: a Polygon of the front part
14// .back: a Polygon of the back part
15function splitPolygonByPlane (plane, polygon) {
16 let result = {
17 type: null,
18 front: null,
19 back: null
20 }
21 // cache in local lets (speedup):
22 let planenormal = plane.normal
23 let vertices = polygon.vertices
24 let numvertices = vertices.length
25 if (polygon.plane.equals(plane)) {
26 result.type = 0
27 } else {
28 let thisw = plane.w
29 let hasfront = false
30 let hasback = false
31 let vertexIsBack = []
32 let MINEPS = -EPS
33 for (let i = 0; i < numvertices; i++) {
34 let t = planenormal.dot(vertices[i].pos) - thisw
35 let isback = (t < 0)
36 vertexIsBack.push(isback)
37 if (t > EPS) hasfront = true
38 if (t < MINEPS) hasback = true
39 }
40 if ((!hasfront) && (!hasback)) {
41 // all points coplanar
42 let t = planenormal.dot(polygon.plane.normal)
43 result.type = (t >= 0) ? 0 : 1
44 } else if (!hasback) {
45 result.type = 2
46 } else if (!hasfront) {
47 result.type = 3
48 } else {
49 // spanning
50 result.type = 4
51 let frontvertices = []
52 let backvertices = []
53 let isback = vertexIsBack[0]
54 for (let vertexindex = 0; vertexindex < numvertices; vertexindex++) {
55 let vertex = vertices[vertexindex]
56 let nextvertexindex = vertexindex + 1
57 if (nextvertexindex >= numvertices) nextvertexindex = 0
58 let nextisback = vertexIsBack[nextvertexindex]
59 if (isback === nextisback) {
60 // line segment is on one side of the plane:
61 if (isback) {
62 backvertices.push(vertex)
63 } else {
64 frontvertices.push(vertex)
65 }
66 } else {
67 // line segment intersects plane:
68 let point = vertex.pos
69 let nextpoint = vertices[nextvertexindex].pos
70 let intersectionpoint = plane.splitLineBetweenPoints(point, nextpoint)
71 let intersectionvertex = new Vertex(intersectionpoint)
72 if (isback) {
73 backvertices.push(vertex)
74 backvertices.push(intersectionvertex)
75 frontvertices.push(intersectionvertex)
76 } else {
77 frontvertices.push(vertex)
78 frontvertices.push(intersectionvertex)
79 backvertices.push(intersectionvertex)
80 }
81 }
82 isback = nextisback
83 } // for vertexindex
84 // remove duplicate vertices:
85 let EPS_SQUARED = EPS * EPS
86 if (backvertices.length >= 3) {
87 let prevvertex = backvertices[backvertices.length - 1]
88 for (let vertexindex = 0; vertexindex < backvertices.length; vertexindex++) {
89 let vertex = backvertices[vertexindex]
90 if (vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) {
91 backvertices.splice(vertexindex, 1)
92 vertexindex--
93 }
94 prevvertex = vertex
95 }
96 }
97 if (frontvertices.length >= 3) {
98 let prevvertex = frontvertices[frontvertices.length - 1]
99 for (let vertexindex = 0; vertexindex < frontvertices.length; vertexindex++) {
100 let vertex = frontvertices[vertexindex]
101 if (vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) {
102 frontvertices.splice(vertexindex, 1)
103 vertexindex--
104 }
105 prevvertex = vertex
106 }
107 }
108 if (frontvertices.length >= 3) {
109 result.front = new Polygon(frontvertices, polygon.shared, polygon.plane)
110 }
111 if (backvertices.length >= 3) {
112 result.back = new Polygon(backvertices, polygon.shared, polygon.plane)
113 }
114 }
115 }
116 return result
117}
118
119// # class PolygonTreeNode
120// This class manages hierarchical splits of polygons
121// At the top is a root node which doesn hold a polygon, only child PolygonTreeNodes
122// Below that are zero or more 'top' nodes; each holds a polygon. The polygons can be in different planes
123// splitByPlane() splits a node by a plane. If the plane intersects the polygon, two new child nodes
124// are created holding the splitted polygon.
125// getPolygons() retrieves the polygon from the tree. If for PolygonTreeNode the polygon is split but
126// the two split parts (child nodes) are still intact, then the unsplit polygon is returned.
127// This ensures that we can safely split a polygon into many fragments. If the fragments are untouched,
128// getPolygons() will return the original unsplit polygon instead of the fragments.
129// remove() removes a polygon from the tree. Once a polygon is removed, the parent polygons are invalidated
130// since they are no longer intact.
131// constructor creates the root node:
132const PolygonTreeNode = function () {
133 this.parent = null
134 this.children = []
135 this.polygon = null
136 this.removed = false
137}
138
139PolygonTreeNode.prototype = {
140 // fill the tree with polygons. Should be called on the root node only; child nodes must
141 // always be a derivate (split) of the parent node.
142 addPolygons: function (polygons) {
143 if (!this.isRootNode())
144 // new polygons can only be added to root node; children can only be splitted polygons
145 {
146 throw new Error('Assertion failed')
147 }
148 let _this = this
149 polygons.map(function (polygon) {
150 _this.addChild(polygon)
151 })
152 },
153
154 // remove a node
155 // - the siblings become toplevel nodes
156 // - the parent is removed recursively
157 remove: function () {
158 if (!this.removed) {
159 this.removed = true
160
161 if (_CSGDEBUG) {
162 if (this.isRootNode()) throw new Error('Assertion failed') // can't remove root node
163 if (this.children.length) throw new Error('Assertion failed') // we shouldn't remove nodes with children
164 }
165
166 // remove ourselves from the parent's children list:
167 let parentschildren = this.parent.children
168 let i = parentschildren.indexOf(this)
169 if (i < 0) throw new Error('Assertion failed')
170 parentschildren.splice(i, 1)
171
172 // invalidate the parent's polygon, and of all parents above it:
173 this.parent.recursivelyInvalidatePolygon()
174 }
175 },
176
177 isRemoved: function () {
178 return this.removed
179 },
180
181 isRootNode: function () {
182 return !this.parent
183 },
184
185 // invert all polygons in the tree. Call on the root node
186 invert: function () {
187 if (!this.isRootNode()) throw new Error('Assertion failed') // can only call this on the root node
188 this.invertSub()
189 },
190
191 getPolygon: function () {
192 if (!this.polygon) throw new Error('Assertion failed') // doesn't have a polygon, which means that it has been broken down
193 return this.polygon
194 },
195
196 getPolygons: function (result) {
197 let children = [this]
198 let queue = [children]
199 let i, j, l, node
200 for (i = 0; i < queue.length; ++i) { // queue size can change in loop, don't cache length
201 children = queue[i]
202 for (j = 0, l = children.length; j < l; j++) { // ok to cache length
203 node = children[j]
204 if (node.polygon) {
205 // the polygon hasn't been broken yet. We can ignore the children and return our polygon:
206 result.push(node.polygon)
207 } else {
208 // our polygon has been split up and broken, so gather all subpolygons from the children
209 queue.push(node.children)
210 }
211 }
212 }
213 },
214
215 // split the node by a plane; add the resulting nodes to the frontnodes and backnodes array
216 // If the plane doesn't intersect the polygon, the 'this' object is added to one of the arrays
217 // If the plane does intersect the polygon, two new child nodes are created for the front and back fragments,
218 // and added to both arrays.
219 splitByPlane: function (plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) {
220 if (this.children.length) {
221 let queue = [this.children]
222 let i
223 let j
224 let l
225 let node
226 let nodes
227 for (i = 0; i < queue.length; i++) { // queue.length can increase, do not cache
228 nodes = queue[i]
229 for (j = 0, l = nodes.length; j < l; j++) { // ok to cache length
230 node = nodes[j]
231 if (node.children.length) {
232 queue.push(node.children)
233 } else {
234 // no children. Split the polygon:
235 node._splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes)
236 }
237 }
238 }
239 } else {
240 this._splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes)
241 }
242 },
243
244 // only to be called for nodes with no children
245 _splitByPlane: function (plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) {
246 let polygon = this.polygon
247 if (polygon) {
248 let bound = polygon.boundingSphere()
249 let sphereradius = bound[1] + EPS // FIXME Why add imprecision?
250 let planenormal = plane.normal
251 let spherecenter = bound[0]
252 let d = planenormal.dot(spherecenter) - plane.w
253 if (d > sphereradius) {
254 frontnodes.push(this)
255 } else if (d < -sphereradius) {
256 backnodes.push(this)
257 } else {
258 let splitresult = splitPolygonByPlane(plane, polygon)
259 switch (splitresult.type) {
260 case 0:
261 // coplanar front:
262 coplanarfrontnodes.push(this)
263 break
264
265 case 1:
266 // coplanar back:
267 coplanarbacknodes.push(this)
268 break
269
270 case 2:
271 // front:
272 frontnodes.push(this)
273 break
274
275 case 3:
276 // back:
277 backnodes.push(this)
278 break
279
280 case 4:
281 // spanning:
282 if (splitresult.front) {
283 let frontnode = this.addChild(splitresult.front)
284 frontnodes.push(frontnode)
285 }
286 if (splitresult.back) {
287 let backnode = this.addChild(splitresult.back)
288 backnodes.push(backnode)
289 }
290 break
291 }
292 }
293 }
294 },
295
296 // PRIVATE methods from here:
297 // add child to a node
298 // this should be called whenever the polygon is split
299 // a child should be created for every fragment of the split polygon
300 // returns the newly created child
301 addChild: function (polygon) {
302 let newchild = new PolygonTreeNode()
303 newchild.parent = this
304 newchild.polygon = polygon
305 this.children.push(newchild)
306 return newchild
307 },
308
309 invertSub: function () {
310 let children = [this]
311 let queue = [children]
312 let i, j, l, node
313 for (i = 0; i < queue.length; i++) {
314 children = queue[i]
315 for (j = 0, l = children.length; j < l; j++) {
316 node = children[j]
317 if (node.polygon) {
318 node.polygon = node.polygon.flipped()
319 }
320 queue.push(node.children)
321 }
322 }
323 },
324
325 recursivelyInvalidatePolygon: function () {
326 let node = this
327 while (node.polygon) {
328 node.polygon = null
329 if (node.parent) {
330 node = node.parent
331 }
332 }
333 }
334}
335
336// # class Tree
337// This is the root of a BSP tree
338// We are using this separate class for the root of the tree, to hold the PolygonTreeNode root
339// The actual tree is kept in this.rootnode
340const Tree = function (polygons) {
341 this.polygonTree = new PolygonTreeNode()
342 this.rootnode = new Node(null)
343 if (polygons) this.addPolygons(polygons)
344}
345
346Tree.prototype = {
347 invert: function () {
348 this.polygonTree.invert()
349 this.rootnode.invert()
350 },
351
352 // Remove all polygons in this BSP tree that are inside the other BSP tree
353 // `tree`.
354 clipTo: function (tree, alsoRemovecoplanarFront) {
355 alsoRemovecoplanarFront = alsoRemovecoplanarFront ? true : false
356 this.rootnode.clipTo(tree, alsoRemovecoplanarFront)
357 },
358
359 allPolygons: function () {
360 let result = []
361 this.polygonTree.getPolygons(result)
362 return result
363 },
364
365 addPolygons: function (polygons) {
366 let _this = this
367 let polygontreenodes = polygons.map(function (p) {
368 return _this.polygonTree.addChild(p)
369 })
370 this.rootnode.addPolygonTreeNodes(polygontreenodes)
371 }
372}
373
374// # class Node
375// Holds a node in a BSP tree. A BSP tree is built from a collection of polygons
376// by picking a polygon to split along.
377// Polygons are not stored directly in the tree, but in PolygonTreeNodes, stored in
378// this.polygontreenodes. Those PolygonTreeNodes are children of the owning
379// Tree.polygonTree
380// This is not a leafy BSP tree since there is
381// no distinction between internal and leaf nodes.
382const Node = function (parent) {
383 this.plane = null
384 this.front = null
385 this.back = null
386 this.polygontreenodes = []
387 this.parent = parent
388}
389
390Node.prototype = {
391 // Convert solid space to empty space and empty space to solid space.
392 invert: function () {
393 let queue = [this]
394 let node
395 for (let i = 0; i < queue.length; i++) {
396 node = queue[i]
397 if (node.plane) node.plane = node.plane.flipped()
398 if (node.front) queue.push(node.front)
399 if (node.back) queue.push(node.back)
400 let temp = node.front
401 node.front = node.back
402 node.back = temp
403 }
404 },
405
406 // clip polygontreenodes to our plane
407 // calls remove() for all clipped PolygonTreeNodes
408 clipPolygons: function (polygontreenodes, alsoRemovecoplanarFront) {
409 let args = {'node': this, 'polygontreenodes': polygontreenodes}
410 let node
411 let stack = []
412
413 do {
414 node = args.node
415 polygontreenodes = args.polygontreenodes
416
417 // begin "function"
418 if (node.plane) {
419 let backnodes = []
420 let frontnodes = []
421 let coplanarfrontnodes = alsoRemovecoplanarFront ? backnodes : frontnodes
422 let plane = node.plane
423 let numpolygontreenodes = polygontreenodes.length
424 for (let i = 0; i < numpolygontreenodes; i++) {
425 let node1 = polygontreenodes[i]
426 if (!node1.isRemoved()) {
427 node1.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes)
428 }
429 }
430
431 if (node.front && (frontnodes.length > 0)) {
432 stack.push({'node': node.front, 'polygontreenodes': frontnodes})
433 }
434 let numbacknodes = backnodes.length
435 if (node.back && (numbacknodes > 0)) {
436 stack.push({'node': node.back, 'polygontreenodes': backnodes})
437 } else {
438 // there's nothing behind this plane. Delete the nodes behind this plane:
439 for (let i = 0; i < numbacknodes; i++) {
440 backnodes[i].remove()
441 }
442 }
443 }
444 args = stack.pop()
445 } while (typeof (args) !== 'undefined')
446 },
447
448 // Remove all polygons in this BSP tree that are inside the other BSP tree
449 // `tree`.
450 clipTo: function (tree, alsoRemovecoplanarFront) {
451 let node = this
452 let stack = []
453 do {
454 if (node.polygontreenodes.length > 0) {
455 tree.rootnode.clipPolygons(node.polygontreenodes, alsoRemovecoplanarFront)
456 }
457 if (node.front) stack.push(node.front)
458 if (node.back) stack.push(node.back)
459 node = stack.pop()
460 } while (typeof (node) !== 'undefined')
461 },
462
463 addPolygonTreeNodes: function (polygontreenodes) {
464 let args = {'node': this, 'polygontreenodes': polygontreenodes}
465 let node
466 let stack = []
467 do {
468 node = args.node
469 polygontreenodes = args.polygontreenodes
470
471 if (polygontreenodes.length === 0) {
472 args = stack.pop()
473 continue
474 }
475 let _this = node
476 if (!node.plane) {
477 let bestplane = polygontreenodes[0].getPolygon().plane
478 node.plane = bestplane
479 }
480 let frontnodes = []
481 let backnodes = []
482
483 for (let i = 0, n = polygontreenodes.length; i < n; ++i) {
484 polygontreenodes[i].splitByPlane(_this.plane, _this.polygontreenodes, backnodes, frontnodes, backnodes)
485 }
486
487 if (frontnodes.length > 0) {
488 if (!node.front) node.front = new Node(node)
489 stack.push({'node': node.front, 'polygontreenodes': frontnodes})
490 }
491 if (backnodes.length > 0) {
492 if (!node.back) node.back = new Node(node)
493 stack.push({'node': node.back, 'polygontreenodes': backnodes})
494 }
495
496 args = stack.pop()
497 } while (typeof (args) !== 'undefined')
498 },
499
500 getParentPlaneNormals: function (normals, maxdepth) {
501 if (maxdepth > 0) {
502 if (this.parent) {
503 normals.push(this.parent.plane.normal)
504 this.parent.getParentPlaneNormals(normals, maxdepth - 1)
505 }
506 }
507 }
508}
509
510module.exports = Tree