Resources Numerical accuracy Calculation policy

Group modifiers: scale and array placements

Works out where each copy of a group or component sits when it has Scale and Array modifiers.

How the precision is kept

Each placement is a 4 × 4 matrix of 64-bit numbers. A scale is one matrix product; an array multiplies its step by the copy's index, so the position of copy i does not accumulate i additions. The result is checked for finite, bounded entries before it replaces the placements.

Calculations under this policy

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

group-mods.js ModelCraft 0.8.1 64-bit 2 calculations Reviewed

Tests tools/modelcraft-test/gh2.mjs

Placements from the modifier stack

64-bit

The list of placement matrices for the source, first the original.

stepₖ = (hiₖ − loₖ) × relₖ + absₖ; Mᵢ = translate(i × step) × M

The size is taken from the eight box corners of the source under every placement so far, by min and max. Copy i uses i × step in one multiplication, so the last copy is as exact as the first. The soundness check compares the diagonal product with 1e-12, an absolute number.

  • group-mods.js
  • lines 57 to 80
  • ModelCraft 0.8.1
/** The placements of the source for these items: matrices (column-major 4×4), first = the original. */
export function groupMatrices(ops, box) {
  let ms = [ident()];
  for (const op of ops) {
    if (!op.on) continue;
    const p = op.params;
    if (op.type === 'scale') {
      const S = ident(); [S[0], S[5], S[10]] = p.factors;
      ms = ms.map((m) => mmul(S, m));
    } else if (op.type === 'array') {
      if (ms.length * p.count > MAX_COPIES) bad();
      // the step is a fraction of the size of everything so far (the mesh stack's rule), plus the extra step
      let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity];
      if (box) for (const m of ms) for (const c of cornersOf(box)) { const q = apply(m, c); for (let i = 0; i < 3; i++) { lo[i] = Math.min(lo[i], q[i]); hi[i] = Math.max(hi[i], q[i]); } }
      else { lo = [0, 0, 0]; hi = [0, 0, 0]; }
      const step = [0, 1, 2].map((i) => (hi[i] - lo[i]) * p.rel[i] + p.abs[i]);
      const next = [];
      for (let i = 0; i < p.count; i++) for (const m of ms) next.push(mmul(translation(step.map((x) => x * i)), m));
      ms = next;
    } else bad();
    if (ms.some((m) => !m.every(ok) || m[0] * m[5] * m[10] < 1e-12)) bad();
  }
  return ms;
}

Number of copies

Exact

How many placements the enabled Array items make together.

copies = Π countᵢ

A product of whole numbers. The callers compare it with the limit of 1000 copies.

  • group-mods.js
  • lines 44 to 45
  • ModelCraft 0.8.1
/** How many copies the enabled items make (1 = just the source). */
export const copyCount = (ops) => ops.reduce((n, m) => (m.on && m.type === 'array' ? n * m.params.count : n), 1);

In detail

Method
Scale: S × M. Array: step = (size of everything so far) × relative step + absolute step, per axis; copy i is translated by i × step.
Number types
64-bit numbers.
Tolerances
Every matrix entry must be finite and below 1e7 in magnitude; the product of the three diagonal entries must be at least 1e-12 (absolute). At most 1000 copies and 32 items.
Limits
Only scale and linear array are evaluated for groups. The diagonal test also refuses any placement whose three diagonal entries multiply to a negative number.

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