diff --git a/src/geo/base.js b/src/geo/base.js index 9085a2d0..8326834e 100644 --- a/src/geo/base.js +++ b/src/geo/base.js @@ -461,6 +461,60 @@ return out; } + // wrapper for earcut that handles higher order dimensions and finds the + // two with the greatest delta to pass to the earcut algorith, then returns + // an unwrapped array in the original dimensions + function triangulate(array, holes, dims) { + let narray, d1, d2; + if (dims === 2) { + narray = array; + } else { + let min = new Array(dims).fill(Infinity); + let max = new Array(dims).fill(-Infinity); + for (let i=0, l=array.length; i dmax) { + dmax = delta[i]; + d1 = i; + } + } + delta[d1] = dmax = 0; + for (let i=0; i dmax) { + dmax = delta[i]; + d2 = i; + } + } + narray = new Array((array.length / dims) * 2); + for (let i=0, j=0; i { if (!v) return v; let [lt,rt] = v.toString().split('.'); diff --git a/src/geo/csg.js b/src/geo/csg.js new file mode 100644 index 00000000..d3152d39 --- /dev/null +++ b/src/geo/csg.js @@ -0,0 +1,448 @@ +// ## License +// +// Copyright (c) 2011 Evan Wallace (http://madebyevan.com/), under the MIT license. +// Modified for GridApps by Stewart Allen (sa@grid.space) + +(function() { + +// dep: geo.base +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 || []; + } + + 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) { + 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/api.js b/src/mesh/api.js index 0b896c8d..df369b68 100644 --- a/src/mesh/api.js +++ b/src/mesh/api.js @@ -367,7 +367,7 @@ let tool = { mesh.api.group.new(models) .centerModels() .position(mid.x, mid.y, mid.z) - .select(); + .setSelected(); }); }, @@ -391,7 +391,7 @@ let tool = { } Promise.all(promises).then(() => { if (newmdl.length) { - mesh.api.group.new(newmdl, undefined, "patch").select(); + mesh.api.group.new(newmdl, undefined, "patch").setSelected(); } api.log.emit('analysis complete').unpin(); }); @@ -405,6 +405,10 @@ let tool = { id: m.id, opt }).then(data => { if (data) { + // mesh.api.group.new([new mesh.model({ + // file: `healed`, + // mesh: data.vertices + // })]).floor().centerXY().setSelected(); m.reload( data.vertices, data.indices, diff --git a/src/mesh/work.js b/src/mesh/work.js index 888d5dd0..3fd5103f 100644 --- a/src/mesh/work.js +++ b/src/mesh/work.js @@ -16,6 +16,7 @@ moto.client.start(`/code/mesh_pool?${gapp.version}`, moto.client.max() * 0); // dep: geo.polygons // dep: geo.bounds // dep: geo.slicer +// dep: geo.csg // dep: ext.clip2 gapp.finalize("mesh.work", [ "moto.license", // dep: moto.license @@ -243,8 +244,29 @@ let model = { let { id, opt } = data; let tool = analyze(id, opt); log(`${id} | unrolling...`); + let unrolled = tool.unrolled(); + + // log(`${id} | intersecting...`); + // let box = new THREE.BoxGeometry(10000, 10000, 10000).attributes.position.array; + // box = base.CSG.Solid.fromPositionArray(box); + // + // let solid = base.CSG.Solid.fromPositionArray(unrolled); + // // let inter = box.intersect(solid); + // let inter = solid.union(solid.clone()); + // let ears = inter.polygons + // .map(p => p.vertices.map(v => v.pos)) + // .map(a => { + // let pa = a.map( v => [ v.x, v.y, v.z ]).flat().map(v => v.round(2)); + // let ec = base.util.triangulate(pa, undefined, 3); + // console.log({pa, ec}); + // return ec; + // });//.flat(); + // + // console.log({solid, inter, ears, ef: ears.flat()}); + // unrolled = ears.flat(); + return { - vertices: tool.unrolled().toFloat32(), + vertices: unrolled.toFloat32(), }; }, @@ -295,7 +317,7 @@ let model = { rebuild(data, send) { send.async(); - let { id, matrix } = data; + let { id, matrix, mode } = data; log(`${id} | rebuilding...`); let points = translate_encode(id, matrix); log(`${id} | ${points.length} points`);