Measures how fair a hull curve is (inflections, curvature jumps, roughness) and rebuilds it with fewer control points within a tolerance.
How the precision is kept
Curvature is evaluated on the NURBS curve itself through the host's cc-nurbs derivatives, in 64-bit. The measures are sampled: 240 samples by default, a finite difference for dκ/ds and Simpson's rule for the roughness integral, so the sample count sets their accuracy.
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.
If the cross product is shorter than 1e-12 a point at one third of the range is tried; if still shorter the curve is treated as straight. The 1e-12 is absolute, not scaled to the curve's size.
hull/fair.js
lines 24 to 32
PartCraft 0.8.1
/** The curve's plane normal: from its ends and middle, else (a straight line) null. */
function planeOf(N, c) {
const [a, b] = N.curve.domain(c);
const p0 = N.curve.point(c, a), p1 = N.curve.point(c, (a + b) / 2), p2 = N.curve.point(c, b);
let n = cross(sub(p1, p0), sub(p2, p0));
if (len(n) < 1e-12) { const q = N.curve.point(c, a + (b - a) / 3); n = cross(sub(q, p0), sub(p2, p0)); }
const l = len(n);
return l < 1e-12 ? null : [n[0] / l, n[1] / l, n[2] / l];
}
Signed curvature
64-bit
The curvature of a curve at a parameter, signed in its plane.
κ = (C′ × C″)·n ÷ |C′|³
First and second derivatives come from cc-nurbs. Where the speed |C′| is below 1e-14 the curvature is set to 0 instead of dividing.
hull/fair.js
lines 33 to 39
PartCraft 0.8.1
/** Signed curvature at u (in the plane with normal nrm) and the speed |C'|. */
function kappa(N, c, u, nrm) {
const D = N.curve.derivs(c, u, 2), d1 = D[1], d2 = D[2], sp = len(d1);
if (sp < 1e-14) return { k: 0, sp };
const x = cross(d1, d2);
return { k: (nrm ? dot(x, nrm) : len(x)) / (sp * sp * sp), sp };
}
Fairness measures
64-bit
Inflection count, largest curvature, roughness, curvature jumps at knots and length.
Curvature is taken at n + 1 evenly spaced parameters (n even, at least 8). dκ/ds is a difference over two steps, one step at the ends, divided by speed × du; Simpson weights 1, 4, 2, …, 1. A jump is |κ(u + ε) − κ(u − ε)| at each interior knot with ε = 1e-9 × the range.
hull/fair.js
lines 41 to 62
PartCraft 0.8.1
export function fairness(N, c, { samples = 240, band = 1e-6 } = {}) {
const [a, b] = N.curve.domain(c), nrm = planeOf(N, c), n = Math.max(8, samples - (samples % 2));
const us = [], ks = [], sps = [];
for (let i = 0; i <= n; i++) { const u = a + (b - a) * i / n, q = kappa(N, c, u, nrm); us.push(u); ks.push(q.k); sps.push(q.sp); }
let maxK = 0; for (const k of ks) maxK = Math.max(maxK, Math.abs(k));
const dead = Math.max(band, 0.01 * maxK); // a sign change counts only through 1 % of the largest curvature (the ends of a wave sit near zero)
const at = [];
let last = 0;
for (let i = 0; i <= n; i++) { const s = ks[i] > dead ? 1 : ks[i] < -dead ? -1 : 0; if (s && last && s !== last) at.push((us[i - 1] + us[i]) / 2); if (s) last = s; }
// roughness ∫ (dκ/ds)² ds by central differences on the sample grid (ds = speed · du), Simpson's rule
const du = (b - a) / n, g = new Array(n + 1);
for (let i = 0; i <= n; i++) { const i0 = Math.max(0, i - 1), i1 = Math.min(n, i + 1), ds = sps[i] * du * (i1 - i0); g[i] = ds > 1e-14 ? ((ks[i1] - ks[i0]) / ds) ** 2 * sps[i] : 0; }
let rough = g[0] + g[n]; for (let i = 1; i < n; i++) rough += (i % 2 ? 4 : 2) * g[i];
rough *= du / 3;
// curvature jumps at interior knots (a cubic is C2 at a simple knot: a jump means a multiple knot or a kink in the fit)
const jumps = [], eps = 1e-9 * (b - a);
const knots = [...new Set(c.knots.slice(c.deg + 1, c.cvs.length))].filter((u) => u > a + eps && u < b - eps);
for (const u of knots) { const l = kappa(N, c, u - eps, nrm).k, r = kappa(N, c, u + eps, nrm).k; jumps.push({ u, dk: Math.abs(r - l) }); }
let maxJump = 0; for (const j of jumps) maxJump = Math.max(maxJump, j.dk);
const length = N.curve.arcLength(c);
return { inflections: at.length, at, maxK, roughness: rough, jumps, maxJump, length, planar: !!nrm };
}
Fairing a curve
64-bit
The rebuild with the fewest control points that stays within tolerance and adds no inflection.
Control point counts are tried from degree + 1 upward. A rebuild is kept when its largest deviation is within tol and it has no more inflections than the original; lower roughness wins, or more than two fewer points at up to 5 % more roughness. The search stops four counts past the best.
hull/fair.js
lines 64 to 77
PartCraft 0.8.1
export function fairCurve(N, c, { tol = 0.1, maxCvs = 40, degree = 3 } = {}) {
const before = fairness(N, c);
let best = null;
for (let n = degree + 1; n <= Math.min(maxCvs, Math.max(degree + 1, c.cvs.length + 8)); n++) {
const r = N.fit.rebuild(c, n, degree);
if (!(r.deviation.max <= tol)) continue;
const f = fairness(N, r.curve);
if (f.inflections > before.inflections) continue;
if (!best || f.roughness < best.after.roughness * (1 - 1e-9) - 1e-15 || (best.cvs - n > 2 && f.roughness <= best.after.roughness * 1.05)) best = { curve: r.curve, cvs: n, deviation: r.deviation.max, after: f };
if (best && n - best.cvs >= 4) break; // more control points than this only follow the original's ripples
}
if (!best) return { curve: c, cvs: c.cvs.length, deviation: 0, before, after: before, fair: false };
return { ...best, before, fair: true };
}
Fairness problems
64-bit
Reports curves that have an inflection or a curvature jump.
A jump is reported when it exceeds 1e-4 × max(1, largest curvature). The value placed in the problem is rounded to 3 significant digits with toPrecision(3).
κ = (C′ × C″)·n ÷ |C′|³ in the curve's plane; inflections as sign changes of κ through a dead band; roughness R = ∫(dκ/ds)² ds by Simpson's rule. The file cites Rawson and Tupper, Basic Ship Theory, for what a fair line is. Fairing rebuilds the curve with 4 or more cubic control points.
Number types
64-bit numbers in plain arrays.
Tolerances
Dead band max(1e-6, 1 % of the largest |κ|). Zero tests at 1e-12 (plane normal) and 1e-14 (speed), absolute. Knot offset 1e-9 × the parameter range. Fairing tolerance 0.1 mm by default. A curvature jump is reported above 1e-4 × max(1, largest κ).
Limits
An inflection is placed midway between two samples. The roughness uses a central difference of sampled curvature, one-sided at the two ends.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.