Collects a placement's triangles, runs the union or subtraction of solids, and turns the result into a solid group or a mesh in the model.
How the precision is kept
Triangles are collected and transformed in 64-bit meters. The boolean runs in manifold-3d, which is given the corners as a Float32Array measured from the middle of the parts' box and hands back 32-bit corners; the middle is added again in 64-bit. A result kept as a solid is then put on a 1 µm grid and accepted only if it is closed and its volume matches within 1e-4 relative.
Open item. Pass and read back the boolean's corners in 64-bit, or tell the person in the tool's result that the corners passed through 32-bit numbers.
Calculations under this policy
6 calculations in 1 source file. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
print-solid.jsModelCraft 0.8.1 32-bit in part6 calculationsReviewed
Every triangle of a placement and what is nested inside it, in the open context's coordinates.
p′ = M · p, M = product of the nested placement matrices
Matrices are multiplied down the nesting (at most 32 levels) and each corner is transformed once, in 64-bit.
print-solid.js
lines 37 to 46
ModelCraft 0.8.1
export function placementTris(model, ctxDef, iid) {
const inst = ctxDef.insts.get(iid), out = [];
const visit = (d, m, depth) => {
if (!d || depth > 32) return;
for (const f of d.faces.values()) for (const tri of faceTris(d, f)) out.push(tri.map((v) => apply(m, d.verts.get(v))));
for (const sub of d.insts.values()) visit(model.def(sub.def), mmul(m, sub.m), depth + 1);
};
if (inst) visit(model.def(inst.def), inst.m, 0);
return out;
}
Welding triangles into a mesh
64-bit
Joins corners that fall in the same cell of a 1e-7 m grid and marks edges sharper than a given angle.
The key is a rounded grid cell, so two corners closer than 1e-7 m that fall either side of a cell boundary are not joined. The first corner in a cell keeps its own 64-bit coordinates; nothing is averaged or rounded.
print-solid.js
lines 48 to 69
ModelCraft 0.8.1
export function meshFromTris(tris, { sharpDeg = 30, weld = 1e-7 } = {}) {
const key = new Map(), pts = [], faces = [];
const vid = (p) => {
const k = Math.round(p[0] / weld) + ',' + Math.round(p[1] / weld) + ',' + Math.round(p[2] / weld);
let i = key.get(k);
if (i === undefined) { i = pts.length; key.set(k, i); pts.push([p[0], p[1], p[2]]); }
return i;
};
for (const t of tris) {
const a = vid(t[0]), b = vid(t[1]), c = vid(t[2]);
if (a !== b && b !== c && a !== c) faces.push([a, b, c]);
}
const mesh = Mesh.fromLists(pts, faces);
const cos = Math.cos(sharpDeg * Math.PI / 180);
for (let h = 0; h < mesh.nh; h++) {
const tw = mesh.hTw[h];
if (tw < h) continue; // each edge once (open edges: tw = -1 < h)
const n1 = mesh.faceNormal(mesh.hF[h]), n2 = mesh.faceNormal(mesh.hF[tw]);
if (n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2] < cos) { mesh.hFl[h] |= FL_SHARP; mesh.hFl[tw] |= FL_SHARP; }
}
return mesh;
}
Triangles into the boolean library
32-bit in part
Hands a part's corners to manifold-3d, measured from a nearby origin.
q = p − o; corners welded where round(q × 10⁷) agree; stored as 32-bit
The corners are subtracted from o in 64-bit and then stored in a Float32Array. Measuring from o, the middle of the parts' box, keeps the 32-bit numbers small, which is the reason the code gives for it.
print-solid.js
lines 71 to 87
ModelCraft 0.8.1
/** Triangles → a manifold Manifold near `o` (32-bit vertices keep their precision there). */
function toManifold(M, tris, o) {
const key = new Map(), verts = [], idx = [];
const vid = (p) => {
const q = [p[0] - o[0], p[1] - o[1], p[2] - o[2]];
const k = q.map((x) => Math.round(x * 1e7)).join(',');
let i = key.get(k);
if (i === undefined) { i = verts.length / 3; key.set(k, i); verts.push(q[0], q[1], q[2]); }
return i;
};
for (const tri of tris) { const a = vid(tri[0]), b = vid(tri[1]), c = vid(tri[2]); if (a !== b && b !== c && a !== c) idx.push(a, b, c); }
const mesh = new M.Mesh({ numProp: 3, vertProperties: new Float32Array(verts), triVerts: new Uint32Array(idx) });
mesh.merge();
const man = new M.Manifold(mesh);
if (man.status() !== 'NoError' || man.isEmpty()) { man.delete?.(); const e = new Error('open'); e.code = 'open'; throw e; }
return man;
}
Union and subtraction
32-bit in part
The triangles of the kept parts joined, less the cut parts.
o = middle of the box of all parts; result corner = o + 32-bit corner from the library
The result's corners are read from the library's vertProperties array, which is 32-bit, and o is added in 64-bit. A 32-bit number between 4 and 8 has a spacing of about 4.8 × 10⁻⁷, so on parts reaching 4 to 8 m from their middle the returned corners lie on a grid of about half a micrometer.
print-solid.js
lines 89 to 107
ModelCraft 0.8.1
export function booleanTris(M, keep, cut = [], op = 'subtract') {
const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
for (const ts of [...keep, ...cut]) for (const tri of ts) for (const p of tri) for (let k = 0; k < 3; k++) { if (p[k] < lo[k]) lo[k] = p[k]; if (p[k] > hi[k]) hi[k] = p[k]; }
const o = lo.map((v, k) => (v + hi[k]) / 2), made = [];
try {
const of = (ts) => { const m = toManifold(M, ts, o); made.push(m); return m; };
const A = keep.map(of), B = cut.map(of);
let r = M.Manifold.union(A);
made.push(r);
if (op === 'subtract' && B.length) { const u = M.Manifold.union(B); made.push(u); r = r.subtract(u); made.push(r); }
if (r.isEmpty()) { const e = new Error('empty'); e.code = 'empty'; throw e; }
const g = r.getMesh(), P = (i) => [o[0] + g.vertProperties[i * g.numProp], o[1] + g.vertProperties[i * g.numProp + 1], o[2] + g.vertProperties[i * g.numProp + 2]];
const out = [];
for (let i = 0; i < g.triVerts.length; i += 3) out.push([P(g.triVerts[i]), P(g.triVerts[i + 1]), P(g.triVerts[i + 2])]);
return out;
} finally {
for (const m of made) m.delete?.();
}
}
Volume of a triangle set
64-bit
The volume the triangles enclose, used to check that a solid kept the result's volume.
V = Σ a·(b × c) ÷ 6
Signed tetrahedron volumes to the coordinate origin, summed in order, in absolute model coordinates. Far from the origin the terms nearly cancel and the sum loses digits.
Turns the result's triangles into a solid group of flat faces, or reports why it stays a mesh.
g(x) = round(x × 10⁶) ÷ 10⁶; keep if closed and |V_solid − V| ≤ |V| × 10⁻⁴ + 10⁻¹⁵
Every corner coordinate is rounded to the nearest micrometer before the faces are built, so the stored solid is on that grid. The volume test is relative (1e-4) with a small absolute floor.
Booleans by manifold-3d (the version is not stated in this file); coplanar triangles merged into faces by the model kernel; volumes by signed tetrahedron sums.
Number types
64-bit numbers in the model; Float32Array into and out of the boolean; meters.
Tolerances
Corners are welded on a grid of 1e-7 m before the boolean and in meshFromTris. A solid result is rounded to 1e-6 m. The volume check allows |ΔV| ≤ |V| × 1e-4 + 1e-15.
Limits
Corners that come back from a boolean have 32-bit precision relative to the middle of the parts. Results with more than 100 000 triangles, or more than 20 000 faces after merging, stay meshes.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.