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