Resources Numerical accuracy Calculation policy

Surfaces from curves: extrude, ruled, revolve and loft

Builds a surface by pulling a curve along a direction, spanning two curves, turning a profile about an axis or passing through a set of sections.

How the precision is kept

These constructions place control points directly in 64-bit and report themselves as exact: the surface contains the input curves up to rounding. Curves are first brought to one degree and one knot vector; in that step knot values are rounded to 12 decimals and every curve is given the first curve's knot vector.

Open item. Insert the knots of the other curves at their own 64-bit values instead of values rounded to 12 decimals.

Calculations under this policy

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

nurbs/construct.js ModelCraft 0.8.1 64-bit 7 calculations Reviewed

Tests tools/nurbs/test.mjs

Merging knot lists

64-bit

The list of distinct knots, each with its highest multiplicity, over several curves or surfaces.

key = round(u × 10¹²) ÷ 10¹²

Knot values are rounded to 12 decimals to decide which knots are the same, and the rounded value is what is returned and later inserted. A knot is therefore inserted at a value that can differ from the original by up to 5e-13 of the 0…1 range.

  • nurbs/construct.js
  • lines 39 to 46
  • ModelCraft 0.8.1
function unionKnots(list) {
  const map = new Map();
  for (const U of list) for (const k of B.distinctKnots(U)) {
    const key = Math.round(k.u * 1e12) / 1e12;
    map.set(key, Math.max(map.get(key) || 0, k.m));
  }
  return [...map.entries()].sort((a, b) => a[0] - b[0]);
}

Making curves compatible

64-bit

Brings curves to one degree and one knot vector on 0…1.

Curves are rescaled to 0…1, elevated to the highest degree and given the missing knots from the merged list. At the end every curve receives the first curve's knot vector as it is; control points are not recomputed for knots that differed from it by rounding.

  • nurbs/construct.js
  • lines 48 to 66
  • ModelCraft 0.8.1
/** Bring curves to one degree and one knot vector on [0, 1] (no curve moves). */
export function compatible(curves) {
  if (!curves.length) return [];
  const p = Math.max(...curves.map((c) => c.deg));
  let cs = curves.map((c) => C.elevate(C.reparam(c, 0, 1), p - c.deg));
  const all = unionKnots(cs.map((c) => c.knots));
  cs = cs.map((c) => {
    let out = c;
    for (const [u, m] of all) {
      if (u <= 0 || u >= 1) continue;
      const have = B.multiplicity(out.knots, u, 1e-12);
      if (m > have) out = C.insertKnot(out, u, m - have);
    }
    // snap knot values that differ only by rounding, so every curve carries the identical vector
    return out;
  });
  const K = cs[0].knots;
  return cs.map((c) => ({ ...c, knots: K.slice() }));
}

Extrude

64-bit

A surface from a curve pulled along a direction.

second row = P·w + dir·w in homogeneous coordinates

The second row of control points is the first plus the direction times the weight: one product and one addition per coordinate. The curve's knots and weights are kept.

  • nurbs/construct.js
  • lines 72 to 76
  • ModelCraft 0.8.1
export function extrude(curve, dir) {
  const H = C.hcvs(curve);
  const cvs = H.map((q) => [q, [q[0] + dir[0] * q[3], q[1] + dir[1] * q[3], q[2] + dir[2] * q[3], q[3]]]);
  return { surface: rowsToSurface(curve.deg, 1, curve.knots, [0, 0, 1, 1], cvs), report: { exact: true } };
}

Revolve

64-bit

A surface from a profile turned about an axis by an angle.

o = axis point + Z·((P − axis point)·Z); r = |P − o|; row = arc(o, X, Z × X, r, 0, angle)·w

Each profile control point is projected on the axis to get its circle's center and radius; a radius of 1e-14 or less is a pole and uses the first non-zero direction found. Each row is the rational arc from curve.arc, so a full turn closes within the arc's own gap.

  • nurbs/construct.js
  • lines 83 to 104
  • ModelCraft 0.8.1
/** A8.1: revolve a profile about an axis; the profile runs in u, the rotation in v. */
export function revolve(profile, axisPoint, axisDir, angle = 2 * Math.PI) {
  const Z = V.norm(axisDir);
  if (V.len(Z) === 0) fail('nurbs.err.axis');
  const H = C.hcvs(profile);
  let ref = null;
  const rows = profile.cvs.map((p, i) => {
    const o = V.add(axisPoint, V.mul(Z, V.dot(V.sub(p, axisPoint), Z)));
    const x = V.sub(p, o), r = V.len(x);
    if (r > 1e-14 && !ref) ref = V.norm(x);
    return { o, x: r > 1e-14 ? V.norm(x) : null, r, w: H[i][3] };
  });
  if (!ref) fail('nurbs.err.on_axis');
  let arcKnots = null;
  const cvs = rows.map((row) => {
    const X = row.x || ref, Y = V.cross(Z, X);
    const a = C.arc(row.o, X, Y, row.r, 0, angle);
    arcKnots = a.knots;
    return C.hcvs(a).map((q) => V.mul(q, row.w));
  });
  return { surface: rowsToSurface(profile.deg, 2, profile.knots, arcKnots, cvs), report: { exact: true } };
}

Loft parameters

64-bit

The v parameter of each section of a loft.

tₖ = mean over control-point columns of the chord-length parameter of section k; t₀ = 0, t(last) = 1

The mean is formed by adding each column's parameter divided by the number of columns. The first and last are then set to 0 and 1 exactly.

  • nurbs/construct.js
  • lines 112 to 124
  • ModelCraft 0.8.1
/** § 10.3: v parameters averaged over the control-point columns (chord or centripetal). */
function skinParams(H, kind) {
  const ns = H.length, t = new Array(ns).fill(0);
  if (ns < 2) return [0];
  const cols = H[0].length;
  for (let j = 0; j < cols; j++) {
    const pts = H.map((row) => row[j].slice(0, 3).map((x) => x / row[j][3]));
    const tj = F.params(pts, kind);
    for (let k = 0; k < ns; k++) t[k] += tj[k] / cols;
  }
  t[0] = 0; t[ns - 1] = 1;
  return t;
}

Loft

64-bit

A surface that passes through each section curve at its parameter.

The sections are made compatible, then each column of control points is interpolated across the sections in homogeneous coordinates with one shared matrix (fit.interpolateMany). The surface passes through each section up to the rounding of that linear solve and of the compatibility step.

  • nurbs/construct.js
  • lines 126 to 137
  • ModelCraft 0.8.1
export function loft(sections, { degree = 3, params = null, kind = 'chord', d0 = null, d1 = null } = {}) {
  if (sections.length < 2) fail('nurbs.err.sections', { min: 2 });
  const cs = compatible(sections), H = cs.map((c) => C.hcvs(c));
  const t = params ? params.slice() : skinParams(H, kind);
  for (let k = 1; k < t.length; k++) if (!(t[k] > t[k - 1])) fail('nurbs.err.sections_order');
  // one interpolation per control-point column, all with the same matrix; homogeneous values, so rational sections stay exact
  const cols = H[0].map((_, i) => H.map((row) => row[i]));
  const dd = (d) => (d ? d.map((x) => [x[0], x[1], x[2], 0]) : null);
  const r = F.interpolateMany(cols, t, degree, { d0: dd(d0), d1: dd(d1) });
  const surface = rowsToSurface(cs[0].deg, r.deg, cs[0].knots, r.knots, r.rows);
  return { surface, report: { exact: true, params: t } };
}

Sum of control nets

64-bit

The control net a + b − c used by the Gordon and Coons surfaces.

Pᵢⱼ = aᵢⱼ + bᵢⱼ − cᵢⱼ

One addition and one subtraction per coordinate on nets that were first brought to the same degrees and knots. Weights are dropped, so the three nets must be polynomial.

  • nurbs/construct.js
  • lines 331 to 334
  • ModelCraft 0.8.1
function sumNets(a, b, c) {
  // a + b − c, control point by control point (polynomial nets of one shape)
  return { ...a, cvs: a.cvs.map((row, i) => row.map((p, j) => V.sub(V.add(p, b.cvs[i][j]), c.cvs[i][j]))), w: null };
}

In detail

Method
The NURBS Book § 8.3 (extruded and ruled surfaces), A8.1 (surface of revolution), § 10.3 (skinning with parameters averaged over the control-point columns), as cited in the file's header. Lofting solves one interpolation system per column in homogeneous coordinates.
Number types
64-bit numbers in plain arrays.
Tolerances
Knots are merged at 12 decimals on curves rescaled to 0…1, and multiplicities are counted within 1e-12. A profile control point within 1e-14 (absolute) of the axis becomes a pole of the revolved surface. Loft parameters must be strictly increasing.
Limits
Making curves compatible replaces each curve's knots with the first curve's, so a knot that differed by up to about 1e-12 moves without its control points being recomputed. A revolved full turn inherits the arc's closing gap of about 2.4e-16 × radius.

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