Resources Numerical accuracy Calculation policy

Hull lines: fairness measures and fairing

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.

hull/fair.js PartCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/hull/test.mjs

Plane of a curve

64-bit

The normal of the plane a hull line lies in.

n = (p_mid − p₀) × (p_end − p₀), made unit

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.

R = ∫(dκ/ds)² ds ≈ (du ÷ 3) × Σ wᵢ gᵢ, gᵢ = ((κᵢ₊₁ − κᵢ₋₁) ÷ ds)² × |C′|ᵢ

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

  • hull/fair.js
  • lines 79 to 89
  • PartCraft 0.8.1
export function fairProblems(N, doc, { source = 'hull.fair', jumpTol = 1e-4 } = {}) {
  const out = [];
  for (const it of doc.items || []) {
    if (it.kind !== 'curve' || it.hidden) continue;
    const f = fairness(N, it.curve);
    const about = [{ kind: 'curve', id: it.id, label: it.name || it.id }];
    if (f.inflections) out.push({ id: source + ':inflection:' + it.id, code: 'hull.inflection', severity: 'note', msg: { key: 'hull.p.inflection', vars: { name: it.name || it.id, n: f.inflections } }, about, source: { kind: 'check', name: source }, found: { value: f.inflections, unit: '' }, fixes: [{ action: 'hull.fair', label: { key: 'hull.fix.fair' }, args: { id: it.id } }] });
    if (f.maxJump > jumpTol * Math.max(1, f.maxK)) out.push({ id: source + ':jump:' + it.id, code: 'hull.jump', severity: 'warning', msg: { key: 'hull.p.jump', vars: { name: it.name || it.id, dk: +f.maxJump.toPrecision(3) } }, about, source: { kind: 'check', name: source }, found: { value: +f.maxJump.toPrecision(3), unit: '1/mm' }, fixes: [{ action: 'hull.fair', label: { key: 'hull.fix.fair' }, args: { id: it.id } }] });
  }
  return out;
}

In detail

Method
κ = (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.

Every policy that takes this approach