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