Spaces:
Sleeping
Sleeping
| // ## License | |
| // | |
| // Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. | |
| // THREE.js rework by thrax | |
| // # class CSG | |
| // Holds a binary space partition tree representing a 3D solid. Two solids can | |
| // be combined using the `union()`, `subtract()`, and `intersect()` methods. | |
| class CSG { | |
| constructor() { | |
| this.polygons = []; | |
| } | |
| clone() { | |
| let csg = new CSG(); | |
| csg.polygons = this.polygons.map(p=>p.clone()) | |
| return csg; | |
| } | |
| toPolygons() { | |
| return this.polygons; | |
| } | |
| union(csg) { | |
| let a = new Node(this.clone().polygons); | |
| let b = new Node(csg.clone().polygons); | |
| a.clipTo(b); | |
| b.clipTo(a); | |
| b.invert(); | |
| b.clipTo(a); | |
| b.invert(); | |
| a.build(b.allPolygons()); | |
| return CSG.fromPolygons(a.allPolygons()); | |
| } | |
| subtract(csg) { | |
| let a = new Node(this.clone().polygons); | |
| let b = new Node(csg.clone().polygons); | |
| a.invert(); | |
| a.clipTo(b); | |
| b.clipTo(a); | |
| b.invert(); | |
| b.clipTo(a); | |
| b.invert(); | |
| a.build(b.allPolygons()); | |
| a.invert(); | |
| return CSG.fromPolygons(a.allPolygons()); | |
| } | |
| intersect(csg) { | |
| let a = new Node(this.clone().polygons); | |
| let b = new Node(csg.clone().polygons); | |
| a.invert(); | |
| b.clipTo(a); | |
| b.invert(); | |
| a.clipTo(b); | |
| b.clipTo(a); | |
| a.build(b.allPolygons()); | |
| a.invert(); | |
| return CSG.fromPolygons(a.allPolygons()); | |
| } | |
| // Return a new CSG solid with solid and empty space switched. This solid is | |
| // not modified. | |
| inverse() { | |
| let csg = this.clone(); | |
| csg.polygons.forEach(p=>p.flip()); | |
| return csg; | |
| } | |
| } | |
| // Construct a CSG solid from a list of `Polygon` instances. | |
| CSG.fromPolygons=function(polygons) { | |
| let csg = new CSG(); | |
| csg.polygons = polygons; | |
| return csg; | |
| } | |
| // # class Vector | |
| // Represents a 3D vector. | |
| // | |
| // Example usage: | |
| // | |
| // new CSG.Vector(1, 2, 3); | |
| class Vector { | |
| constructor(x=0, y=0, z=0) { | |
| this.x=x; | |
| this.y=y; | |
| this.z=z; | |
| } | |
| copy(v){ | |
| this.x=v.x; | |
| this.y=v.y; | |
| this.z=v.z; | |
| return this | |
| } | |
| clone() { | |
| return new Vector(this.x,this.y,this.z) | |
| } | |
| negate() { | |
| this.x*=-1; | |
| this.y*=-1; | |
| this.z*=-1; | |
| return this | |
| } | |
| add(a) { | |
| this.x+=a.x | |
| this.y+=a.y | |
| this.z+=a.z | |
| return this; | |
| } | |
| sub(a) { | |
| this.x-=a.x | |
| this.y-=a.y | |
| this.z-=a.z | |
| return this | |
| } | |
| times(a) { | |
| this.x*=a | |
| this.y*=a | |
| this.z*=a | |
| return this | |
| } | |
| dividedBy(a) { | |
| this.x/=a | |
| this.y/=a | |
| this.z/=a | |
| return this | |
| } | |
| lerp(a, t) { | |
| return this.add(tv0.copy(a).sub(this).times(t)) | |
| } | |
| unit() { | |
| return this.dividedBy(this.length()) | |
| } | |
| length(){ | |
| return Math.sqrt((this.x**2)+(this.y**2)+(this.z**2)) | |
| } | |
| normalize(){ | |
| return this.unit() | |
| } | |
| cross(b) { | |
| let a = this; | |
| const ax = a.x, ay = a.y, az = a.z; | |
| const bx = b.x, by = b.y, bz = b.z; | |
| this.x = ay * bz - az * by; | |
| this.y = az * bx - ax * bz; | |
| this.z = ax * by - ay * bx; | |
| return this; | |
| } | |
| dot(b){ | |
| return (this.x*b.x)+(this.y*b.y)+(this.z*b.z) | |
| } | |
| } | |
| //Temporaries used to avoid internal allocation.. | |
| let tv0=new Vector() | |
| let tv1=new Vector() | |
| // # class Vertex | |
| // Represents a vertex of a polygon. Use your own vertex class instead of this | |
| // one to provide additional features like texture coordinates and vertex | |
| // colors. Custom vertex classes need to provide a `pos` property and `clone()`, | |
| // `flip()`, and `interpolate()` methods that behave analogous to the ones | |
| // defined by `CSG.Vertex`. This class provides `normal` so convenience | |
| // functions like `CSG.sphere()` can return a smooth vertex normal, but `normal` | |
| // is not used anywhere else. | |
| class Vertex { | |
| constructor(pos, normal, uv, color) { | |
| this.pos = new Vector().copy(pos); | |
| this.normal = new Vector().copy(normal); | |
| uv && (this.uv = new Vector().copy(uv)) && (this.uv.z=0); | |
| color && (this.color = new Vector().copy(color)); | |
| } | |
| clone() { | |
| return new Vertex(this.pos,this.normal,this.uv,this.color); | |
| } | |
| // Invert all orientation-specific data (e.g. vertex normal). Called when the | |
| // orientation of a polygon is flipped. | |
| flip() { | |
| this.normal.negate(); | |
| } | |
| // Create a new vertex between this vertex and `other` by linearly | |
| // interpolating all properties using a parameter of `t`. Subclasses should | |
| // override this to interpolate additional properties. | |
| interpolate(other, t) { | |
| return new Vertex(this.pos.clone().lerp(other.pos, t),this.normal.clone().lerp(other.normal, t),this.uv&&other.uv&&this.uv.clone().lerp(other.uv, t), this.color&&other.color&&this.color.clone().lerp(other.color,t)) | |
| } | |
| } | |
| ; | |
| // # class Plane | |
| // Represents a plane in 3D space. | |
| class Plane { | |
| constructor(normal, w) { | |
| this.normal = normal; | |
| this.w = w; | |
| } | |
| clone() { | |
| return new Plane(this.normal.clone(),this.w); | |
| } | |
| flip() { | |
| this.normal.negate(); | |
| this.w = -this.w; | |
| } | |
| // Split `polygon` by this plane if needed, then put the polygon or polygon | |
| // fragments in the appropriate lists. Coplanar polygons go into either | |
| // `coplanarFront` or `coplanarBack` depending on their orientation with | |
| // respect to this plane. Polygons in front or in back of this plane go into | |
| // either `front` or `back`. | |
| splitPolygon(polygon, coplanarFront, coplanarBack, front, back) { | |
| const COPLANAR = 0; | |
| const FRONT = 1; | |
| const BACK = 2; | |
| const SPANNING = 3; | |
| // Classify each point as well as the entire polygon into one of the above | |
| // four classes. | |
| let polygonType = 0; | |
| let types = []; | |
| for (let i = 0; i < polygon.vertices.length; i++) { | |
| let t = this.normal.dot(polygon.vertices[i].pos) - this.w; | |
| let type = (t < -Plane.EPSILON) ? BACK : (t > Plane.EPSILON) ? FRONT : COPLANAR; | |
| polygonType |= type; | |
| types.push(type); | |
| } | |
| // Put the polygon in the correct list, splitting it when necessary. | |
| switch (polygonType) { | |
| case COPLANAR: | |
| (this.normal.dot(polygon.plane.normal) > 0 ? coplanarFront : coplanarBack).push(polygon); | |
| break; | |
| case FRONT: | |
| front.push(polygon); | |
| break; | |
| case BACK: | |
| back.push(polygon); | |
| break; | |
| case SPANNING: | |
| let f = [] | |
| , b = []; | |
| for (let i = 0; i < polygon.vertices.length; i++) { | |
| let j = (i + 1) % polygon.vertices.length; | |
| let ti = types[i] | |
| , tj = types[j]; | |
| let vi = polygon.vertices[i] | |
| , vj = polygon.vertices[j]; | |
| if (ti != BACK) | |
| f.push(vi); | |
| if (ti != FRONT) | |
| b.push(ti != BACK ? vi.clone() : vi); | |
| if ((ti | tj) == SPANNING) { | |
| let t = (this.w - this.normal.dot(vi.pos)) / this.normal.dot(tv0.copy(vj.pos).sub(vi.pos)); | |
| let v = vi.interpolate(vj, t); | |
| f.push(v); | |
| b.push(v.clone()); | |
| } | |
| } | |
| if (f.length >= 3) | |
| front.push(new Polygon(f,polygon.shared)); | |
| if (b.length >= 3) | |
| back.push(new Polygon(b,polygon.shared)); | |
| break; | |
| } | |
| } | |
| } | |
| // `Plane.EPSILON` is the tolerance used by `splitPolygon()` to decide if a | |
| // point is on the plane. | |
| Plane.EPSILON = 1e-5; | |
| Plane.fromPoints = function(a, b, c) { | |
| let n = tv0.copy(b).sub(a).cross(tv1.copy(c).sub(a)).normalize() | |
| return new Plane(n.clone(),n.dot(a)); | |
| } | |
| // # class Polygon | |
| // Represents a convex polygon. The vertices used to initialize a polygon must | |
| // be coplanar and form a convex loop. They do not have to be `Vertex` | |
| // instances but they must behave similarly (duck typing can be used for | |
| // customization). | |
| // | |
| // Each convex polygon has a `shared` property, which is shared between all | |
| // polygons that are clones of each other or were split from the same polygon. | |
| // This can be used to define per-polygon properties (such as surface color). | |
| class Polygon { | |
| constructor(vertices, shared) { | |
| this.vertices = vertices; | |
| this.shared = shared; | |
| this.plane = Plane.fromPoints(vertices[0].pos, vertices[1].pos, vertices[2].pos); | |
| } | |
| clone() { | |
| return new Polygon(this.vertices.map(v=>v.clone()),this.shared); | |
| } | |
| flip() { | |
| this.vertices.reverse().forEach(v=>v.flip()) | |
| this.plane.flip(); | |
| } | |
| } | |
| // # 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. That polygon (and all other coplanar | |
| // polygons) are added directly to that node and the other polygons are added to | |
| // the front and/or back subtrees. This is not a leafy BSP tree since there is | |
| // no distinction between internal and leaf nodes. | |
| class Node { | |
| constructor(polygons) { | |
| this.plane = null; | |
| this.front = null; | |
| this.back = null; | |
| this.polygons = []; | |
| if (polygons) | |
| this.build(polygons); | |
| } | |
| clone() { | |
| let node = new Node(); | |
| node.plane = this.plane && this.plane.clone(); | |
| node.front = this.front && this.front.clone(); | |
| node.back = this.back && this.back.clone(); | |
| node.polygons = this.polygons.map(p=>p.clone()); | |
| return node; | |
| } | |
| // Convert solid space to empty space and empty space to solid space. | |
| invert() { | |
| for (let i = 0; i < this.polygons.length; i++) | |
| this.polygons[i].flip(); | |
| this.plane && this.plane.flip(); | |
| this.front && this.front.invert(); | |
| this.back && this.back.invert(); | |
| let temp = this.front; | |
| this.front = this.back; | |
| this.back = temp; | |
| } | |
| // Recursively remove all polygons in `polygons` that are inside this BSP | |
| // tree. | |
| clipPolygons(polygons) { | |
| if (!this.plane) | |
| return polygons.slice(); | |
| let front = [] | |
| , back = []; | |
| for (let i = 0; i < polygons.length; i++) { | |
| this.plane.splitPolygon(polygons[i], front, back, front, back); | |
| } | |
| if (this.front) | |
| front = this.front.clipPolygons(front); | |
| if (this.back) | |
| back = this.back.clipPolygons(back); | |
| else | |
| back = []; | |
| //return front; | |
| return front.concat(back); | |
| } | |
| // Remove all polygons in this BSP tree that are inside the other BSP tree | |
| // `bsp`. | |
| clipTo(bsp) { | |
| this.polygons = bsp.clipPolygons(this.polygons); | |
| if (this.front) | |
| this.front.clipTo(bsp); | |
| if (this.back) | |
| this.back.clipTo(bsp); | |
| } | |
| // Return a list of all polygons in this BSP tree. | |
| allPolygons() { | |
| let polygons = this.polygons.slice(); | |
| if (this.front) | |
| polygons = polygons.concat(this.front.allPolygons()); | |
| if (this.back) | |
| polygons = polygons.concat(this.back.allPolygons()); | |
| return polygons; | |
| } | |
| // Build a BSP tree out of `polygons`. When called on an existing tree, the | |
| // new polygons are filtered down to the bottom of the tree and become new | |
| // nodes there. Each set of polygons is partitioned using the first polygon | |
| // (no heuristic is used to pick a good split). | |
| build(polygons) { | |
| if (!polygons.length) | |
| return; | |
| if (!this.plane) | |
| this.plane = polygons[0].plane.clone(); | |
| let front = [] | |
| , back = []; | |
| for (let i = 0; i < polygons.length; i++) { | |
| this.plane.splitPolygon(polygons[i], this.polygons, this.polygons, front, back); | |
| } | |
| if (front.length) { | |
| if (!this.front) | |
| this.front = new Node(); | |
| this.front.build(front); | |
| } | |
| if (back.length) { | |
| if (!this.back) | |
| this.back = new Node(); | |
| this.back.build(back); | |
| } | |
| } | |
| } | |
| // Inflate/deserialize a vanilla struct into a CSG structure webworker. | |
| CSG.fromJSON=function(json){ | |
| return CSG.fromPolygons(json.polygons.map(p=>new Polygon(p.vertices.map(v=> new Vertex(v.pos,v.normal,v.uv)),p.shared))) | |
| } | |
| export {CSG,Vertex,Vector,Polygon,Plane} | |
| // Return a new CSG solid representing space in either this solid or in the | |
| // solid `csg`. Neither this solid nor the solid `csg` are modified. | |
| // | |
| // A.union(B) | |
| // | |
| // +-------+ +-------+ | |
| // | | | | | |
| // | A | | | | |
| // | +--+----+ = | +----+ | |
| // +----+--+ | +----+ | | |
| // | B | | | | |
| // | | | | | |
| // +-------+ +-------+ | |
| // | |
| // Return a new CSG solid representing space in this solid but not in the | |
| // solid `csg`. Neither this solid nor the solid `csg` are modified. | |
| // | |
| // A.subtract(B) | |
| // | |
| // +-------+ +-------+ | |
| // | | | | | |
| // | A | | | | |
| // | +--+----+ = | +--+ | |
| // +----+--+ | +----+ | |
| // | B | | |
| // | | | |
| // +-------+ | |
| // | |
| // Return a new CSG solid representing space both this solid and in the | |
| // solid `csg`. Neither this solid nor the solid `csg` are modified. | |
| // | |
| // A.intersect(B) | |
| // | |
| // +-------+ | |
| // | | | |
| // | A | | |
| // | +--+----+ = +--+ | |
| // +----+--+ | +--+ | |
| // | B | | |
| // | | | |
| // +-------+ | |
| // | |