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