Turns the numbers of a STEP file into millimeters and radians and puts every solid where the file's assembly structure places it.
How the precision is kept
Coordinates are parsed as 64-bit numbers and multiplied once by the file's length unit in millimeters; a file already in millimeters is multiplied by 1. Placements become 4 × 4 matrices in plain arrays, multiplied in 64-bit, and the placed vertices are stored in a Float64Array. The one rounded value, a matrix written to six decimals, is only a key for spotting a solid placed twice.
Calculations under this policy
10 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 file's length unit in millimeters and its plane-angle unit in radians.
mm = prefix × 1000 for a meter; a conversion-based unit = its value × its base unit
The SI prefixes are decimal literals (1e-3, 0.01, …) multiplied by 1000 in 64-bit; a conversion-based unit multiplies the file's own factor by its base unit, followed to a depth limit of 4. The factor is used as the file gives it: an inch written as 25.4 mm stays 25.4.
step/read.js
lines 82 to 105
BimCraft 0.8.1
const unitOf = (id, depth = 0) => {
const e = get(id);
if (!e || depth > 4) return null;
const si = part(e, 'SI_UNIT');
const pre = { EXA: 1e18, PETA: 1e15, TERA: 1e12, GIGA: 1e9, MEGA: 1e6, KILO: 1e3, HECTO: 100, DECA: 10, DECI: 0.1, CENTI: 0.01, MILLI: 1e-3, MICRO: 1e-6, NANO: 1e-9, PICO: 1e-12 };
if (si) {
const [prefix, name] = si.params;
const f = prefix && prefix.enum ? pre[prefix.enum] ?? 1 : 1;
if (name && name.enum === 'METRE') return { kind: 'length', mm: f * 1000 };
if (name && name.enum === 'RADIAN') return { kind: 'angle', rad: f };
return null;
}
const cb = part(e, 'CONVERSION_BASED_UNIT');
if (cb) {
const mwu = get(ref(cb.params[1]));
if (!mwu) return null;
const [val, base] = mwu.params || (part(mwu, 'MEASURE_WITH_UNIT') || {}).params || [];
const v = num(val), b = unitOf(ref(base), depth + 1);
if (!Number.isFinite(v) || !b) return null;
if (b.kind === 'length') return { kind: 'length', mm: v * b.mm };
if (b.kind === 'angle') return { kind: 'angle', rad: v * b.rad };
}
return null;
};
Point coordinates
64-bit
A CARTESIAN_POINT in millimeters.
(x, y, z) × unit in mm
Each coordinate is multiplied once by the length unit in 64-bit; nothing is rounded. A missing third coordinate is taken as 0, and a point with a non-finite number is refused.
step/read.js
lines 125 to 131
BimCraft 0.8.1
const point = (id) => once(id, () => {
const e = get(id);
if (!e || e.type !== 'CARTESIAN_POINT') return null;
const c = e.params[1];
if (!Array.isArray(c) || c.length < 2 || !c.every((x) => Number.isFinite(x))) return null;
return [c[0] * unitMm, c[1] * unitMm, (c[2] || 0) * unitMm];
});
Radii and cone angles
64-bit
The radius of a cylinder, cone, sphere or torus in millimeters and a cone's half angle in radians.
r × unit in mm; α × angle unit, accepted when 0 < |α| < π ÷ 2
One 64-bit multiplication per value. A cylinder, sphere or torus radius that is not greater than zero, or a cone half angle of zero or of a quarter turn or more, makes the surface unreadable instead of being adjusted; a cone's radius only has to be a finite number.
step/read.js
lines 206 to 209
BimCraft 0.8.1
case 'CYLINDRICAL_SURFACE': { const f = F(), r = num(e.params[2]) * unitMm; return f && r > 0 ? G.cylinder(f, r) : null; }
case 'CONICAL_SURFACE': { const f = F(), r = num(e.params[2]) * unitMm, a = num(e.params[3]) * angle; return f && Number.isFinite(r) && Math.abs(a) > 0 && Math.abs(a) < Math.PI / 2 ? G.cone(f, r, a) : null; }
case 'SPHERICAL_SURFACE': { const f = F(), r = num(e.params[2]) * unitMm; return f && r > 0 ? G.sphere(f, r) : null; }
case 'TOROIDAL_SURFACE': case 'DEGENERATE_TOROIDAL_SURFACE': { const f = F(), R = num(e.params[2]) * unitMm, r = num(e.params[3]) * unitMm; return f && R > 0 && r > 0 ? G.torus(f, R, r) : null; }
Knot vectors
Exact
The full knot vector of a B-spline from the file's knots and multiplicities, or the one a uniform, quasi-uniform or Bézier form implies.
Explicit knots are copied as parsed and repeated by their whole-number multiplicities. Implied knots are whole numbers (0 and 1 for Bézier, i − p for uniform, clamped whole numbers for quasi-uniform), so no rounding enters.
step/read.js
lines 154 to 165
BimCraft 0.8.1
const knotsOf = (p, n, params, kind) => {
// the explicit knots, or those a uniform / quasi-uniform / Bézier form implies
if (kind === 'with_knots') {
const [mults, knots] = params;
if (!Array.isArray(mults) || !Array.isArray(knots) || mults.length !== knots.length || !mults.every((m) => Number.isInteger(m) && m >= 0 && m <= p + 1) || !knots.every(Number.isFinite)) return null;
return G.expandKnots(knots, mults);
}
const m = n + p + 2;
if (kind === 'bezier') return [...Array(p + 1).fill(0), ...Array(m - p - 1).fill(1)];
if (kind === 'quasi') { const out = []; for (let i = 0; i < m; i++) out.push(Math.min(Math.max(i - p, 0), n - p + 1)); return out; }
return Array.from({ length: m }, (_, i) => i - p);
};
Model size and chord tolerance
64-bit
The size of the model and the distance a triangle may stray from a curved face.
size = diagonal of the box around every point; tol = max(4e-4 × size, 1e-4 mm)
The box is the minimum and maximum of every CARTESIAN_POINT, including placement origins and control points that are not on the solid, so the size can be larger than the part. With fewer than two points the size is taken as 100 mm. The tolerance is what limits how close the triangles are to curved faces.
step/read.js
lines 256 to 261
BimCraft 0.8.1
let lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], n = 0;
// every point of the file (vertices, placements, control points): a whole sphere has one vertex, its centre is a placement
for (const id of byType(new Set(['CARTESIAN_POINT']))) { const p = point(id); if (!p) continue; n++; for (let k = 0; k < 3; k++) { lo[k] = Math.min(lo[k], p[k]); hi[k] = Math.max(hi[k], p[k]); } }
size = n > 1 ? Math.hypot(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]) : 100;
if (!(size > 0)) size = 100;
tol = Math.max(size * 4e-4, 1e-4);
Closing a loop of edges
64-bit
Drops the last point of an edge loop when it repeats the first.
|first − last| ≤ 0.01 × tol
An absolute distance test at one hundredth of the chord tolerance; the first point is kept as it is and the last one is removed, so no coordinate is averaged or moved.
step/read.js
line 298
BimCraft 0.8.1
if (out.length > 1 && len(sub(out[0], out[out.length - 1])) <= tol * 1e-2) out.pop();
Placement matrices
64-bit
A placement as a 4 × 4 matrix, the product of two and the inverse of one.
M⁻¹ = [Rᵀ, −Rᵀ t]
Sixteen 64-bit numbers in a plain array. The inverse is the transpose of the rotation and −Rᵀt, which is exact only for orthonormal axes; the axes come from the placement builder, which normalizes them. The product adds four terms per entry in a fixed order.
The transform that carries a component's coordinates into the assembly that uses it.
T = M(A₂) × M(A₁)⁻¹
Two 64-bit matrix steps per link; nested assemblies multiply their links on the way down, so rounding accumulates by one matrix product per level (at most 32 levels are followed). A link with an operator the reader does not know is taken as no movement and noted.
step/read.js
lines 452 to 453
BimCraft 0.8.1
const A1 = place3(ref(idt.params[2])), A2 = place3(ref(idt.params[3]));
if (A1 && A2) T = mm(M4(A2), inv(M4(A1))); // rep_1's contents (the component) into rep_2 (the assembly)
Placed vertex positions
64-bit
Every vertex of a solid moved to its place and stored for the app.
p′ = R p + t
The positions are written to a Float64Array in millimeters, three products and a sum per coordinate. The toFixed(6) on the matrix only builds a text key to skip a solid placed twice by the same path: two placements whose 16 entries all round to the same six decimals are kept once.
step/read.js
lines 491 to 500
BimCraft 0.8.1
const k = id + ':' + T.map((v) => v.toFixed(6)).join(',');
if (seenKey.has(k)) continue;
seenKey.add(k);
const s = solidMesh(id);
if (!s || !s.idx.length) continue;
const pos = new Float64Array(s.pos.length);
for (let i = 0; i < s.pos.length; i += 3) {
const x = s.pos[i], y = s.pos[i + 1], z = s.pos[i + 2];
pos[i] = T[0] * x + T[4] * y + T[8] * z + T[12]; pos[i + 1] = T[1] * x + T[5] * y + T[9] * z + T[13]; pos[i + 2] = T[2] * x + T[6] * y + T[10] * z + T[14];
}
Faceted faces
64-bit
Triangles for a flat face given as loops of points.
The face normal is the Newell sum over the outer loop in 64-bit. The points are projected on a frame built from that normal only to decide the triangles; the triangles use the file's own 3D points, so projection rounding does not move any vertex. A loop with a zero normal gives no triangles.
step/read.js
lines 342 to 358
BimCraft 0.8.1
const flatFace = (loops) => {
if (!loops.length) return null;
const outer = loops.find((l) => l.outer) || loops.reduce((a, b) => (a.pts.length >= b.pts.length ? a : b));
const n = [0, 0, 0];
const op = outer.pts;
for (let i = 0; i < op.length; i++) { const a = op[i], b = op[(i + 1) % op.length]; n[0] += (a[1] - b[1]) * (a[2] + b[2]); n[1] += (a[2] - b[2]) * (a[0] + b[0]); n[2] += (a[0] - b[0]) * (a[1] + b[1]); }
if (!(len(n) > 0)) return null;
const F = G.frame(op[0], n, null);
const pos = [], P2 = [];
const ring = (l) => l.pts.map((p) => { pos.push(p); const d = sub(p, F.o); P2.push([dot(d, F.x), dot(d, F.y)]); return P2.length - 1; });
const o = ring(outer), holes = loops.filter((l) => l !== outer).map(ring);
const area = (r) => { let s = 0; for (let i = 0; i < r.length; i++) { const a = P2[r[i]], b = P2[r[(i + 1) % r.length]]; s += a[0] * b[1] - b[0] * a[1]; } return s; };
if (area(o) < 0) o.reverse();
for (const h of holes) if (area(h) > 0) h.reverse();
// counter-clockwise in a frame whose z is the outer loop's own normal (outward, as the file runs it): no turn needed
return { pos, tris: earcut(P2, o, holes), notes: [] };
};
In detail
Method
Units from GLOBAL_UNIT_ASSIGNED_CONTEXT (SI prefixes; conversion-based units followed to a depth limit of 4). Placement from representation relationships with transformation and mapped items: T = M(target) × M(origin)⁻¹, the inverse taken as the transpose of the rotation. The file header cites ISO 10303-42, -43 and -21.
Number types
64-bit numbers; vertex positions in Float64Array (mm); triangle and face indexes in Uint32Array.
Tolerances
Default chord tolerance = max(4e-4 × the diagonal of the box around every CARTESIAN_POINT, 1e-4 mm). A loop's last point is dropped when it is within 0.01 × that tolerance of the first. Two placements of one solid count as the same when all 16 matrix entries round to the same six decimals.
Limits
A placement's inverse assumes its axes are orthonormal, which the placement builder provides. With no unit context in the file the numbers are taken as millimeters and radians, and a note says so.
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.