Resources Numerical accuracy Calculation policy

Modifier references and lattice cages

Places the objects, curves and cages a mesh's modifiers refer to into that mesh's own space.

How the precision is kept

A referenced object is carried from its own placement into the modified mesh's space by one matrix: the inverse of the mesh's world matrix times the target's. Points are transformed in 64-bit and kept in a Float64Array. A cage point's move is the difference of two transformed points.

Calculations under this policy

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

mesh/mod-eval.js ModelCraft 0.8.1 64-bit 3 calculations Reviewed

Tests tools/modelcraft-test/mesh.mjs

Triangles of a referenced object

64-bit

The target's triangles in the modified mesh's space, for booleans and similar modifiers.

q = (toLocal × placement) × p

The two matrices are multiplied first and each point is transformed once. Positions are collected in a plain array and copied to a Float64Array.

  • mesh/mod-eval.js
  • lines 35 to 54
  • ModelCraft 0.8.1
/** Triangles of a definition (what shows: a mesh with its modifiers, a group's faces and what is placed in it), in `into`'s space. */
function trisOf(model, def, toLocal) {
  const pos = [], tri = [];
  model.walk((d, m) => {
    const M = mmul(toLocal, m);
    if (d.mesh) {
      const s = d.shown, T = s.triangulate(), base = pos.length / 3, map = new Int32Array(s.nv).fill(-1);
      let k = 0;
      for (let v = 0; v < s.nv; v++) if (!s.vertDead(v)) { map[v] = base + k++; const q = apply(M, [s.P[v * 3], s.P[v * 3 + 1], s.P[v * 3 + 2]]); pos.push(q[0], q[1], q[2]); }
      for (let i = 0; i < T.n * 3; i++) tri.push(map[s.hV[T.tri[i]]]);
      return;
    }
    // push/pull faces through the kernel's own triangles (holes handled there)
    for (const f of d.faces.values()) {
      if (f.hidden) continue;
      for (const t of faceTris(d, f)) { const b = pos.length / 3; for (const v of t) { const q = apply(M, d.verts.get(v)); pos.push(q[0], q[1], q[2]); } tri.push(b, b + 1, b + 2); }
    }
  }, def, ident());
  return { pos: Float64Array.from(pos), tri: Uint32Array.from(tri) };
}

Cage point displacements

64-bit

How far each cage point has been moved from its place on the even grid.

rest = min + (max − min) × i ÷ (n − 1); disp = M × now − M × rest

Both points are transformed and then subtracted. For a cage point that was not moved the two are nearly equal, so the small difference carries the rounding of the transformed coordinates rather than being an exact zero.

  • mesh/mod-eval.js
  • lines 84 to 86
  • ModelCraft 0.8.1
const rest = [L.min[0] + ((L.max[0] - L.min[0]) * i) / (nx - 1), L.min[1] + ((L.max[1] - L.min[1]) * j) / (ny - 1), L.min[2] + ((L.max[2] - L.min[2]) * k) / (nz - 1)];
const now = apply(M, [me.P[v * 3], me.P[v * 3 + 1], me.P[v * 3 + 2]]), restL = apply(M, rest);
disp.set(key, [now[0] - restL[0], now[1] - restL[1], now[2] - restL[2]]);

Cage around a mesh

64-bit

A box of grid points around a mesh's bounds.

d = pad × max side; min′ = min − d; max′ = max + d; p = min′ + (max′ − min′) × i ÷ (n − 1)

Each grid point is computed from its index, so the last one in a row lands on max′ to within one rounding. A side is treated as at least 1e-6 (absolute) when the pad is sized.

  • mesh/mod-eval.js
  • lines 95 to 115
  • ModelCraft 0.8.1
/** A lattice cage round a mesh's bounds: the surface of a res grid (a box of quads), and its description. */
export function makeLattice(mesh, res = [3, 3, 3], { pad = 0.05 } = {}) {
  const b = mesh.bounds() || { min: [0, 0, 0], max: [1, 1, 1] };
  const size = [0, 1, 2].map((k) => Math.max(1e-6, b.max[k] - b.min[k])), d = Math.max(...size) * pad;
  const min = b.min.map((x) => x - d), max = b.max.map((x) => x + d);
  const [nx, ny, nz] = res.map((x) => Math.max(2, Math.min(8, Math.round(x))));
  const pts = [], ids = {}, at = new Map();
  const P = (i, j, k) => {
    const key = i + ',' + j + ',' + k;
    if (!at.has(key)) { at.set(key, pts.length); ids[key] = pts.length; pts.push([min[0] + ((max[0] - min[0]) * i) / (nx - 1), min[1] + ((max[1] - min[1]) * j) / (ny - 1), min[2] + ((max[2] - min[2]) * k) / (nz - 1)]); }
    return at.get(key);
  };
  const faces = [];
  // the six sides, each a grid of quads facing out
  for (let j = 0; j < ny - 1; j++) for (let k = 0; k < nz - 1; k++) { faces.push([P(0, j, k), P(0, j, k + 1), P(0, j + 1, k + 1), P(0, j + 1, k)]); faces.push([P(nx - 1, j, k), P(nx - 1, j + 1, k), P(nx - 1, j + 1, k + 1), P(nx - 1, j, k + 1)]); }
  for (let i = 0; i < nx - 1; i++) for (let k = 0; k < nz - 1; k++) { faces.push([P(i, 0, k), P(i + 1, 0, k), P(i + 1, 0, k + 1), P(i, 0, k + 1)]); faces.push([P(i, ny - 1, k), P(i, ny - 1, k + 1), P(i + 1, ny - 1, k + 1), P(i + 1, ny - 1, k)]); }
  for (let i = 0; i < nx - 1; i++) for (let j = 0; j < ny - 1; j++) { faces.push([P(i, j, 0), P(i, j + 1, 0), P(i + 1, j + 1, 0), P(i + 1, j, 0)]); faces.push([P(i, j, nz - 1), P(i + 1, j, nz - 1), P(i + 1, j + 1, nz - 1), P(i, j + 1, nz - 1)]); }
  const cage = Mesh.fromLists(pts, faces);
  for (let h = 0; h < cage.nh; h++) cage.hFl[h] |= 8;   // FL_SHARP: a cage shows as its wires
  return { mesh: cage, lattice: { res: [nx, ny, nz], min, max, ids } };
}

In detail

Method
M = inverse(world of the mesh) × world of the target; cage rest points by dividing the box evenly; displacement = M × now − M × rest.
Number types
64-bit numbers; Float64Array for triangle positions; Uint32Array for indices.
Tolerances
A cage box side is at least 1e-6; the cage is padded by 0.05 × its largest side by default; 2 to 8 points per axis.
Limits
Only the first placement of each definition is used for its world matrix.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach