Resources Numerical accuracy Calculation policy

Body move, scale and path pattern arithmetic

Builds the matrices that move, rotate and scale bodies, and the positions of pattern copies along a path.

How the precision is kept

Closed-form 64-bit expressions on plain arrays. The smallest rotation between two directions uses atan2 of the cross and dot products, which keeps its digits for small and near-half-turn angles alike. Pattern stations are computed as start + step × i, not by repeated addition.

Calculations under this policy

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

bodies.js PartCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/partcraft-test/test-bodies.mjs

Scale about a point

64-bit

The matrix that scales a body by three factors about a fixed point.

p′ = c + diag(sx, sy, sz) × (p − c); translation = c × (1 − s)

The translation is c × (1 − s) per axis. For a factor very close to 1 the term 1 − s loses digits, but the product is then small compared with the coordinates it is added to.

  • bodies.js
  • lines 217 to 218
  • PartCraft 0.8.1
/** Column-major 4×4 of p ↦ c + diag(sx, sy, sz)(p − c). */
export const scaleAbout = (c, sx, sy, sz) => [sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, c[0] * (1 - sx), c[1] * (1 - sy), c[2] * (1 - sz), 1];

Smallest rotation between two directions

64-bit

The rotation that turns one unit direction onto another by the smallest angle.

k = (a × b) ÷ |a × b|; θ = atan2(|a × b|, a · b)

atan2 of the cross length and the dot product is used instead of acos, so the angle keeps its digits near 0 and near a half turn. When the cross length is below 1e-12 the directions are treated as parallel: the identity, or a half turn about a perpendicular. The inputs are taken as unit vectors without a check.

  • bodies.js
  • lines 219 to 229
  • PartCraft 0.8.1
/** 3×3 rotation (rows) taking unit a to unit b by the smallest turn (Rodrigues); a ≈ −b turns π about `perp`. */
export function minimalRotation(a, b, perp = null) {
  const v = cross(a, b), c = dot(a, b), s = len(v);
  let k, ang;
  if (s < 1e-12) {
    if (c > 0) return [[1, 0, 0], [0, 1, 0], [0, 0, 1]];
    k = perp || (Math.abs(a[0]) < 0.9 ? cross(a, [1, 0, 0]) : cross(a, [0, 1, 0]));
    k = mul(k, 1 / len(k)); ang = Math.PI;
  } else { k = mul(v, 1 / s); ang = Math.atan2(s, c); }
  return axisRotation(k, ang);
}

Rotation about an axis

64-bit

The 3 × 3 rotation about a unit axis by an angle in radians.

R = cos θ I + sin θ [k]× + (1 − cos θ) k kᵀ

The term 1 − cos θ is formed by subtraction, which loses digits for very small angles; those entries are then small next to cos θ on the diagonal. The axis is not normalized here.

  • bodies.js
  • lines 230 to 234
  • PartCraft 0.8.1
/** 3×3 rotation (rows) about unit axis k by `ang` radians. */
export function axisRotation(k, ang) {
  const c = Math.cos(ang), s = Math.sin(ang), t = 1 - c, [x, y, z] = k;
  return [[t * x * x + c, t * x * y - s * z, t * x * z + s * y], [t * x * y + s * z, t * y * y + c, t * y * z - s * x], [t * x * z - s * y, t * y * z + s * x, t * z * z + c]];
}

Rigid motion about a point

64-bit

The 4 × 4 matrix of a rotation about a point followed by a translation.

p′ = R (p − c) + c + t; offset = c − R c + t

The offset c − R c subtracts nearly equal vectors when the rotation is small or the center is far from the origin; the lost digits are relative to |c|, the same scale as the coordinates the offset is added to.

  • bodies.js
  • lines 237 to 241
  • PartCraft 0.8.1
/** Column-major 4×4 of p ↦ R(p − c) + c + t. */
export function rigid(R, c = [0, 0, 0], t = [0, 0, 0]) {
  const o = add(sub(c, mat3Apply(R, c)), t);
  return [R[0][0], R[1][0], R[2][0], 0, R[0][1], R[1][1], R[2][1], 0, R[0][2], R[1][2], R[2][2], 0, o[0], o[1], o[2], 1];
}

Pattern stations along a path

64-bit

The arc length at which each copy of a path pattern sits.

equal: step = L ÷ count (closed) or room ÷ (count − 1) (open); s_i = s₀ ± step × i, wrapped into [0, L) on a closed path

Each station is s₀ + step × i, so rounding does not accumulate along the pattern. On a closed path the station is wrapped with a double modulo. The fit test for a distance pattern has a relative slack of 1e-9 plus an absolute 1e-9. The 6-decimal rounding in this function is only in the error message.

  • bodies.js
  • lines 243 to 263
  • PartCraft 0.8.1
/**
 * Where the instances of a path pattern sit, along a path of total length L, the seed's own spot at arc length s0.
 * → [arc lengths] (instance 0 = the seed), or { error } in words. Closed paths wrap; equal spacing on an open path
 * spreads the count from the seed to the end it points at, on a closed one round the whole loop.
 */
export function pathStations({ L, s0, closed, count, spacing, d, reverse }) {
  const dir = reverse ? -1 : 1;
  const room = closed ? L : dir > 0 ? L - s0 : s0;
  if (count < 2) return [s0];
  let step;
  if (spacing === 'equal') {
    if (!closed && room <= 1e-9 * Math.max(1, L)) return { error: dir > 0 ? t('model.bodies.path.seed_at_end') : t('model.bodies.path.seed_at_start') };
    step = closed ? L / count : room / (count - 1);
  } else {
    step = d;
    if (!closed && step * (count - 1) > room * (1 + 1e-9) + 1e-9) return { error: t('model.bodies.path.too_short', { count, d: +d.toFixed(6), need: +(step * (count - 1)).toFixed(6), left: +room.toFixed(6) }) };
  }
  const out = [];
  for (let i = 0; i < count; i++) { let s = s0 + dir * step * i; if (closed) s = ((s % L) + L) % L; out.push(s); }
  return out;
}

In detail

Method
Rodrigues rotation formula; scale about a point as p ↦ c + S(p − c); rigid motion p ↦ R(p − c) + c + t; arc-length stations with wrap-around on closed paths.
Number types
64-bit numbers.
Tolerances
Two directions count as parallel when |a × b| < 1e-12 (unit vectors). A seed counts as at the path's end when the room left is at most 1e-9 × max(1, L). A distance pattern fits when step × (count − 1) ≤ room × (1 + 1e-9) + 1e-9.
Limits
minimalRotation expects unit vectors and does not normalize them.

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