Resources Numerical accuracy Calculation policy

Vectors, plane frames and transforms

The small vector and 4 × 4 matrix helpers that place sketches on planes and move, rotate and mirror parts.

How the precision is kept

Vectors are plain arrays of 64-bit numbers and matrices are 16 numbers in column-major order. Every helper is a closed-form expression; angles are converted with Math.PI. The only tolerances are a zero-length guard in the normalization and a singularity test in the small linear solver.

Calculations under this policy

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

vec.js PartCraft 0.8.1 64-bit 10 calculations Reviewed

Tests bin/test-partcraft.mjs

Unit vector

64-bit

Scales a vector to length 1.

a ÷ |a|

The length is Math.hypot and each component is divided by it. A zero vector is returned unchanged (divided by 1), so callers get a zero vector rather than NaN.

  • vec.js
  • line 19
  • PartCraft 0.8.1
export const norm = (a) => { const l = len(a) || 1; return [a[0] / l, a[1] / l, a[2] / l]; };

Frame of a plane

64-bit

The origin and the x, y and normal axes given to a sketch plane.

o = (p·n) n; x = Y × n (or world X made square to n when |n_y| > 0.9); y = n × x

The normal is normalized first; x is normalized after the cross product and y is the cross product of two unit vectors without a further normalization. The switch at |n_y| > 0.9 keeps the cross product away from a near-zero length.

  • vec.js
  • lines 33 to 46
  • PartCraft 0.8.1
/**
 * The frame PartCraft gives any plane with normal n through point p: origin = the world origin
 * projected onto the plane; x = world X when the plane faces up/down, else Y × n (so a plane
 * parallel to a base plane gets exactly that base plane's axes); y = n × x.
 */
export function frameFor(n, p) {
  n = norm(n);
  let x;
  if (Math.abs(n[1]) > 0.9) x = norm(sub([1, 0, 0], mul(n, n[0])));
  else x = norm(cross([0, 1, 0], n));
  const y = cross(n, x);
  const d = dot(p, n);
  return { o: mul(n, d), x, y, n };
}

Sketch point to model space

64-bit

The 3D position of a sketch point (u, v) on its plane.

p = o + u x + v y

One 64-bit expression per coordinate: two products and two additions.

  • vec.js
  • line 48
  • PartCraft 0.8.1
export const toWorld = (f, u, v) => [f.o[0] + f.x[0] * u + f.y[0] * v, f.o[1] + f.x[1] * u + f.y[1] * v, f.o[2] + f.x[2] * u + f.y[2] * v];

Model point to sketch coordinates

64-bit

The sketch coordinates of a 3D point on a plane.

u = (p − o)·x; v = (p − o)·y

The origin is subtracted first, then two dot products; the result is exact in form when x and y are unit and square, which the frame builder provides to rounding.

  • vec.js
  • line 49
  • PartCraft 0.8.1
export const toPlane = (f, p) => { const d = sub(p, f.o); return [dot(d, f.x), dot(d, f.y)]; };

Rotation about an axis

64-bit

The matrix that turns geometry by an angle about an axis through a point.

R = c I + s [d]× + (1 − c) d dᵀ, c = cos a, s = sin a, a = deg × π ÷ 180; translation = p − R p

The axis is normalized and the angle converted with Math.PI. The translation is computed as p − R p, which subtracts nearly equal numbers when the angle is small or the point is far from the origin; its absolute error stays at the rounding of p.

  • vec.js
  • lines 60 to 71
  • PartCraft 0.8.1
/** Rotation by `deg` about the axis through point p with direction d (right-hand rule). */
export function rotationAbout(p, d, deg) {
  const [x, y, z] = norm(d);
  const a = deg * Math.PI / 180, c = Math.cos(a), s = Math.sin(a), t = 1 - c;
  const r = [
    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,
  ]; // column-major 3×3
  const rp = [r[0] * p[0] + r[3] * p[1] + r[6] * p[2], r[1] * p[0] + r[4] * p[1] + r[7] * p[2], r[2] * p[0] + r[5] * p[1] + r[8] * p[2]];
  return [r[0], r[1], r[2], 0, r[3], r[4], r[5], 0, r[6], r[7], r[8], 0, p[0] - rp[0], p[1] - rp[1], p[2] - rp[2], 1];
}

Reflection in a plane

64-bit

The matrix that mirrors geometry in a plane.

M = I − 2 n nᵀ; translation = 2 (o·n) n

The normal is normalized, then each entry is one product and one subtraction.

  • vec.js
  • lines 73 to 79
  • PartCraft 0.8.1
/** Reflection in the plane through o with normal n. */
export function reflection(o, n) {
  n = norm(n);
  const k = 2 * dot(o, n);
  const m = (i, j) => (i === j ? 1 : 0) - 2 * n[i] * n[j];
  return [m(0, 0), m(1, 0), m(2, 0), 0, m(0, 1), m(1, 1), m(2, 1), 0, m(0, 2), m(1, 2), m(2, 2), 0, k * n[0], k * n[1], k * n[2], 1];
}

Matrix applied to a point

64-bit

Transforms a point by a 4 × 4 matrix.

p′ = M₃ₓ₃ p + t

Three products and three additions per coordinate, in a fixed order. The bottom row of the matrix is not used.

  • vec.js
  • lines 81 to 87
  • PartCraft 0.8.1
export function applyMat(m, p) {
  return [
    m[0] * p[0] + m[4] * p[1] + m[8] * p[2] + m[12],
    m[1] * p[0] + m[5] * p[1] + m[9] * p[2] + m[13],
    m[2] * p[0] + m[6] * p[1] + m[10] * p[2] + m[14],
  ];
}

Matrix product

64-bit

Multiplies two 4 × 4 matrices.

C = A B

Sixteen sums of four products, 64-bit, column-major. Chained transforms accumulate one rounding per product.

  • vec.js
  • lines 90 to 98
  • PartCraft 0.8.1
export function matMul(a, b) {
  const r = new Array(16).fill(0);
  for (let c = 0; c < 4; c++) for (let rI = 0; rI < 4; rI++) {
    let s = 0;
    for (let k = 0; k < 4; k++) s += a[k * 4 + rI] * b[c * 4 + k];
    r[c * 4 + rI] = s;
  }
  return r;
}

Small linear system

64-bit

Solves A x = b for a small dense system.

Gaussian elimination with partial pivoting, then back substitution

The largest entry in the column is chosen as pivot. The system is called singular only when that pivot is below 1e-300, an absolute value not scaled to the matrix, so an ill-conditioned system returns an answer with no warning.

  • vec.js
  • lines 100 to 123
  • PartCraft 0.8.1
/** Solve A x = b for a small dense square system (Gaussian elimination, partial pivoting). Returns null when singular. */
export function solveDense(A, b) {
  const n = b.length;
  const M = A.map((row, i) => [...row, b[i]]);
  for (let c = 0; c < n; c++) {
    let p = c;
    for (let r = c + 1; r < n; r++) if (Math.abs(M[r][c]) > Math.abs(M[p][c])) p = r;
    if (Math.abs(M[p][c]) < 1e-300) return null;
    [M[c], M[p]] = [M[p], M[c]];
    const piv = M[c][c];
    for (let r = c + 1; r < n; r++) {
      const f = M[r][c] / piv;
      if (f === 0) continue;
      for (let k = c; k <= n; k++) M[r][k] -= f * M[c][k];
    }
  }
  const x = new Array(n).fill(0);
  for (let r = n - 1; r >= 0; r--) {
    let s = M[r][n];
    for (let k = r + 1; k < n; k++) s -= M[r][k] * x[k];
    x[r] = s / M[r][r];
  }
  return x;
}

Number shown with up to four decimals

64-bit

Formats a length or number for display.

text = value rounded to dp decimals (4 by default); |value| < 10⁻⁹ shown as 0

toFixed rounds the text only; the function returns a string and does not change the value it was given. A value that is not finite gives an empty string.

  • vec.js
  • lines 125 to 130
  • PartCraft 0.8.1
/** Display a length/number: up to 4 decimals, no trailing zeros. */
export function fmt(n, dp = 4) {
  if (!Number.isFinite(n)) return '';
  const a = Math.abs(n) < 1e-9 ? 0 : n;
  return Number(a.toFixed(dp)).toString();
}

In detail

Method
Rotation about an axis by the Rodrigues matrix; reflection as I − 2 n nᵀ; small dense systems by Gaussian elimination with partial pivoting.
Number types
64-bit numbers in plain arrays.
Tolerances
Normalization divides by 1 when the length is 0. A plane's frame switches its x-axis rule when |n_y| > 0.9. The linear solver reports a singular system only when a pivot is below 1e-300 (absolute). The display format treats |value| < 1e-9 as 0.
Limits
A nearly singular system is solved without a warning. A rotation about a point far from the origin forms its translation as p − R p, a difference of nearly equal numbers for small angles.

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