diff --git a/app.js b/app.js index b7f84137..7dc339a9 100644 --- a/app.js +++ b/app.js @@ -485,6 +485,7 @@ const script = { "&main/mesh" ], mesh_work : [ + "ext/jscad", "&mesh/work" ], mesh_pool : [ diff --git a/package.json b/package.json index 1c554fd5..695e179c 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "connect": "^3.7.0", "earcut": "^2.2.3", "express-useragent": "^1.0.13", + "@jscad/modeling": "^2.7.1", "jszip": "^3.7.1", "moment": "^2.24.0", "pixi.js": "^4.8.9", diff --git a/src/geo/csg.js b/src/geo/csg.js index d3152d39..d4e537ed 100644 --- a/src/geo/csg.js +++ b/src/geo/csg.js @@ -9,440 +9,48 @@ let base = self.base; if (base.CSG) return; -// # 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(polygons) { - this.polygons = polygons || []; +base.CSG = { + + union() { + return jscadModeling.booleans.union(...arguments); + }, + + subtract() { + return jscadModeling.booleans.subtract(...arguments); + }, + + intersect() { + return jscadModeling.booleans.intersect(...arguments); + }, + + toPositionArray(geom) { + return geom.polygons + .map(p => p.vertices.flat()) + .map(a => base.util.triangulate(a, undefined, 3)) + .flat().toFloat32(); + }, + + // Construct a CSG from a THREE Geometry BufferAttribute array (or similar) + fromPositionArray(array) { + let polys = []; + for (let i=0, l=array.length; ip.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) { - return new CSG(polygons); }; -// Construct a CSG from a THREE Geometry BufferAttribute array (or similar) -CSG.fromPositionArray = function(array) { - let polys = []; - for (let i=0, l=array.length; i 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); - } - } -} - -// add to base objects -base.CSG = { Solid: CSG, Vector, Vertex, Plane, Node }; - })(); diff --git a/src/mesh/model.js b/src/mesh/model.js index 8855b71c..d26dfee0 100644 --- a/src/mesh/model.js +++ b/src/mesh/model.js @@ -162,17 +162,10 @@ mesh.model = class MeshModel extends mesh.object { return; } worker.model_rebuild({ - mode: opt.mode || 'slice', matrix: this.matrix, id: this.id }).then(data => { - let { vertices, lines } = data; - if (vertices) { - mesh.api.group.new([new mesh.model({ - file: `healed`, - mesh: data.vertices - })]).floor().centerXY().setSelected(); - } + let { lines } = data; if (lines) { let points = []; for (let i=0; i translate_encode(rec.id, rec.matrix)); + let solids = arrays.map(a => base.CSG.fromPositionArray(a)); + let union = base.CSG.union(...solids); + return base.CSG.toPositionArray(union); + }, + // used to generate a list for split snapping zlist(data) { let { id, matrix, round } = data; @@ -296,29 +303,10 @@ let model = { }, rebuild(data, send) { - let { id, matrix, mode } = data; + let { id, matrix } = data; log(`${id} | rebuilding...`); let points = translate_encode(id, matrix); log(`${id} | ${points.length} points`); - - if (mode === 'csg') { - log(`${id} | intersecting...`); - let box = base.CSG.Solid.fromPositionArray( - new THREE.BoxGeometry(3000, 3000, 3000) - .toNonIndexed().attributes.position.array - ); - let solid = base.CSG.Solid.fromPositionArray(points); - let solution = box.intersect(solid); - let vertices = solution.polygons - .map(p => p.vertices.map(v => v.pos)) - .map(a => { - let pa = a.map( v => [ v.x, v.y, v.z ]).flat(); - let ec = base.util.triangulate(pa, undefined, 3); - return ec; - }).flat().toFloat32(); - return { vertices }; - } - send.async(); let layers = []; base.slice(points, {