Writes B-spline curves and surfaces, with their trims, to a STEP file in millimeters and reads such files back.
How the precision is kept
Control points, knots and weights are written with the shortest text that reads back to the same 64-bit number, and read with the language's own parser, so they pass through unchanged. Trim loops are different: they are written as straight-segment curves through the loop's points and read back by sampling each edge and projecting the samples onto the surface, so a trim is as fine as its points.
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.
String(x), with a decimal point added when it has none
String(x) is the shortest decimal that reads back to the same 64-bit value, up to 17 significant digits. Negative zero is written as 0. A value that is not finite stops the export.
nurbs/step.js
lines 35 to 41
ModelCraft 0.8.1
export function real(x) {
if (!Number.isFinite(x)) fail('nurbs.err.step_number');
if (Object.is(x, -0)) x = 0;
let s = String(x).toUpperCase();
if (!s.includes('.')) s = s.includes('E') ? s.replace('E', '.E') : s + '.';
return s;
}
Knots and multiplicities
64-bit
Turns a full knot vector into STEP's list of distinct knots with their counts.
Uses distinctKnots with its relative 1e-12 tolerance, so two knots that differ by less are written as one value, the first of the two. On reading, each knot is repeated by its count.
nurbs/step.js
lines 55 to 60
ModelCraft 0.8.1
function knotLists(U, p, n) {
// STEP keeps distinct knots and their multiplicities (clamped: p + 1 at each end)
const ks = B.distinctKnots(U);
void p; void n;
return { mults: ks.map((k) => k.m), knots: ks.map((k) => k.u) };
}
Edges that have collapsed to a point
64-bit
Leaves a zero-length boundary edge, such as a pole, out of the face loop.
skipped when arc length ≤ 10⁻⁹ × surface size
The arc length comes from curve.js and the size from surface.js; the comparison is relative to the surface's size.
nurbs/step.js
lines 112 to 113
ModelCraft 0.8.1
const ec = S.edgeCurve(s, edge);
if (C.arcLength(ec) <= 1e-9 * size) continue; // collapsed to a point (a pole): no edge
Trim loop written as a curve
64-bit
Writes a trim loop as one closed curve on the face.
3D point = S(u, v) for each loop point; curve = straight segments through them
Each loop point is evaluated on the surface in 64-bit and the loop is written as a degree 1 B-spline. Between the points the written boundary is a chord, not the surface curve.
SI: prefix × 1000; conversion-based: value × its base unit
The SI prefixes are 1e-3, 1e-2, 0.1, 1e3 and 1e-6, each multiplied by 1000; no prefix means meters. A conversion-based unit multiplies its stated value by its base, followed to a depth of 4. The first length unit found in the file is used for every point.
nurbs/step.js
lines 248 to 257
ModelCraft 0.8.1
const unitMm = (e, depth = 0) => {
if (!e || depth > 4) return null;
const si = part(e, 'SI_UNIT');
if (si) { const pre = si.params[0] && si.params[0].enum; return (({ MILLI: 1e-3, CENTI: 1e-2, DECI: 0.1, KILO: 1e3, MICRO: 1e-6 })[pre] ?? 1) * 1000; }
const cb = part(e, 'CONVERSION_BASED_UNIT');
if (cb) { const mw = get(cb.params[1]), p = mw && (mw.params || (part(mw, 'MEASURE_WITH_UNIT') || {}).params); if (!p) return null; const v = num(p[0]), b = unitMm(get(p[1]), depth + 1); return Number.isFinite(v) && b ? v * b : null; }
return null;
};
let unit = 1;
for (const e of E.values()) if (part(e, 'LENGTH_UNIT')) { const u = unitMm(e); if (u > 0) { unit = u; break; } }
Control points in millimeters
64-bit
Reads each control point and scales it by the file's unit.
p′ = p × unit
One multiplication per coordinate; for a millimeter file the factor is 1 and the numbers are unchanged. A missing third coordinate reads as 0.
nurbs/step.js
lines 258 to 259
ModelCraft 0.8.1
const pts = (list) => list.map((r) => { const p = get(r); const c = p && p.params && p.params[1]; if (!Array.isArray(c)) fail('nurbs.err.step_point'); return [num(c[0]) * unit, num(c[1]) * unit, num(c[2] ?? 0) * unit]; });
const expand = (mults, knots) => { const U = []; mults.forEach((m, i) => { for (let k = 0; k < m; k++) U.push(num(knots[i])); }); return U; };
Trim loops read back
64-bit
Turns a face's boundary edges into trim loops in the surface's own parameters.
33 samples per curved edge; each projected to (u, v); outer when |area| < 0.999 × (u range × v range)
Edges of degree 1 keep their own points; others are sampled at 33 even parameter values. Each point is projected by surface.js closestPoint and the loop is dropped when a point lies more than 1e-4 × the surface's size away. The loop area is a shoelace sum in parameter space.
nurbs/step.js
lines 288 to 324
ModelCraft 0.8.1
const trimOf = (face, surf) => {
const size = S.size(surf);
const [ua, ub] = S.domainU(surf), [va, vb] = S.domainV(surf);
const loops = [];
for (const br of face.params[1] || []) {
const b = get(br), lp = b && get(b.params[1]);
if (!lp || lp.type !== 'EDGE_LOOP') continue;
const pts3 = [];
for (const oer of lp.params[1] || []) {
const oe = get(oer), ed = oe && get(oe.params[3]);
if (!ed || ed.type !== 'EDGE_CURVE') continue;
const ce = get(ed.params[3]), c = ce ? curveOf(ce) : null;
if (!c) continue;
const [a, z] = C.domain(c);
let seg = c.deg === 1 ? c.cvs.slice() : Array.from({ length: 33 }, (_, i) => C.point(c, a + (z - a) * i / 32));
const sameEdge = !(ed.params[4] && ed.params[4].enum === 'F'), sameOr = !(oe.params[4] && oe.params[4].enum === 'F');
if (sameEdge !== sameOr) seg = seg.reverse();
for (const q of seg) if (!pts3.length || V.dist(pts3[pts3.length - 1], q) > 1e-9 * size) pts3.push(q);
}
if (pts3.length > 1 && V.dist(pts3[0], pts3[pts3.length - 1]) < 1e-6 * size) pts3.pop();
if (pts3.length < 3) continue;
const L = [];
let uv0 = null, offSurface = false;
for (const q of pts3) { const r = S.closestPoint(surf, q, uv0 ? { uv0 } : {}); if (r.d > 1e-4 * size) { offSurface = true; break; } uv0 = [r.u, r.v]; L.push([r.u, r.v]); }
if (offSurface) continue;
const bflag = b.params[2] && b.params[2].enum === 'F';
loops.push({ L: bflag ? L.reverse() : L, outer: face.params[1].indexOf(br) === 0 || /OUTER/.test(b.type || '') });
}
if (!loops.length) return null;
let outer = null; const holes = [];
const full = (ub - ua) * (va === vb ? 0 : (vb - va));
for (const { L, outer: isOuter } of loops) {
let area = 0; for (let i = 0; i < L.length; i++) { const p = L[i], q = L[(i + 1) % L.length]; area += p[0] * q[1] - q[0] * p[1]; }
if (isOuter) { if (Math.abs(area / 2) < 0.999 * full) outer = L; } else holes.push(L);
}
try { return TR.cleanTrim({ outer, holes }, surf); } catch { return null; }
};
In detail
Method
ISO 10303-21 text with b_spline_curve_with_knots and b_spline_surface_with_knots entities, written from the published schemas the file's header names. Length unit read from the file's SI or conversion-based unit.
Number types
64-bit numbers; text in the file.
Tolerances
Knots closer than 1e-12 of the knot range are written as one knot with a multiplicity. An edge shorter than 1e-9 × the surface's size is left out of the face loop. Reading a trim: points closer than 1e-9 × size are merged, a loop closes within 1e-6 × size, and a point more than 1e-4 × size from the surface discards the loop. The file states a distance accuracy of 1.E-06.
Limits
Trim edges of degree above 1 are sampled at 33 points each when read. A loop covering 99.9 percent or more of the parameter rectangle is taken as the untrimmed boundary.
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.