Resources Numerical accuracy Calculation policy

Print tool results back in the model

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.js ModelCraft 0.8.1 32-bit in part 6 calculations Reviewed

Tests tools/modelcraft-test/print07.mjs, tools/modelcraft-test/kernel-print.mjs

Triangles of a placement

64-bit

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.

key = round(x ÷ weld), round(y ÷ weld), round(z ÷ weld); sharp if n₁·n₂ < cos(sharpDeg)

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.

  • print-solid.js
  • lines 129 to 133
  • ModelCraft 0.8.1
const volumeOf = (tris) => {
  let v = 0;
  for (const [a, b, c] of tris) v += (a[0] * (b[1] * c[2] - b[2] * c[1]) + a[1] * (b[2] * c[0] - b[0] * c[2]) + a[2] * (b[0] * c[1] - b[1] * c[0])) / 6;
  return v;
};

Result as a solid on a 1 µm grid

64-bit

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.

  • print-solid.js
  • lines 135 to 149
  • ModelCraft 0.8.1
export function solidFromTris(model, tris, name, { sharpDeg = 30, max = SOLID_MAX_TRIS, maxFaces = SOLID_MAX_FACES } = {}) {
  if (!tris.length || tris.length > max) return { def: null, why: 'size' };
  const g = (x) => Math.round(x * 1e6) / 1e6;   // the 1 µm grid of solids.js: exact sizes stay exact, flat sides flat
  const polys = tris.map((t) => t.map((p) => [g(p[0]), g(p[1]), g(p[2])]));
  const d = meshDef(model, polys, name, 'group');
  mergeCoplanar(d, [...d.edges].filter(([, e]) => e.soft).map(([id]) => id));
  if (d.faces.size > maxFaces) { model.defs.delete(d.id); return { def: null, why: 'size' }; }
  const info = solidInfo(d), want = volumeOf(tris);
  if (!info.closed || !(Math.abs(info.volume - want) <= Math.abs(want) * 1e-4 + 1e-15)) {
    model.defs.delete(d.id);
    return { def: null, why: 'open' };
  }
  softenEdges(d, [...d.edges.keys()], sharpDeg);
  return { def: d, why: null };
}

In detail

Method
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.

Every policy that takes this approach