Last active
February 10, 2021 16:25
-
-
Save RichardLindhout/99b730b648ded04e9ef2b9308bc8da71 to your computer and use it in GitHub Desktop.
CSGMesh.ts fix bug in production
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ## 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. | |
// | |
// Differences Copyright 2020-2021 Sean Bradley : https://sbcode.net/threejs/ | |
// - Started with CSGMesh.js from https://github.com/manthrax/THREE-CSGMesh/blob/master/CSGMesh.js | |
// - Converted to TypeScript by adding type annotations to all variables | |
// - Converted var to const and let | |
// - More THREEJS integration (THREE r119) | |
// - Some Refactoring | |
// - support for three r125 | |
import * as THREE from 'three' | |
class CSG { | |
polygons: Polygon[] | |
constructor() { | |
this.polygons = [] | |
} | |
clone() { | |
const csg = new CSG() | |
csg.polygons = this.polygons.map(function (p) { | |
return p.clone() | |
}) | |
return csg | |
} | |
toPolygons() { | |
return this.polygons | |
} | |
union(csg: 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: 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: 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() { | |
const csg = this.clone() | |
csg.polygons.map(function (p) { | |
p.flip() | |
}) | |
return csg | |
} | |
static fromPolygons = function (polygons: Polygon[]) { | |
const csg = new CSG() | |
csg.polygons = polygons | |
return csg | |
} | |
static fromGeometry = function (geom: THREE.Geometry | THREE.BufferGeometry) { | |
if ((geom as THREE.BufferGeometry).isBufferGeometry) | |
geom = new THREE.Geometry().fromBufferGeometry( | |
geom as THREE.BufferGeometry | |
) | |
const fs = (geom as THREE.Geometry).faces | |
const vs = (geom as THREE.Geometry).vertices | |
const polys = [] | |
const fm = ['a', 'b', 'c'] | |
for (let i = 0; i < fs.length; i++) { | |
const f = fs[i] | |
const vertices = [] | |
for (let j = 0; j < 3; j++) { | |
vertices.push( | |
new Vertex( | |
// @ts-ignore | |
vs[f[fm[j]]], | |
f.vertexNormals[j], | |
(geom as any).faceVertexUvs[0][i][j] | |
) | |
) | |
} | |
polys.push(new Polygon(vertices)) | |
} | |
return CSG.fromPolygons(polys) | |
} | |
private static _tmpm3 = new THREE.Matrix3() | |
private static currentOp: string | |
private static sourceMesh: THREE.Mesh | |
private static currentPrim: CSG | |
private static nextPrim: CSG | |
private static doRemove: boolean | |
static fromMesh = function (mesh: THREE.Mesh) { | |
const csg = CSG.fromGeometry(mesh.geometry) | |
CSG._tmpm3.getNormalMatrix(mesh.matrix) | |
for (let i = 0; i < csg.polygons.length; i++) { | |
let p = csg.polygons[i] | |
for (let j = 0; j < p.vertices.length; j++) { | |
let v = p.vertices[j] | |
v.pos.applyMatrix4(mesh.matrix) | |
v.normal.applyMatrix3(CSG._tmpm3) | |
} | |
} | |
return csg | |
} | |
static toMesh = function (csg: CSG, toMatrix: THREE.Matrix4) { | |
const geom = new THREE.Geometry() | |
const ps = csg.polygons | |
const vs = geom.vertices | |
const fvuv = geom.faceVertexUvs[0] | |
for (let i = 0; i < ps.length; i++) { | |
const p = ps[i] | |
const pvs = p.vertices | |
const v0 = vs.length | |
const pvlen = pvs.length | |
for (let j = 0; j < pvlen; j++) | |
vs.push(new THREE.Vector3().copy(pvs[j].pos)) | |
for (let j = 3; j <= pvlen; j++) { | |
const fc = new THREE.Face3(0, 0, 0) | |
const fuv: THREE.Vector3[] = [] | |
// @ts-ignore | |
fvuv.push(fuv) | |
const fnml = fc.vertexNormals | |
fc.a = v0 | |
fc.b = v0 + j - 2 | |
fc.c = v0 + j - 1 | |
fnml.push(new THREE.Vector3().copy(pvs[0].normal)) | |
fnml.push(new THREE.Vector3().copy(pvs[j - 2].normal)) | |
fnml.push(new THREE.Vector3().copy(pvs[j - 1].normal)) | |
fuv.push(new THREE.Vector3().copy(pvs[0].uv)) | |
fuv.push(new THREE.Vector3().copy(pvs[j - 2].uv)) | |
fuv.push(new THREE.Vector3().copy(pvs[j - 1].uv)) | |
fc.normal = new THREE.Vector3().copy(p.plane.normal) | |
geom.faces.push(fc) | |
} | |
} | |
//const inv = new THREE.Matrix4().getInverse(toMatrix); | |
const inv = new THREE.Matrix4().copy(toMatrix).invert() | |
geom.applyMatrix4(inv) | |
geom.verticesNeedUpdate = geom.elementsNeedUpdate = geom.normalsNeedUpdate = true | |
geom.computeBoundingSphere() | |
geom.computeBoundingBox() | |
// TODO: go back to buffer geometry? | |
const m = new THREE.Mesh(geom) | |
m.matrix.copy(toMatrix) | |
m.matrix.decompose(m.position, m.quaternion, m.scale) | |
m.updateMatrixWorld() | |
return m | |
} | |
static ieval = function (tokens: string | object | any[]) { | |
if (typeof tokens === 'string') CSG.currentOp = tokens | |
else if (tokens instanceof Array) { | |
for (let i = 0; i < tokens.length; i++) CSG.ieval(tokens[i]) | |
} else if (tokens instanceof THREE.Mesh) { | |
let op = CSG.currentOp | |
;(tokens as THREE.Mesh).updateMatrix() | |
;(tokens as THREE.Mesh).updateMatrixWorld() | |
if (!CSG.sourceMesh) | |
CSG.currentPrim = CSG.fromMesh((CSG.sourceMesh = tokens)) | |
else { | |
CSG.nextPrim = CSG.fromMesh(tokens) | |
// @ts-ignore | |
CSG.currentPrim = CSG.currentPrim[op](CSG.nextPrim) | |
} | |
if (CSG.doRemove) { | |
// @ts-ignore | |
tokens.parent.remove(tokens) | |
} | |
} //union,subtract,intersect,inverse | |
} | |
static eval = function (tokens: string | object | any[], doRemove: boolean) { | |
//[['add',mesh,mesh,mesh,mesh],['sub',mesh,mesh,mesh,mesh]] | |
//@ts-ignore | |
CSG.currentOp = null | |
//@ts-ignore | |
CSG.sourceMesh = null | |
CSG.doRemove = doRemove | |
CSG.ieval(tokens) | |
const result = CSG.toMesh(CSG.currentPrim, CSG.sourceMesh.matrix) | |
result.material = CSG.sourceMesh.material | |
result.castShadow = result.receiveShadow = true | |
return result | |
} | |
} | |
// Construct a CSG solid from a list of `Polygon` instances. | |
// # class Vector | |
// Represents a 3D vector. | |
// | |
// Example usage: | |
// | |
// new CSG.Vector(1, 2, 3); | |
// new CSG.Vector([1, 2, 3]); | |
// new CSG.Vector({ x: 1, y: 2, z: 3 }); | |
class Vector extends THREE.Vector3 { | |
constructor(x: number | THREE.Vector3, y?: number, z?: number) { | |
if (arguments.length == 3) { | |
super(x as number, y, z) | |
} else if (Array.isArray(x)) { | |
super(x[0], x[1], x[2]) | |
} else if (x instanceof THREE.Vector3) { | |
super() | |
this.set( | |
(x as THREE.Vector3).x, | |
(x as THREE.Vector3).y, | |
(x as THREE.Vector3).z | |
) | |
} else throw 'Invalid constructor to vector' | |
} | |
clone(): any { | |
return new Vector(this.x, this.y, this.z) | |
} | |
negate() { | |
return this.clone().multiplyScalar(-1) | |
} | |
plus(a: THREE.Vector3) { | |
return this.clone().add(a) | |
} | |
minus(a: THREE.Vector3) { | |
return this.clone().sub(a) | |
} | |
times(a: THREE.Vector3) { | |
return this.clone().multiplyScalar(a) | |
} | |
dividedBy(a: number) { | |
return this.clone().divideScalar(a) | |
} | |
lerp(a: Vector, t: number) { | |
return this.plus(a.minus(this).times(t)) | |
} | |
unit() { | |
return this.dividedBy(this.length()) | |
} | |
// @ts-ignore | |
cross(a) { | |
return THREE.Vector3.prototype.cross.call(this.clone(), a) | |
} | |
} | |
// # 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 { | |
pos: THREE.Vector3 | |
normal: THREE.Vector3 | |
//@ts-ignore | |
uv: THREE.Vector3 | |
constructor(pos: THREE.Vector3, normal: THREE.Vector3, uv?: THREE.Vector3) { | |
this.pos = new Vector(pos.x, pos.y, pos.z) | |
this.normal = new Vector(normal.x, normal.y, normal.z) | |
if (uv) this.uv = new Vector(uv.x, uv.y, uv.z) | |
} | |
clone() { | |
return new Vertex( | |
this.pos.clone(), | |
this.normal.clone(), | |
this.uv && this.uv.clone() | |
) | |
} | |
// Invert all orientation-specific data (e.g. vertex normal). Called when the | |
// orientation of a polygon is flipped. | |
flip() { | |
this.normal = 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: Vertex, t: number) { | |
return new Vertex( | |
this.pos.lerp(other.pos, t), | |
this.normal.lerp(other.normal, t), | |
this.uv && this.uv.lerp(other.uv, t) | |
) | |
} | |
} | |
// # class Plane | |
// Represents a plane in 3D space. | |
class Plane { | |
normal: Vector | |
w: number | |
constructor(normal: Vector, w: number) { | |
this.normal = normal | |
this.w = w | |
} | |
clone() { | |
return new Plane(this.normal.clone(), this.w) | |
} | |
flip() { | |
this.normal = 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: Polygon, | |
coplanarFront: Polygon[], | |
coplanarBack: Polygon[], | |
front: Polygon[], | |
back: Polygon[] | |
) { | |
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 | |
const types = [] | |
for (let i = 0; i < polygon.vertices.length; i++) { | |
const t = this.normal.dot(polygon.vertices[i].pos) - this.w | |
const 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: | |
const f = [] | |
const b = [] | |
for (let i = 0; i < polygon.vertices.length; i++) { | |
const j = (i + 1) % polygon.vertices.length | |
const ti = types[i] | |
const tj = types[j] | |
const vi = polygon.vertices[i] | |
const vj = polygon.vertices[j] | |
if (ti != BACK) f.push(vi) | |
if (ti != FRONT) b.push(ti != BACK ? vi.clone() : vi) | |
if ((ti | tj) == SPANNING) { | |
const t = | |
(this.w - this.normal.dot(vi.pos)) / | |
this.normal.dot((vj.pos as Vector).minus(vi.pos)) | |
const 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. | |
static EPSILON = 1e-5 | |
static fromPoints = function (a: Vector, b: Vector, c: Vector) { | |
const n = b.minus(a).cross(c.minus(a)).unit() | |
return new Plane(n, 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 { | |
vertices: Vertex[] | |
shared: object | undefined | |
plane: Plane | |
constructor(vertices: Vertex[], shared?: object) { | |
this.vertices = vertices | |
this.shared = shared | |
this.plane = Plane.fromPoints( | |
vertices[0].pos as Vector, | |
vertices[1].pos as Vector, | |
vertices[2].pos as Vector | |
) | |
} | |
clone() { | |
const vertices = this.vertices.map(function (v) { | |
return v.clone() | |
}) | |
return new Polygon(vertices, this.shared) | |
} | |
flip() { | |
this.vertices.reverse().map(function (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 { | |
plane: Plane | null | |
front: Node | null | |
back: Node | null | |
polygons: Polygon[] | |
constructor(polygons?: Polygon[]) { | |
this.plane = null | |
this.front = null | |
this.back = null | |
this.polygons = [] | |
if (polygons) this.build(polygons) | |
} | |
clone() { | |
const 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(function (p) { | |
return 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() | |
if (this.front) this.front.invert() | |
if (this.back) this.back.invert() | |
const temp = this.front | |
this.front = this.back | |
this.back = temp | |
} | |
// Recursively remove all polygons in `polygons` that are inside this BSP | |
// tree. | |
// @ts-ignore | |
clipPolygons(polygons: Polygon[]) { | |
if (!this.plane) return polygons.slice() | |
let front: string | any[] = [] | |
let back: string | Polygon[] = [] | |
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 = [] | |
// @ts-ignore | |
return front.concat(back) | |
} | |
// Remove all polygons in this BSP tree that are inside the other BSP tree | |
// `bsp`. | |
clipTo(bsp: Node) { | |
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: Polygon[]) { | |
if (!polygons.length) return | |
if (!this.plane) this.plane = polygons[0].plane.clone() | |
let front: string | any[] = [] | |
let back: string | any[] = [] | |
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) | |
} | |
} | |
} | |
export default CSG | |
// 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 | | |
// | | | |
// +-------+ | |
// |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment