Resources Numerical accuracy Calculation policy

Reference planes, axes and points

Works out a reference plane, axis or point from the faces, edges, planes and points it is defined by.

How the precision is kept

Every construction is closed-form 64-bit vector arithmetic on the positions and unit normals the kernel hands in: cross and dot products, one 3 × 3 solve, one quadratic. Degenerate inputs (parallel planes, three points on a line, a point on the axis) are refused with a message when a length or a product falls under a stated tolerance.

Calculations under this policy

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

refgeom.js PartCraft 0.8.1 64-bit 9 calculations Reviewed

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

Rotation about an axis

64-bit

Turns a vector about a unit axis by an angle; used for a plane at an angle to another.

v′ = v·cos a + (u × v)·sin a + u·(u · v)·(1 − cos a)

The Rodrigues formula in one 64-bit expression. For a very small angle 1 − cos a loses digits, but that term is then itself very small next to v·cos a.

  • refgeom.js
  • lines 26 to 30
  • PartCraft 0.8.1
/** v rotated about unit axis u by a radians (Rodrigues). */
export function rotate(v, u, a) {
  const c = Math.cos(a), s = Math.sin(a);
  return V.add(V.add(V.mul(v, c), V.mul(V.cross(u, v), s)), V.mul(u, V.dot(u, v) * (1 - c)));
}

Point where three planes meet

64-bit

The single point that lies on three planes.

solve [nₐ; n_b; n_c] · p = (nₐ·oₐ, n_b·o_b, n_c·o_c)

The 3 × 3 system is solved by solveDense in vec.js. The point is refused when the triple product of the three normals is under 1e-9 in size; the result of the solve is used as is, with no refinement step.

  • refgeom.js
  • lines 40 to 45
  • PartCraft 0.8.1
if (f.mode === 'planes3') {
  const a = PL(0), b = PL(1), c = f.planes?.[2] ? R.plane(f.planes[2]) : R.fail(tr('model.refgeom.pick_third_plane'));
  const p = V.solveDense([[...a.n], [...b.n], [...c.n]], [V.dot(a.n, a.o), V.dot(b.n, b.o), V.dot(c.n, c.o)]);
  const det = V.dot(a.n, V.cross(b.n, c.n));
  if (!p || Math.abs(det) < 1e-9) R.fail(tr('model.refgeom.planes3_no_point'));
  return { kind: 'point', p };

Axis where two planes meet

64-bit

The line of intersection of two planes: its direction and a point on it.

d = nₐ × n_b; solve nₐ·x = cₐ, n_b·x = c_b, d·x = d·(oₐ + o_b) ÷ 2

The direction is the normalized cross product, refused when its length is under 1e-9. For nearly parallel planes the cross product subtracts nearly equal numbers, so the direction loses digits in proportion to how small the angle is.

  • refgeom.js
  • lines 60 to 68
  • PartCraft 0.8.1
} else if (f.mode === 'planes') {
  const a = PL(0), b = PL(1);
  d = V.cross(a.n, b.n);
  if (V.len(d) < 1e-9) R.fail(tr('model.refgeom.planes_parallel'));
  d = V.norm(d);
  // the point of the line nearest the planes' origins: solve n_a·x = c_a, n_b·x = c_b, d·x = d·(o_a+o_b)/2
  const ca = V.dot(a.n, a.o), cb = V.dot(b.n, b.o), cd = V.dot(d, V.mul(V.add(a.o, b.o), 0.5));
  o = V.solveDense([[...a.n], [...b.n], [...d]], [ca, cb, cd]);
  if (!o) R.fail(tr('model.refgeom.planes_no_line'));

Offset plane and plane at an angle

64-bit

A plane parallel to another at a distance, and a plane turned about an axis by an angle in degrees.

o′ = o + n·d; n′ = rotate(n ⊥ axis, axis, angle × π ÷ 180)

The offset is one product and one addition per coordinate. The angle is converted with Math.PI ÷ 180; the starting normal is the base normal with its part along the axis removed, refused when what remains is shorter than 1e-9.

  • refgeom.js
  • lines 84 to 98
  • PartCraft 0.8.1
case 'offset': {
  const b = PL(0);
  const n = b.n, o = V.add(b.o, V.mul(n, f.d));
  return planeOf(flip(n), o, V.add(b.center || b.o, V.mul(n, f.d)));
}
case 'angle': {
  const b = PL(0);
  if (!f.axis) R.fail(tr('model.refgeom.pick_turn_axis'));
  const ax = R.dir(f.axis);
  const u = V.norm(ax.d);
  const perp = V.sub(b.n, V.mul(u, V.dot(b.n, u)));
  if (V.len(perp) < 1e-9) R.fail(tr('model.refgeom.axis_square'));
  const n0 = V.norm(perp);
  const n = rotate(n0, u, f.angle * Math.PI / 180);
  return planeOf(flip(n), ax.o, ax.o);

Plane through three points

64-bit

The plane through three points, with its center at their mean.

n = (b − a) × (c − a); center = (a + b + c) ÷ 3

The points are refused as lying on one line when |n| < 1e-9 × max(1, |b − a| × |c − a|): relative to the edge lengths when those are large, absolute when their product is under 1.

  • refgeom.js
  • lines 100 to 104
  • PartCraft 0.8.1
case '3pt': {
  const a = P(0), b = P(1), c = P(2);
  const n = V.cross(V.sub(b, a), V.sub(c, a));
  if (V.len(n) < 1e-9 * Math.max(1, V.len(V.sub(b, a)) * V.len(V.sub(c, a)))) R.fail(tr('model.refgeom.points_on_line'));
  return planeOf(flip(V.norm(n)), a, V.mul(V.add(V.add(a, b), c), 1 / 3));

Mid plane between two planes

64-bit

The plane half way between two planes, or the one that bisects them when they are not parallel.

n = (nₐ − n_b) ÷ |nₐ − n_b|; c = (oₐ·nₐ − o_b·n_b) ÷ |nₐ − n_b|; parallel and same facing: o = oₐ + n·((o_b − oₐ)·n) ÷ 2

When the two normals differ by only a little more than the 1e-9 threshold, the bisector's normal is that small difference scaled up, so the rounding of the two unit normals is magnified by 1 ÷ |nₐ − n_b|. Below the threshold the planes are treated as parallel.

  • refgeom.js
  • lines 106 to 124
  • PartCraft 0.8.1
case 'mid': {
  const a = PL(0), b = PL(1);
  const diff = V.sub(a.n, b.n);
  let n, o;
  if (V.len(diff) > 1e-9) {
    // points as far from one as from the other: (x − oa)·na = (x − ob)·nb, i.e. x·(na − nb) = oa·na − ob·nb
    const L = V.len(diff);
    n = V.mul(diff, 1 / L);
    const c = (V.dot(a.o, a.n) - V.dot(b.o, b.n)) / L;
    o = V.mul(n, c);
  } else {
    // same facing and parallel: half way between them
    n = a.n;
    o = V.add(a.o, V.mul(n, V.dot(V.sub(b.o, a.o), n) / 2));
  }
  const ca = a.center || a.o, cb = b.center || b.o;
  const mid = V.mul(V.add(ca, cb), 0.5);
  const fr = V.frameFor(n, o);
  return planeOf(flip(n), o, project(fr, mid));

Plane tangent to a cylinder

64-bit

The plane that touches a cylindrical face on the side of a chosen point.

radial = w − d·(w · d); n = radial ÷ |radial|; o = foot on the axis + n·r

The radial direction is the point's offset from the axis with its part along the axis removed; a point closer than 1e-9 to the axis is refused. The plane's origin uses the cylinder's own radius.

  • refgeom.js
  • lines 126 to 138
  • PartCraft 0.8.1
case 'tangent': {
  if (!f.face) R.fail(tr('model.refgeom.pick_cyl_face'));
  const fc = R.face(f.face);
  if (fc.type !== 'cylinder' || !fc.axis) R.fail(tr('model.refgeom.tangent_needs_cyl'));
  const p = P(0);
  const d = V.norm(fc.axis.d);
  const w = V.sub(p, fc.axis.o);
  const radial = V.sub(w, V.mul(d, V.dot(w, d)));
  if (V.len(radial) < 1e-9) R.fail(tr('model.refgeom.point_on_axis'));
  const n = V.norm(radial);
  const foot = V.add(fc.axis.o, V.mul(d, V.dot(w, d)));
  const o = V.add(foot, V.mul(n, fc.r));
  return planeOf(flip(n), o, o);

Plane through a line and a point

64-bit

The plane that contains a straight edge or axis and one more point.

n = d × (p − o)

Refused when |n| < 1e-9 × max(1, |p − o|), that is when the point lies on the line within that tolerance.

  • refgeom.js
  • lines 140 to 145
  • PartCraft 0.8.1
case 'edgept': {
  const ln = lineOf(f, R, tr('model.refgeom.pick_line_through'));
  const p = P(0);
  const n = V.cross(ln.d, V.sub(p, ln.o));
  if (V.len(n) < 1e-9 * Math.max(1, V.len(V.sub(p, ln.o)))) R.fail(tr('model.refgeom.point_on_line'));
  return planeOf(flip(V.norm(n)), ln.o, V.add(ln.o, V.mul(V.sub(p, ln.o), 0.5)));

Where a line meets a plane or a cylinder

64-bit

The point where a straight edge or axis crosses a plane, a flat face or a cylindrical face.

plane: t = ((o_s − o) · n) ÷ (d · n); cylinder: A·t² + B·t + C = 0, t = (−B ± √(B² − 4AC)) ÷ 2A

The plane case is one guarded division (|d · n| < 1e-12 is refused). The cylinder case is the textbook quadratic formula: when B² is much larger than 4AC one of the two roots subtracts nearly equal numbers. C = |w⊥|² − r² also cancels for a line starting near the surface. A discriminant below zero is refused with no tolerance, so a tangent line can be reported as missing.

  • refgeom.js
  • lines 192 to 224
  • PartCraft 0.8.1
/** Where a line (straight edge, axis) or a curved edge meets a plane, a flat face or a cylinder. */
function meet(f, R) {
  const surf = f.face ? R.face(f.face) : f.planes?.[0] ? { type: 'plane', ...R.plane(f.planes[0]) } : R.fail(tr('model.refgeom.pick_meet_surface'));
  const curved = f.edge ? R.edge(f.edge) : null;
  if (curved && curved.type !== 'line') {
    if (surf.type !== 'plane') R.fail(tr('model.refgeom.curved_curved'));
    if (!curved.cross) R.fail(tr('model.refgeom.curved_cross_exact'));
    const hits = curved.cross({ o: surf.o, n: surf.n });
    if (!hits.length) R.fail(tr('model.refgeom.edge_misses_plane'));
    return hits[f.flip && hits.length > 1 ? 1 : 0];
  }
  const ln = lineOf(f, R, tr('model.refgeom.pick_meet_line'));
  if (surf.type === 'plane') {
    const dn = V.dot(ln.d, surf.n);
    if (Math.abs(dn) < 1e-12) R.fail(tr('model.refgeom.line_parallel_plane'));
    return V.add(ln.o, V.mul(ln.d, V.dot(V.sub(surf.o, ln.o), surf.n) / dn));
  }
  if (surf.type === 'cylinder' && surf.axis) {
    // |w − (w·a)a|² = r², w = o + t d − c: a quadratic in t
    const a = V.norm(surf.axis.d), w0 = V.sub(ln.o, surf.axis.o);
    const dp = V.sub(ln.d, V.mul(a, V.dot(ln.d, a))), wp = V.sub(w0, V.mul(a, V.dot(w0, a)));
    const A = V.dot(dp, dp), B = 2 * V.dot(dp, wp), C = V.dot(wp, wp) - surf.r * surf.r;
    if (A < 1e-18) R.fail(tr('model.refgeom.line_along_axis'));
    const disc = B * B - 4 * A * C;
    if (disc < 0) R.fail(tr('model.refgeom.line_misses_cyl'));
    const q = Math.sqrt(disc);
    // the crossing nearer the line's own point first (an edge: its middle)
    const t0 = ln.a ? V.dot(V.sub(V.mul(V.add(ln.a, ln.b), 0.5), ln.o), ln.d) : 0;
    const ts = [(-B - q) / (2 * A), (-B + q) / (2 * A)].sort((x, y) => Math.abs(x - t0) - Math.abs(y - t0));
    return V.add(ln.o, V.mul(ln.d, ts[f.flip ? 1 : 0]));
  }
  return R.fail(tr('model.refgeom.meet_other_curved'));
}

In detail

Method
Rodrigues rotation; 3 × 3 linear solve by Gaussian elimination with partial pivoting (solveDense in vec.js); line and plane by one division; line and cylinder by the quadratic formula.
Number types
64-bit numbers in plain arrays of three.
Tolerances
Zero-length vector: 1e-9 (absolute, model units). Parallel planes: |nₐ × n_b| < 1e-9. Three planes: |nₐ · (n_b × n_c)| < 1e-9. Three points on a line: |cross| < 1e-9 × max(1, |b − a| × |c − a|). Point on a line: |cross| < 1e-9 × max(1, |p − o|). Line parallel to a plane: |d · n| < 1e-12. Line along a cylinder axis: A < 1e-18. Midnormal direction: length < 1e-12.
Limits
The tolerances on lengths are absolute, so they do not scale with the size of the model. A line that just touches a cylinder is refused when rounding makes the discriminant negative.

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