511 lines
16 KiB
JavaScript
511 lines
16 KiB
JavaScript
const {_CSGDEBUG, EPS} = require('./constants')
|
|
const Vertex = require('./math/Vertex3')
|
|
const Polygon = require('./math/Polygon3')
|
|
|
|
// Returns object:
|
|
// .type:
|
|
// 0: coplanar-front
|
|
// 1: coplanar-back
|
|
// 2: front
|
|
// 3: back
|
|
// 4: spanning
|
|
// In case the polygon is spanning, returns:
|
|
// .front: a Polygon of the front part
|
|
// .back: a Polygon of the back part
|
|
function splitPolygonByPlane (plane, polygon) {
|
|
let result = {
|
|
type: null,
|
|
front: null,
|
|
back: null
|
|
}
|
|
// cache in local lets (speedup):
|
|
let planenormal = plane.normal
|
|
let vertices = polygon.vertices
|
|
let numvertices = vertices.length
|
|
if (polygon.plane.equals(plane)) {
|
|
result.type = 0
|
|
} else {
|
|
let thisw = plane.w
|
|
let hasfront = false
|
|
let hasback = false
|
|
let vertexIsBack = []
|
|
let MINEPS = -EPS
|
|
for (let i = 0; i < numvertices; i++) {
|
|
let t = planenormal.dot(vertices[i].pos) - thisw
|
|
let isback = (t < 0)
|
|
vertexIsBack.push(isback)
|
|
if (t > EPS) hasfront = true
|
|
if (t < MINEPS) hasback = true
|
|
}
|
|
if ((!hasfront) && (!hasback)) {
|
|
// all points coplanar
|
|
let t = planenormal.dot(polygon.plane.normal)
|
|
result.type = (t >= 0) ? 0 : 1
|
|
} else if (!hasback) {
|
|
result.type = 2
|
|
} else if (!hasfront) {
|
|
result.type = 3
|
|
} else {
|
|
// spanning
|
|
result.type = 4
|
|
let frontvertices = []
|
|
let backvertices = []
|
|
let isback = vertexIsBack[0]
|
|
for (let vertexindex = 0; vertexindex < numvertices; vertexindex++) {
|
|
let vertex = vertices[vertexindex]
|
|
let nextvertexindex = vertexindex + 1
|
|
if (nextvertexindex >= numvertices) nextvertexindex = 0
|
|
let nextisback = vertexIsBack[nextvertexindex]
|
|
if (isback === nextisback) {
|
|
// line segment is on one side of the plane:
|
|
if (isback) {
|
|
backvertices.push(vertex)
|
|
} else {
|
|
frontvertices.push(vertex)
|
|
}
|
|
} else {
|
|
// line segment intersects plane:
|
|
let point = vertex.pos
|
|
let nextpoint = vertices[nextvertexindex].pos
|
|
let intersectionpoint = plane.splitLineBetweenPoints(point, nextpoint)
|
|
let intersectionvertex = new Vertex(intersectionpoint)
|
|
if (isback) {
|
|
backvertices.push(vertex)
|
|
backvertices.push(intersectionvertex)
|
|
frontvertices.push(intersectionvertex)
|
|
} else {
|
|
frontvertices.push(vertex)
|
|
frontvertices.push(intersectionvertex)
|
|
backvertices.push(intersectionvertex)
|
|
}
|
|
}
|
|
isback = nextisback
|
|
} // for vertexindex
|
|
// remove duplicate vertices:
|
|
let EPS_SQUARED = EPS * EPS
|
|
if (backvertices.length >= 3) {
|
|
let prevvertex = backvertices[backvertices.length - 1]
|
|
for (let vertexindex = 0; vertexindex < backvertices.length; vertexindex++) {
|
|
let vertex = backvertices[vertexindex]
|
|
if (vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) {
|
|
backvertices.splice(vertexindex, 1)
|
|
vertexindex--
|
|
}
|
|
prevvertex = vertex
|
|
}
|
|
}
|
|
if (frontvertices.length >= 3) {
|
|
let prevvertex = frontvertices[frontvertices.length - 1]
|
|
for (let vertexindex = 0; vertexindex < frontvertices.length; vertexindex++) {
|
|
let vertex = frontvertices[vertexindex]
|
|
if (vertex.pos.distanceToSquared(prevvertex.pos) < EPS_SQUARED) {
|
|
frontvertices.splice(vertexindex, 1)
|
|
vertexindex--
|
|
}
|
|
prevvertex = vertex
|
|
}
|
|
}
|
|
if (frontvertices.length >= 3) {
|
|
result.front = new Polygon(frontvertices, polygon.shared, polygon.plane)
|
|
}
|
|
if (backvertices.length >= 3) {
|
|
result.back = new Polygon(backvertices, polygon.shared, polygon.plane)
|
|
}
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// # class PolygonTreeNode
|
|
// This class manages hierarchical splits of polygons
|
|
// At the top is a root node which doesn hold a polygon, only child PolygonTreeNodes
|
|
// Below that are zero or more 'top' nodes; each holds a polygon. The polygons can be in different planes
|
|
// splitByPlane() splits a node by a plane. If the plane intersects the polygon, two new child nodes
|
|
// are created holding the splitted polygon.
|
|
// getPolygons() retrieves the polygon from the tree. If for PolygonTreeNode the polygon is split but
|
|
// the two split parts (child nodes) are still intact, then the unsplit polygon is returned.
|
|
// This ensures that we can safely split a polygon into many fragments. If the fragments are untouched,
|
|
// getPolygons() will return the original unsplit polygon instead of the fragments.
|
|
// remove() removes a polygon from the tree. Once a polygon is removed, the parent polygons are invalidated
|
|
// since they are no longer intact.
|
|
// constructor creates the root node:
|
|
const PolygonTreeNode = function () {
|
|
this.parent = null
|
|
this.children = []
|
|
this.polygon = null
|
|
this.removed = false
|
|
}
|
|
|
|
PolygonTreeNode.prototype = {
|
|
// fill the tree with polygons. Should be called on the root node only; child nodes must
|
|
// always be a derivate (split) of the parent node.
|
|
addPolygons: function (polygons) {
|
|
if (!this.isRootNode())
|
|
// new polygons can only be added to root node; children can only be splitted polygons
|
|
{
|
|
throw new Error('Assertion failed')
|
|
}
|
|
let _this = this
|
|
polygons.map(function (polygon) {
|
|
_this.addChild(polygon)
|
|
})
|
|
},
|
|
|
|
// remove a node
|
|
// - the siblings become toplevel nodes
|
|
// - the parent is removed recursively
|
|
remove: function () {
|
|
if (!this.removed) {
|
|
this.removed = true
|
|
|
|
if (_CSGDEBUG) {
|
|
if (this.isRootNode()) throw new Error('Assertion failed') // can't remove root node
|
|
if (this.children.length) throw new Error('Assertion failed') // we shouldn't remove nodes with children
|
|
}
|
|
|
|
// remove ourselves from the parent's children list:
|
|
let parentschildren = this.parent.children
|
|
let i = parentschildren.indexOf(this)
|
|
if (i < 0) throw new Error('Assertion failed')
|
|
parentschildren.splice(i, 1)
|
|
|
|
// invalidate the parent's polygon, and of all parents above it:
|
|
this.parent.recursivelyInvalidatePolygon()
|
|
}
|
|
},
|
|
|
|
isRemoved: function () {
|
|
return this.removed
|
|
},
|
|
|
|
isRootNode: function () {
|
|
return !this.parent
|
|
},
|
|
|
|
// invert all polygons in the tree. Call on the root node
|
|
invert: function () {
|
|
if (!this.isRootNode()) throw new Error('Assertion failed') // can only call this on the root node
|
|
this.invertSub()
|
|
},
|
|
|
|
getPolygon: function () {
|
|
if (!this.polygon) throw new Error('Assertion failed') // doesn't have a polygon, which means that it has been broken down
|
|
return this.polygon
|
|
},
|
|
|
|
getPolygons: function (result) {
|
|
let children = [this]
|
|
let queue = [children]
|
|
let i, j, l, node
|
|
for (i = 0; i < queue.length; ++i) { // queue size can change in loop, don't cache length
|
|
children = queue[i]
|
|
for (j = 0, l = children.length; j < l; j++) { // ok to cache length
|
|
node = children[j]
|
|
if (node.polygon) {
|
|
// the polygon hasn't been broken yet. We can ignore the children and return our polygon:
|
|
result.push(node.polygon)
|
|
} else {
|
|
// our polygon has been split up and broken, so gather all subpolygons from the children
|
|
queue.push(node.children)
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// split the node by a plane; add the resulting nodes to the frontnodes and backnodes array
|
|
// If the plane doesn't intersect the polygon, the 'this' object is added to one of the arrays
|
|
// If the plane does intersect the polygon, two new child nodes are created for the front and back fragments,
|
|
// and added to both arrays.
|
|
splitByPlane: function (plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) {
|
|
if (this.children.length) {
|
|
let queue = [this.children]
|
|
let i
|
|
let j
|
|
let l
|
|
let node
|
|
let nodes
|
|
for (i = 0; i < queue.length; i++) { // queue.length can increase, do not cache
|
|
nodes = queue[i]
|
|
for (j = 0, l = nodes.length; j < l; j++) { // ok to cache length
|
|
node = nodes[j]
|
|
if (node.children.length) {
|
|
queue.push(node.children)
|
|
} else {
|
|
// no children. Split the polygon:
|
|
node._splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes)
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
this._splitByPlane(plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes)
|
|
}
|
|
},
|
|
|
|
// only to be called for nodes with no children
|
|
_splitByPlane: function (plane, coplanarfrontnodes, coplanarbacknodes, frontnodes, backnodes) {
|
|
let polygon = this.polygon
|
|
if (polygon) {
|
|
let bound = polygon.boundingSphere()
|
|
let sphereradius = bound[1] + EPS // FIXME Why add imprecision?
|
|
let planenormal = plane.normal
|
|
let spherecenter = bound[0]
|
|
let d = planenormal.dot(spherecenter) - plane.w
|
|
if (d > sphereradius) {
|
|
frontnodes.push(this)
|
|
} else if (d < -sphereradius) {
|
|
backnodes.push(this)
|
|
} else {
|
|
let splitresult = splitPolygonByPlane(plane, polygon)
|
|
switch (splitresult.type) {
|
|
case 0:
|
|
// coplanar front:
|
|
coplanarfrontnodes.push(this)
|
|
break
|
|
|
|
case 1:
|
|
// coplanar back:
|
|
coplanarbacknodes.push(this)
|
|
break
|
|
|
|
case 2:
|
|
// front:
|
|
frontnodes.push(this)
|
|
break
|
|
|
|
case 3:
|
|
// back:
|
|
backnodes.push(this)
|
|
break
|
|
|
|
case 4:
|
|
// spanning:
|
|
if (splitresult.front) {
|
|
let frontnode = this.addChild(splitresult.front)
|
|
frontnodes.push(frontnode)
|
|
}
|
|
if (splitresult.back) {
|
|
let backnode = this.addChild(splitresult.back)
|
|
backnodes.push(backnode)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// PRIVATE methods from here:
|
|
// add child to a node
|
|
// this should be called whenever the polygon is split
|
|
// a child should be created for every fragment of the split polygon
|
|
// returns the newly created child
|
|
addChild: function (polygon) {
|
|
let newchild = new PolygonTreeNode()
|
|
newchild.parent = this
|
|
newchild.polygon = polygon
|
|
this.children.push(newchild)
|
|
return newchild
|
|
},
|
|
|
|
invertSub: function () {
|
|
let children = [this]
|
|
let queue = [children]
|
|
let i, j, l, node
|
|
for (i = 0; i < queue.length; i++) {
|
|
children = queue[i]
|
|
for (j = 0, l = children.length; j < l; j++) {
|
|
node = children[j]
|
|
if (node.polygon) {
|
|
node.polygon = node.polygon.flipped()
|
|
}
|
|
queue.push(node.children)
|
|
}
|
|
}
|
|
},
|
|
|
|
recursivelyInvalidatePolygon: function () {
|
|
let node = this
|
|
while (node.polygon) {
|
|
node.polygon = null
|
|
if (node.parent) {
|
|
node = node.parent
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// # class Tree
|
|
// This is the root of a BSP tree
|
|
// We are using this separate class for the root of the tree, to hold the PolygonTreeNode root
|
|
// The actual tree is kept in this.rootnode
|
|
const Tree = function (polygons) {
|
|
this.polygonTree = new PolygonTreeNode()
|
|
this.rootnode = new Node(null)
|
|
if (polygons) this.addPolygons(polygons)
|
|
}
|
|
|
|
Tree.prototype = {
|
|
invert: function () {
|
|
this.polygonTree.invert()
|
|
this.rootnode.invert()
|
|
},
|
|
|
|
// Remove all polygons in this BSP tree that are inside the other BSP tree
|
|
// `tree`.
|
|
clipTo: function (tree, alsoRemovecoplanarFront) {
|
|
alsoRemovecoplanarFront = alsoRemovecoplanarFront ? true : false
|
|
this.rootnode.clipTo(tree, alsoRemovecoplanarFront)
|
|
},
|
|
|
|
allPolygons: function () {
|
|
let result = []
|
|
this.polygonTree.getPolygons(result)
|
|
return result
|
|
},
|
|
|
|
addPolygons: function (polygons) {
|
|
let _this = this
|
|
let polygontreenodes = polygons.map(function (p) {
|
|
return _this.polygonTree.addChild(p)
|
|
})
|
|
this.rootnode.addPolygonTreeNodes(polygontreenodes)
|
|
}
|
|
}
|
|
|
|
// # class Node
|
|
// Holds a node in a BSP tree. A BSP tree is built from a collection of polygons
|
|
// by picking a polygon to split along.
|
|
// Polygons are not stored directly in the tree, but in PolygonTreeNodes, stored in
|
|
// this.polygontreenodes. Those PolygonTreeNodes are children of the owning
|
|
// Tree.polygonTree
|
|
// This is not a leafy BSP tree since there is
|
|
// no distinction between internal and leaf nodes.
|
|
const Node = function (parent) {
|
|
this.plane = null
|
|
this.front = null
|
|
this.back = null
|
|
this.polygontreenodes = []
|
|
this.parent = parent
|
|
}
|
|
|
|
Node.prototype = {
|
|
// Convert solid space to empty space and empty space to solid space.
|
|
invert: function () {
|
|
let queue = [this]
|
|
let node
|
|
for (let i = 0; i < queue.length; i++) {
|
|
node = queue[i]
|
|
if (node.plane) node.plane = node.plane.flipped()
|
|
if (node.front) queue.push(node.front)
|
|
if (node.back) queue.push(node.back)
|
|
let temp = node.front
|
|
node.front = node.back
|
|
node.back = temp
|
|
}
|
|
},
|
|
|
|
// clip polygontreenodes to our plane
|
|
// calls remove() for all clipped PolygonTreeNodes
|
|
clipPolygons: function (polygontreenodes, alsoRemovecoplanarFront) {
|
|
let args = {'node': this, 'polygontreenodes': polygontreenodes}
|
|
let node
|
|
let stack = []
|
|
|
|
do {
|
|
node = args.node
|
|
polygontreenodes = args.polygontreenodes
|
|
|
|
// begin "function"
|
|
if (node.plane) {
|
|
let backnodes = []
|
|
let frontnodes = []
|
|
let coplanarfrontnodes = alsoRemovecoplanarFront ? backnodes : frontnodes
|
|
let plane = node.plane
|
|
let numpolygontreenodes = polygontreenodes.length
|
|
for (let i = 0; i < numpolygontreenodes; i++) {
|
|
let node1 = polygontreenodes[i]
|
|
if (!node1.isRemoved()) {
|
|
node1.splitByPlane(plane, coplanarfrontnodes, backnodes, frontnodes, backnodes)
|
|
}
|
|
}
|
|
|
|
if (node.front && (frontnodes.length > 0)) {
|
|
stack.push({'node': node.front, 'polygontreenodes': frontnodes})
|
|
}
|
|
let numbacknodes = backnodes.length
|
|
if (node.back && (numbacknodes > 0)) {
|
|
stack.push({'node': node.back, 'polygontreenodes': backnodes})
|
|
} else {
|
|
// there's nothing behind this plane. Delete the nodes behind this plane:
|
|
for (let i = 0; i < numbacknodes; i++) {
|
|
backnodes[i].remove()
|
|
}
|
|
}
|
|
}
|
|
args = stack.pop()
|
|
} while (typeof (args) !== 'undefined')
|
|
},
|
|
|
|
// Remove all polygons in this BSP tree that are inside the other BSP tree
|
|
// `tree`.
|
|
clipTo: function (tree, alsoRemovecoplanarFront) {
|
|
let node = this
|
|
let stack = []
|
|
do {
|
|
if (node.polygontreenodes.length > 0) {
|
|
tree.rootnode.clipPolygons(node.polygontreenodes, alsoRemovecoplanarFront)
|
|
}
|
|
if (node.front) stack.push(node.front)
|
|
if (node.back) stack.push(node.back)
|
|
node = stack.pop()
|
|
} while (typeof (node) !== 'undefined')
|
|
},
|
|
|
|
addPolygonTreeNodes: function (polygontreenodes) {
|
|
let args = {'node': this, 'polygontreenodes': polygontreenodes}
|
|
let node
|
|
let stack = []
|
|
do {
|
|
node = args.node
|
|
polygontreenodes = args.polygontreenodes
|
|
|
|
if (polygontreenodes.length === 0) {
|
|
args = stack.pop()
|
|
continue
|
|
}
|
|
let _this = node
|
|
if (!node.plane) {
|
|
let bestplane = polygontreenodes[0].getPolygon().plane
|
|
node.plane = bestplane
|
|
}
|
|
let frontnodes = []
|
|
let backnodes = []
|
|
|
|
for (let i = 0, n = polygontreenodes.length; i < n; ++i) {
|
|
polygontreenodes[i].splitByPlane(_this.plane, _this.polygontreenodes, backnodes, frontnodes, backnodes)
|
|
}
|
|
|
|
if (frontnodes.length > 0) {
|
|
if (!node.front) node.front = new Node(node)
|
|
stack.push({'node': node.front, 'polygontreenodes': frontnodes})
|
|
}
|
|
if (backnodes.length > 0) {
|
|
if (!node.back) node.back = new Node(node)
|
|
stack.push({'node': node.back, 'polygontreenodes': backnodes})
|
|
}
|
|
|
|
args = stack.pop()
|
|
} while (typeof (args) !== 'undefined')
|
|
},
|
|
|
|
getParentPlaneNormals: function (normals, maxdepth) {
|
|
if (maxdepth > 0) {
|
|
if (this.parent) {
|
|
normals.push(this.parent.plane.normal)
|
|
this.parent.getParentPlaneNormals(normals, maxdepth - 1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = Tree
|