Resources Numerical accuracy Calculation policy

Pattern along a path

Places copies of a feature or body at stations along a path, optionally turned to follow it.

How the precision is kept

The path's length is integrated numerically from the kernel's curve derivative: 8-point Gauss–Legendre on 24 pieces of every smooth stretch, in 64-bit. A station's parameter is found by a guarded Newton iteration to 1e-13 of the length. The turn of each copy is carried along the path in small steps, so its accuracy depends on the step count.

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.

brep-bodies.js PartCraft 0.8.1 64-bit 5 calculations Reviewed

Tests None found by the review

Quadrature nodes and weights

64-bit

The eight points and weights used to integrate the path's speed.

Sixteen significant digits per constant, symmetric about zero. The four distinct weights as written add to 1.0000000000000001; the full rule's eight weights should add to 2.

  • brep-bodies.js
  • lines 283 to 285
  • PartCraft 0.8.1
/** Arc length along a wire (BRepAdaptor_CompCurve), by 8-point Gauss–Legendre on 24 pieces of every smooth stretch. */
const GL = [[-0.9602898564975363, 0.1012285362903763], [-0.7966664774136267, 0.2223810344533745], [-0.5255324099163290, 0.3137066458778873], [-0.1834346424956498, 0.3626837833783620],
  [0.1834346424956498, 0.3626837833783620], [0.5255324099163290, 0.3137066458778873], [0.7966664774136267, 0.2223810344533745], [0.9602898564975363, 0.1012285362903763]];

Length of the path

64-bit

The arc length of the path and the running length at each piece boundary.

piece length = h Σ wᵢ |C′(m + h xᵢ)|, h = (b − a) ÷ 2, m = (a + b) ÷ 2; L = Σ pieces

Each smooth stretch reported by the kernel is cut into 24 pieces and each piece gets one 8-point rule. The running total adds the pieces in order along the path. The accuracy is the rule's, not a tolerance that is checked.

  • brep-bodies.js
  • lines 299 to 304
  • PartCraft 0.8.1
const knots = [], cum = [0];
for (let k = 0; k < br.length - 1; k++) for (let j = 0; j < 24; j++) knots.push(br[k] + (br[k + 1] - br[k]) * j / 24);
knots.push(t1);
const seg = (a, b) => { let s = 0; const h = (b - a) / 2, m = (a + b) / 2; for (const [x, w] of GL) s += w * speed(m + h * x); return s * h; };
for (let i = 0; i < knots.length - 1; i++) cum.push(cum[i] + seg(knots[i], knots[i + 1]));
const L = cum[cum.length - 1];

Parameter at an arc length

64-bit

Finds the curve parameter that lies a given length along the path.

tₙ₊₁ = tₙ − (s(tₙ) − s) ÷ |C′(tₙ)|, kept inside the bracket by bisection

Stops when the length error is below 1e-13 × max(1, L) or after 60 iterations, whichever comes first; the last value is returned without a check that it converged. Each step integrates from the piece's start to t with a single 8-point rule.

  • brep-bodies.js
  • lines 306 to 320
  • PartCraft 0.8.1
const paramAt = (s) => {
  s = Math.max(0, Math.min(L, s));
  let i = cum.findIndex((c) => c > s) - 1;
  if (i < 0) i = knots.length - 2;
  let a = knots[i], b = knots[i + 1], lo = a, hi = b, t = a + (b - a) * ((s - cum[i]) / Math.max(1e-300, cum[i + 1] - cum[i]));
  for (let it = 0; it < 60; it++) {
    const f0 = cum[i] + seg(a, t) - s;
    if (Math.abs(f0) < 1e-13 * Math.max(1, L)) break;
    if (f0 > 0) hi = t; else lo = t;
    let tn = t - f0 / Math.max(1e-300, speed(t));
    if (!(tn > lo && tn < hi)) tn = (lo + hi) / 2;
    t = tn;
  }
  return t;
};

Path point nearest the seed

64-bit

The arc length of the path point closest to the center of what is copied.

root of (C(s) − p) · C′(s) = 0 by bisection, beside the nearest of 8 samples per piece

The path is sampled first and the root is bisected in the two intervals beside the best sample, to 1e-15 × max(1, L) or 200 steps. The sampled point itself stays a candidate, and candidates are compared by distance with a margin of 1e-15 (absolute).

  • brep-bodies.js
  • lines 322 to 336
  • PartCraft 0.8.1
const nearestS = (p) => {
  let best = 0, bd = Infinity;
  const n = (knots.length - 1) * 8;
  const sAt = (k) => (L * k) / n;
  for (let k = 0; k <= n; k++) { const q = d1(paramAt(sAt(k))).p; const d = V.len(V.sub(q, p)); if (d < bd) { bd = d; best = k; } }
  const g = (s) => { const r = d1(paramAt(s)); return V.dot(V.sub(r.p, p), r.v); };
  const pick = (lo, hi) => {
    let glo = g(lo); const ghi = g(hi);
    if (glo > 0 || ghi < 0) return null;   // no minimum inside
    for (let it = 0; it < 200 && hi - lo > 1e-15 * Math.max(1, L); it++) { const m = (lo + hi) / 2, gm = g(m); if (gm < 0) { lo = m; glo = gm; } else hi = m; }
    return (lo + hi) / 2;
  };
  const cands = [pick(sAt(Math.max(0, best - 1)), sAt(best)), pick(sAt(best), sAt(Math.min(n, best + 1))), sAt(best)].filter((x) => x !== null);
  return cands.reduce((a, b) => (V.len(V.sub(d1(paramAt(b)).p, p)) < V.len(V.sub(d1(paramAt(a)).p, p)) - 1e-15 ? b : a));
};

Turn of each copy

64-bit

Rotates each copy so it follows the path's direction without twisting.

R ← rot(t_prev → t) × R, in n = ⌈distance ÷ (L ÷ 2000)⌉ steps between stations

The rotation is a running product of many small rotations, multiplied in order; this file does not re-orthonormalize it between steps. On a closed path offsets are wrapped with a remainder by L. The step length sets how closely the carried frame follows a curved path in space.

  • brep-bodies.js
  • lines 372 to 388
  • PartCraft 0.8.1
const offs = st.map((x) => { let o = (x - s0) * dir; if (path.closed) o = ((o % pc.L) + pc.L) % pc.L; return Math.abs(o); });
let R = [[1, 0, 0], [0, 1, 0], [0, 0, 1]], tPrev = base.t, walked = 0;
for (let i = 0; i < st.length; i++) {
  if (f.orient !== 'path' || i === 0) { turns.push(R); continue; }
  const dist = offs[i] - walked;
  const n = Math.max(1, Math.ceil(dist / (pc.L / 2000)));
  for (let k = 1; k <= n; k++) {
    let s = s0 + dir * (walked + dist * k / n);
    if (path.closed) s = ((s % pc.L) + pc.L) % pc.L;
    const t = pc.at(s).t;
    R = BD.mat3Mul(BD.minimalRotation(tPrev, t), R);
    tPrev = t;
  }
  walked = offs[i];
  turns.push(R);
}
const matrixOf = (i) => BD.rigid(turns[i], base.p, V.sub(pc.at(st[i]).p, base.p));

In detail

Method
Gauss–Legendre quadrature of |C′(t)| with the 8-point nodes and weights written to 16 digits; Newton with bisection for the inverse; bisection for the nearest point; rotation-minimizing transport of the tangent.
Number types
64-bit numbers.
Tolerances
Arc length inverse stops at |error| < 1e-13 × max(1, L), at most 60 iterations. Nearest point bisects to 1e-15 × max(1, L), at most 200 steps. A path shorter than 1e-9 is refused. The turn is carried in steps of at most L ÷ 2000.
Limits
The quadrature is exact only for integrands that are polynomials of degree 15 or less on each piece; for other curves its error depends on the curve. The file's own comment says the carried turn is exact for flat paths.

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