/** * GitHub Repo : https://github.com/Sean-Bradley/THREE-CSGMesh * License : MIT * * Original work 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-2022 Sean Bradley : https://sbcode.net/threejs/ * - Started with CSGMesh.js and csg-lib.js from https://github.com/manthrax/THREE-CSGMesh * - Converted to TypeScript by adding type annotations to all variables * - Converted var to const and let * - Some Refactoring * - support for three r141 */import*asTHREEfrom'three'classCSG{polygons:Polygon[]constructor(){this.polygons=[]}clone(){constcsg=newCSG()csg.polygons=this.polygons.map(function(p){returnp.clone()})returncsg}toPolygons(){returnthis.polygons}union(csg:CSG){leta=newNode(this.clone().polygons)letb=newNode(csg.clone().polygons)a.clipTo(b)b.clipTo(a)b.invert()b.clipTo(a)b.invert()a.build(b.allPolygons())returnCSG.fromPolygons(a.allPolygons())}subtract(csg:CSG){leta=newNode(this.clone().polygons)letb=newNode(csg.clone().polygons)a.invert()a.clipTo(b)b.clipTo(a)b.invert()b.clipTo(a)b.invert()a.build(b.allPolygons())a.invert()returnCSG.fromPolygons(a.allPolygons())}intersect(csg:CSG){leta=newNode(this.clone().polygons)letb=newNode(csg.clone().polygons)a.invert()b.clipTo(a)b.invert()a.clipTo(b)b.clipTo(a)a.build(b.allPolygons())a.invert()returnCSG.fromPolygons(a.allPolygons())}// Return a new CSG solid with solid and empty space switched. This solid is// not modified.inverse(){constcsg=this.clone()csg.polygons.map(function(p){p.flip()})returncsg}// Construct a CSG solid from a list of `Polygon` instances.staticfromPolygons=function(polygons:Polygon[]){constcsg=newCSG()csg.polygons=polygonsreturncsg}staticfromGeometry=function(geom:THREE.BufferGeometry,objectIndex?:object){letpolys=[]letposattr=geom.attributes.positionletnormalattr=geom.attributes.normalletuvattr=geom.attributes.uvletcolorattr=geom.attributes.colorletindex:number[]if(geom.index){index=geom.index.arrayasnumber[]}else{index=newArray((posattr.array.length/posattr.itemSize)|0)for(leti=0;i<index.length;i++)index[i]=i}lettriCount=(index.length/3)|0polys=newArray(triCount)for(leti=0,pli=0,l=index.length;i<l;i+=3,pli++){letvertices=newArray(3)for(letj=0;j<3;j++){letvi=index[i+j]letvp=vi*3letvt=vi*2letx=posattr.array[vp]lety=posattr.array[vp+1]letz=posattr.array[vp+2]letnx=normalattr.array[vp]letny=normalattr.array[vp+1]letnz=normalattr.array[vp+2]letu=uvattr.array[vt]letv=uvattr.array[vt+1]vertices[j]=newVertex({x:x,y:y,z:z,}asVector,{x:nx,y:ny,z:nz,}asVector,{x:u,y:v,z:0,}asVector,colorattr&&({x:colorattr.array[vt],y:colorattr.array[vt+1],z:colorattr.array[vt+2],}asVector))}polys[pli]=newPolygon(vertices,objectIndex)}returnCSG.fromPolygons(polys)}privatestaticttvv0=newTHREE.Vector3()privatestatictmpm3=newTHREE.Matrix3()staticfromMesh=function(mesh:THREE.Mesh,objectIndex?:object){constcsg=CSG.fromGeometry(mesh.geometry,objectIndex)CSG.tmpm3.getNormalMatrix(mesh.matrix)for(leti=0;i<csg.polygons.length;i++){letp=csg.polygons[i]for(letj=0;j<p.vertices.length;j++){letv=p.vertices[j]v.pos.copy(CSG.ttvv0.copy(newTHREE.Vector3(v.pos.x,v.pos.y,v.pos.z)).applyMatrix4(mesh.matrix))v.normal.copy(CSG.ttvv0.copy(newTHREE.Vector3(v.normal.x,v.normal.y,v.normal.z)).applyMatrix3(CSG.tmpm3))}}returncsg}staticnbuf3=(ct:number)=>{return{top:0,array:newFloat32Array(ct),write:function(v:Vector){this.array[this.top++]=v.xthis.array[this.top++]=v.ythis.array[this.top++]=v.z},}}staticnbuf2=(ct:number)=>{return{top:0,array:newFloat32Array(ct),write:function(v:Vector){this.array[this.top++]=v.xthis.array[this.top++]=v.y},}}statictoMesh=function(csg:CSG,toMatrix:THREE.Matrix4,toMaterial?:THREE.Material){letps=csg.polygonsletgeom:THREE.BufferGeometrylettriCount=0ps.forEach((p)=>(triCount+=p.vertices.length-2))geom=newTHREE.BufferGeometry()letvertices=CSG.nbuf3(triCount*3*3)letnormals=CSG.nbuf3(triCount*3*3)letuvs=CSG.nbuf2(triCount*2*3)letcolors:anyletgrps:any[]=[]ps.forEach((p)=>{letpvs=p.verticesletpvlen=pvs.lengthif(p.shared!==undefined){if(!grps[p.shared])grps[p.shared]=[]}if(pvlen&&pvs[0].color!==undefined){if(!colors)colors=CSG.nbuf3(triCount*3*3)}for(letj=3;j<=pvlen;j++){p.shared!==undefined&&grps[p.shared].push(vertices.top/3,vertices.top/3+1,vertices.top/3+2)vertices.write(pvs[0].pos)vertices.write(pvs[j-2].pos)vertices.write(pvs[j-1].pos)normals.write(pvs[0].normal)normals.write(pvs[j-2].normal)normals.write(pvs[j-1].normal)uvs.write(pvs[0].uv)uvs.write(pvs[j-2].uv)uvs.write(pvs[j-1].uv)colors&&(colors.write(pvs[0].color)||colors.write(pvs[j-2].color)||colors.write(pvs[j-1].color))}})geom.setAttribute('position',newTHREE.BufferAttribute(vertices.array,3))geom.setAttribute('normal',newTHREE.BufferAttribute(normals.array,3))geom.setAttribute('uv',newTHREE.BufferAttribute(uvs.array,2))colors&&geom.setAttribute('color',newTHREE.BufferAttribute(colors.array,3))if(grps.length){letindex:any[]=[]letgbase=0for(letgi=0;gi<grps.length;gi++){geom.addGroup(gbase,grps[gi].length,gi)gbase+=grps[gi].lengthindex=index.concat(grps[gi])}geom.setIndex(index)}letinv=newTHREE.Matrix4().copy(toMatrix).invert()geom.applyMatrix4(inv)geom.computeBoundingSphere()geom.computeBoundingBox()letm=newTHREE.Mesh(geom,toMaterial)m.matrix.copy(toMatrix)m.matrix.decompose(m.position,m.quaternion,m.scale)m.rotation.setFromQuaternion(m.quaternion)m.updateMatrixWorld()m.castShadow=m.receiveShadow=truereturnm}}// # class Vector// Represents a 3D vector.//// Example usage://// new CSG.Vector(1, 2, 3);classVector{x:numbery:numberz:numberconstructor(x=0,y=0,z=0){this.x=xthis.y=ythis.z=z}copy(v:any){this.x=v.xthis.y=v.ythis.z=v.zreturnthis}clone(){returnnewVector(this.x,this.y,this.z)}negate(){this.x*=-1this.y*=-1this.z*=-1returnthis}add(a:Vector){this.x+=a.xthis.y+=a.ythis.z+=a.zreturnthis}sub(a:Vector){this.x-=a.xthis.y-=a.ythis.z-=a.zreturnthis}times(a:number){this.x*=athis.y*=athis.z*=areturnthis}dividedBy(a:number){this.x/=athis.y/=athis.z/=areturnthis}lerp(a:Vector,t:number){returnthis.add(tv0.copy(a).sub(this).times(t))}unit(){returnthis.dividedBy(this.length())}length(){returnMath.sqrt(this.x**2+this.y**2+this.z**2)}normalize(){returnthis.unit()}cross(b:Vector){leta=thisconstax=a.x,ay=a.y,az=a.zconstbx=b.x,by=b.y,bz=b.zthis.x=ay*bz-az*bythis.y=az*bx-ax*bzthis.z=ax*by-ay*bxreturnthis}dot(b:Vector){returnthis.x*b.x+this.y*b.y+this.z*b.z}}//Temporaries used to avoid internal allocation..lettv0=newVector(0,0,0)lettv1=newVector(0,0,0)// # 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.classVertex{pos:Vectornormal:Vectoruv:Vectorcolor:anyconstructor(pos:Vector,normal:Vector,uv?:Vector,color?:Vector){this.pos=newVector().copy(pos)this.normal=newVector().copy(normal)this.uv=newVector().copy(uv)this.uv.z=0color&&(this.color=newVector().copy(color))}clone(){returnnewVertex(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:Vertex,t:number){returnnewVertex(this.pos.clone().lerp(other.pos,t),this.normal.clone().lerp(other.normal,t),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.classPlane{normal:Vectorw:numberconstructor(normal:Vector,w:number){this.normal=normalthis.w=w}clone(){returnnewPlane(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:Polygon,coplanarFront:Polygon[],coplanarBack:Polygon[],front:Polygon[],back:Polygon[]){constCOPLANAR=0constFRONT=1constBACK=2constSPANNING=3// Classify each point as well as the entire polygon into one of the above// four classes.letpolygonType=0lettypes=[]for(leti=0;i<polygon.vertices.length;i++){lett=this.normal.dot(polygon.vertices[i].pos)-this.wlettype=t<-Plane.EPSILON?BACK:t>Plane.EPSILON?FRONT:COPLANARpolygonType|=typetypes.push(type)}// Put the polygon in the correct list, splitting it when necessary.switch(polygonType){caseCOPLANAR:;(this.normal.dot(polygon.plane.normal)>0?coplanarFront:coplanarBack).push(polygon)breakcaseFRONT:front.push(polygon)breakcaseBACK:back.push(polygon)breakcaseSPANNING:letf=[],b=[]for(leti=0;i<polygon.vertices.length;i++){letj=(i+1)%polygon.vertices.lengthletti=types[i],tj=types[j]letvi=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){lett=(this.w-this.normal.dot(vi.pos))/this.normal.dot(tv0.copy(vj.pos).sub(vi.pos))letv=vi.interpolate(vj,t)f.push(v)b.push(v.clone())}}if(f.length>=3)front.push(newPolygon(f,polygon.shared))if(b.length>=3)back.push(newPolygon(b,polygon.shared))break}}// `Plane.EPSILON` is the tolerance used by `splitPolygon()` to decide if a// point is on the plane.staticEPSILON=1e-5staticfromPoints=function(a:Vector,b:Vector,c:Vector){letn=tv0.copy(b).sub(a).cross(tv1.copy(c).sub(a)).normalize()returnnewPlane(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).classPolygon{vertices:Vertex[]shared:anyplane:Planeconstructor(vertices:Vertex[],shared?:any){this.vertices=verticesthis.shared=sharedthis.plane=Plane.fromPoints(vertices[0].posasVector,vertices[1].posasVector,vertices[2].posasVector)}clone(){returnnewPolygon(this.vertices.map((v)=>v.clone()),this.shared)}flip(){this.vertices.reverse().map((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.classNode{plane?:Planefront?:Nodeback?:Nodepolygons:Polygon[]constructor(polygons?:Polygon[]){this.polygons=[]if(polygons)this.build(polygons)}clone(){letnode=newNode()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())returnnode}// Convert solid space to empty space and empty space to solid space.invert(){for(leti=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()lettemp=this.frontthis.front=this.backthis.back=temp}// Recursively remove all polygons in `polygons` that are inside this BSP// tree.clipPolygons(polygons:Polygon[]){if(!this.plane)returnpolygons.slice()letfront:Polygon[]=[]letback:Polygon[]=[]for(leti=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)elseback=[]returnfront.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(){letpolygons=this.polygons.slice()if(this.front)polygons=polygons.concat(this.front.allPolygons())if(this.back)polygons=polygons.concat(this.back.allPolygons())returnpolygons}// 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)returnif(!this.plane)this.plane=polygons[0].plane.clone()letfront:Polygon[]=[]letback:Polygon[]=[]for(leti=0;i<polygons.length;i++){this.plane.splitPolygon(polygons[i],this.polygons,this.polygons,front,back)}if(front.length){if(!this.front)this.front=newNode()this.front.build(front)}if(back.length){if(!this.back)this.back=newNode()this.back.build(back)}}staticfromJSON=function(json:CSG){returnCSG.fromPolygons(json.polygons.map((p)=>newPolygon(p.vertices.map((v)=>newVertex(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 |// | |// +-------+//
import*asTHREEfrom'three'import{OrbitControls}from'three/examples/jsm/controls/OrbitControls'importStatsfrom'three/examples/jsm/libs/stats.module'import{GLTFLoader}from'three/examples/jsm/loaders/GLTFLoader'import{CSG}from'./utils/CSGMesh'constscene=newTHREE.Scene()constgridHelper=newTHREE.GridHelper(10,10,0xaec6cf,0xaec6cf)scene.add(gridHelper)constlight1=newTHREE.PointLight(0xffffff,400)light1.position.set(5,10,5)scene.add(light1)constcamera=newTHREE.PerspectiveCamera(75,window.innerWidth/window.innerHeight,0.1,1000)camera.position.x=0camera.position.y=3camera.position.z=3constrenderer=newTHREE.WebGLRenderer()renderer.setSize(window.innerWidth,window.innerHeight)document.body.appendChild(renderer.domElement)constcontrols=newOrbitControls(camera,renderer.domElement)controls.enableDamping=truecontrols.target.set(0,1.5,0)constenvTexture=newTHREE.CubeTextureLoader().load(['img/px_25.jpg','img/nx_25.jpg','img/py_25.jpg','img/ny_25.jpg','img/pz_25.jpg','img/nz_25.jpg',])envTexture.mapping=THREE.CubeReflectionMappingconstmaterial=newTHREE.MeshPhysicalMaterial({color:0xb2ffc8,envMap:envTexture,metalness:0.5,roughness:0.1,transparent:true,opacity:0.75,//transmission: .9,side:THREE.DoubleSide,flatShading:true,})constcubeMesh=newTHREE.Mesh(newTHREE.BoxGeometry(1,2,2),newTHREE.MeshPhongMaterial({color:0xff0000}))cubeMesh.position.set(-2,1.5,-3)scene.add(cubeMesh)letmodelsReady=falseletcubeMonkeyMeshIntersect:THREE.MeshletcubeMonkeyMeshSubtract:THREE.MeshletcubeMonkeyMeshUnion:THREE.Meshconstloader=newGLTFLoader()loader.load('models/monkey.glb',function(gltf){gltf.scene.traverse(function(child){if((childasTHREE.Mesh).isMesh){if((childasTHREE.Mesh).name==='Suzanne'){constmesh=newTHREE.Mesh((childasTHREE.Mesh).geometry.clone(),newTHREE.MeshPhongMaterial({color:0x00ff00}))mesh.position.set(2,1.5,-3)mesh.geometry.scale(1.15,1.15,1.15)scene.add(mesh)//adding only the monkey mesh to the scene and ignoring everything else inside the gltfconstcubeCSG=CSG.fromGeometry(cubeMesh.geometry.clone().translate(-0.5,0,0))constmonkeyMeshCSG=CSG.fromMesh(mesh)constcubeMonkeyMeshIntersectCSG=cubeCSG.intersect(monkeyMeshCSG.clone())cubeMonkeyMeshIntersect=CSG.toMesh(cubeMonkeyMeshIntersectCSG,newTHREE.Matrix4())cubeMonkeyMeshIntersect.material=materialcubeMonkeyMeshIntersect.position.set(-3,1.5,0)scene.add(cubeMonkeyMeshIntersect)constcubeMonkeyMeshSubtractCSG=cubeCSG.subtract(monkeyMeshCSG.clone())cubeMonkeyMeshSubtract=CSG.toMesh(cubeMonkeyMeshSubtractCSG,newTHREE.Matrix4())cubeMonkeyMeshSubtract.material=materialcubeMonkeyMeshSubtract.position.set(0,1.5,0)scene.add(cubeMonkeyMeshSubtract)constcubeMonkeyMeshUnionCSG=cubeCSG.union(monkeyMeshCSG.clone())cubeMonkeyMeshUnion=CSG.toMesh(cubeMonkeyMeshUnionCSG,newTHREE.Matrix4())cubeMonkeyMeshUnion.material=materialcubeMonkeyMeshUnion.position.set(3,1.5,0)scene.add(cubeMonkeyMeshUnion)modelsReady=true}}})},(xhr)=>{console.log((xhr.loaded/xhr.total)*100+'% loaded')},(error)=>{console.log(error)})window.addEventListener('resize',onWindowResize,false)functiononWindowResize(){camera.aspect=window.innerWidth/window.innerHeightcamera.updateProjectionMatrix()renderer.setSize(window.innerWidth,window.innerHeight)}conststats=newStats()document.body.appendChild(stats.dom)functionanimate(){requestAnimationFrame(animate)controls.update()if(modelsReady){cubeMonkeyMeshIntersect.rotation.y+=0.005cubeMonkeyMeshSubtract.rotation.y+=0.005cubeMonkeyMeshUnion.rotation.y+=0.005}render()stats.update()}functionrender(){renderer.render(scene,camera)}animate()