swap in openjscad modeling library for csg

This commit is contained in:
Stewart Allen 2022-01-30 20:24:02 -05:00
commit efc3c5c77a
5 changed files with 36 additions and 445 deletions

1
app.js
View file

@ -485,6 +485,7 @@ const script = {
"&main/mesh"
],
mesh_work : [
"ext/jscad",
"&mesh/work"
],
mesh_pool : [

View file

@ -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",

View file

@ -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; i<l; ) {
polys.push([ [
array[i++],
array[i++],
array[i++],
],[
array[i++],
array[i++],
array[i++],
],[
array[i++],
array[i++],
array[i++],
] ]);
}
return jscadModeling.geometries.geom3.fromPoints(polys);
}
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<l; ) {
polys.push(new Polygon([
new Vertex(new Vector(
array[i++],
array[i++],
array[i++],
)),
new Vertex(new Vector(
array[i++],
array[i++],
array[i++],
)),
new Vertex(new Vector(
array[i++],
array[i++],
array[i++],
)),
]));
}
return CSG.fromPolygons(polys);
};
CSG.computeFaceNormal = function(vA,vB,vC) {
let cb = vC.clone().sub(vB);
let ab = vA.clone().sub(vB);
cb.cross(ab);
return cb.normalize();
};
// # class Vector
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()
// zero normal for Vertices when none provided
let nnn = 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 || nnn);
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);
}
}
}
// add to base objects
base.CSG = { Solid: CSG, Vector, Vertex, Plane, Node };
})();

View file

@ -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<lines.length;) {

View file

@ -121,6 +121,13 @@ let model = {
return data;
},
union(recs) {
let arrays = recs.map(rec => 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, {