Works out the 3D point at a parameter on each kind of curve and surface a STEP file can hold, which is where the triangles of an imported solid get their corners.
How the precision is kept
Every curve and surface is a closed-form 64-bit expression of its placement, its radii and the parameter: plain arrays of three numbers, with Float64Array work tables for the B-spline basis. Lengths arrive in millimeters and angles in radians. Decisions about closed curves, seams and poles compare distances with tolerances scaled to the size of the control points.
Calculations under this policy
14 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 origin and three unit axes of an AXIS2_PLACEMENT_3D.
z = axis ÷ |axis|; x = (ref − z (ref·z)) ÷ |…|; y = z × x
The reference direction has its part along the axis subtracted, which cancels digits when the two are nearly parallel; the result is then normalized, which magnifies the rounding left in it, so x is less square to z the closer the reference is to the axis. Below a length of 1e-12 the code swaps in a world axis instead. y is the cross product of z and x.
step/geom.js
lines 28 to 36
BimCraft 0.8.1
/** An axis2_placement_3d as { o, x, y, z } (Part 42 build_axes: x is ref_direction made square to z; defaults (1,0,0) / (0,1,0)). */
export function frame(o, axis, ref) {
const z = axis ? norm(axis) : [0, 0, 1];
let r = ref ? ref : (Math.abs(z[0] - 1) < 1e-12 && Math.abs(z[1]) < 1e-12 && Math.abs(z[2]) < 1e-12 ? [0, 1, 0] : [1, 0, 0]);
let x = sub(r, mul(z, dot(r, z)));
if (len(x) < 1e-12) { r = Math.abs(z[0]) < 0.9 ? [1, 0, 0] : [0, 1, 0]; x = sub(r, mul(z, dot(r, z))); }
x = norm(x);
return { o: [...o], x, y: cross(z, x), z };
}
Circle
64-bit
The point at an angle on a circle, and the angle of a point.
P(t) = o + r cos t · x + r sin t · y; t = atan2(l_y, l_x)
One 64-bit expression each way. The angle of a point is atan2 of its two local coordinates, moved into 0 to 2π by adding 2π when negative.
step/geom.js
lines 49 to 53
BimCraft 0.8.1
export function circle(F, r) {
return { kind: 'circle', range: [0, TAU], period: TAU, r, F,
at: (t) => world(F, r * Math.cos(t), r * Math.sin(t), 0),
project: (q) => { const l = local(F, q); let a = Math.atan2(l[1], l[0]); if (a < 0) a += TAU; return a; } };
}
Polyline
64-bit
The point at a parameter along a polyline, and the parameter nearest a point.
P(t) = (1 − f) Pᵢ + f Pᵢ₊₁, i = ⌊t⌋, f = t − i
Linear blend of two stored points in 64-bit. The nearest parameter projects the point on every segment, clamps to the segment and keeps the smallest distance; a zero-length segment is guarded.
step/geom.js
lines 70 to 75
BimCraft 0.8.1
export function polyline(pts) {
const n = pts.length;
const at = (t) => { const i = Math.max(0, Math.min(n - 2, Math.floor(t))); const f = t - i; return add(mul(pts[i], 1 - f), mul(pts[i + 1], f)); };
return { kind: 'polyline', range: [0, n - 1], period: 0, pts, at,
project: (q) => { let best = 0, bd = Infinity; for (let i = 0; i + 1 < n; i++) { const d = sub(pts[i + 1], pts[i]), l2 = dot(d, d); let f = l2 > 0 ? dot(sub(q, pts[i]), d) / l2 : 0; f = Math.max(0, Math.min(1, f)); const e = len(sub(add(pts[i], mul(d, f)), q)); if (e < bd) { bd = e; best = i + f; } } return best; } };
}
Knot span
Exact
Which knot interval a B-spline parameter falls in.
A binary search that only compares the parameter with stored knots; no arithmetic is done on the values. The halving of the index uses a bit shift on an index, not on a measured value.
step/geom.js
lines 81 to 88
BimCraft 0.8.1
/** The span index i with U[i] ≤ t < U[i+1] (the last non-empty one at the end). */
function span(U, p, n, t) {
if (t >= U[n + 1]) return n;
if (t <= U[p]) return p;
let lo = p, hi = n + 1;
while (hi - lo > 1) { const mid = (lo + hi) >> 1; if (t < U[mid]) hi = mid; else lo = mid; }
return lo;
}
B-spline basis functions
64-bit
The p + 1 basis functions that are not zero at a parameter.
N_{i,0} = 1; N_{i,j} built from (t − u) ÷ (u_b − u_a) terms in the Cox–de Boor triangular table
The work tables are Float64Array. Each denominator is a sum of two knot differences; a zero denominator gives a zero term instead of a division by zero. All terms are non-negative, so the sums do not cancel.
step/geom.js
lines 89 to 100
BimCraft 0.8.1
/** The p + 1 non-zero basis functions at t in span i (Cox–de Boor, the triangular table). */
function basis(U, p, i, t, out) {
const left = new Float64Array(p + 1), right = new Float64Array(p + 1);
out[0] = 1;
for (let j = 1; j <= p; j++) {
left[j] = t - U[i + 1 - j]; right[j] = U[i + j] - t;
let saved = 0;
for (let r = 0; r < j; r++) { const den = right[r + 1] + left[j - r]; const tmp = den !== 0 ? out[r] / den : 0; out[r] = saved + right[r + 1] * tmp; saved = left[j - r] * tmp; }
out[j] = saved;
}
return out;
}
B-spline curve point
64-bit
The point on a B-spline or rational B-spline curve at a parameter.
C(t) = Σ Nᵢ wᵢ Pᵢ ÷ Σ Nᵢ wᵢ
A sum of p + 1 weighted control points in 64-bit, divided by the summed weight for a rational curve. A parameter outside the range is clamped, or wrapped by a modulo when the curve is periodic.
step/geom.js
lines 112 to 117
BimCraft 0.8.1
if (per) t = range[0] + ((((t - range[0]) % per) + per) % per);
else t = Math.max(range[0], Math.min(range[1], t));
const i = span(U, p, n, t); basis(U, p, i, t, N);
let x = 0, y = 0, z = 0, w = 0;
for (let r = 0; r <= p; r++) { const k = i - p + r, b = N[r] * (W ? W[k] : 1); x += b * P[k][0]; y += b * P[k][1]; z += b * P[k][2]; w += b; }
return W ? [x / w, y / w, z / w] : [x, y, z];
Is a B-spline curve closed
64-bit
Decides whether a B-spline curve's two ends meet, so it can be treated as periodic.
|C(t₀) − C(t₁)| < 1e-9 × max(1, max |Pᵢ|)
The tolerance is scaled to the largest distance of a control point from the origin (in millimeters), not to the curve's own length. The file's closed flag is only used together with the same distance test on the first and last control points.
step/geom.js
lines 109 to 110
BimCraft 0.8.1
let ext = 0; for (const q of P) ext = Math.max(ext, len(q));
const per = len(sub(ends[0], ends[1])) < 1e-9 * Math.max(1, ext) || (closed && len(sub(P[0], P[n])) < 1e-9 * Math.max(1, ext)) ? range[1] - range[0] : 0;
B-spline surface point
64-bit
The point on a B-spline or rational B-spline surface at (u, v).
A double sum of (p_u + 1)(p_v + 1) terms in 64-bit; both parameters are clamped to the knot range first. The reader accepts weights only when they are finite and greater than zero, so the division is by a positive number.
step/geom.js
lines 147 to 155
BimCraft 0.8.1
u = Math.max(ru[0], Math.min(ru[1], u)); v = Math.max(rv[0], Math.min(rv[1], v));
const i = span(U, pu, nu, u), j = span(Vk, pv, nv, v);
basis(U, pu, i, u, Nu); basis(Vk, pv, j, v, Nv);
let x = 0, y = 0, z = 0, w = 0;
for (let a = 0; a <= pu; a++) for (let b = 0; b <= pv; b++) {
const ka = i - pu + a, kb = j - pv + b, f = Nu[a] * Nv[b] * (W ? W[ka][kb] : 1), q = P[ka][kb];
x += f * q[0]; y += f * q[1]; z += f * q[2]; w += f;
}
return W ? [x / w, y / w, z / w] : [x, y, z];
Seams and poles of a B-spline surface
64-bit
Decides which direction of a B-spline surface closes on itself and which boundary rows collapse to a point.
two points are the same when |a − b| < 1e-7 × max(1, |a|)
The comparison is relative to the point's distance from the origin, with a floor of 1e-7 mm. A direction is periodic when every control point of its first row meets the matching point of its last row and the row is not collapsed.
The point at (angle, height) on a cylinder, and the angle and height of a point.
S(u, v) = o + r cos u · x + r sin u · y + v · z
One 64-bit expression each way; the inverse is atan2 of the local x and y and the local z as it is.
step/geom.js
lines 233 to 237
BimCraft 0.8.1
export function cylinder(F, r) {
return { kind: 'cylinder', F, r, at: (u, v) => world(F, r * Math.cos(u), r * Math.sin(u), v),
project: (q) => { const l = local(F, q); let a = Math.atan2(l[1], l[0]); if (a < 0) a += TAU; return [a, l[2]]; },
periodU: TAU, periodV: 0, scale: [r, 1], singular: () => null, flat: false };
}
Cone
64-bit
The point at (angle, distance along the axis) on a cone, and the inverse.
r(v) = R + v tan α; v = ((ρ − R) tan α + l_z) ÷ (tan² α + 1)
The tangent of the half angle is taken once. The inverse is the closed-form foot of the point on the generating line; the divisor is hypot(tan α, 1) squared, which is at least 1. A point counts as the apex when its distance from the axis is under 1e-9 × max(1, |R|, |l_z|).
step/geom.js
lines 238 to 252
BimCraft 0.8.1
/** Cone: radius R at v = 0, growing by tan(semi) per unit of v along z (Part 42). The apex (radius 0) is a pole. */
export function cone(F, R, semi) {
const k = Math.tan(semi);
const rad = (v) => R + v * k;
return { kind: 'cone', F, at: (u, v) => { const r = rad(v); return world(F, r * Math.cos(u), r * Math.sin(u), v); },
project: (q) => {
const l = local(F, q);
let a = Math.atan2(l[1], l[0]); if (a < 0) a += TAU;
// the closest point on the generating line at angle a: along (k, 1) from (R, 0)
const rho = Math.hypot(l[0], l[1]), c = Math.hypot(k, 1);
return [a, ((rho - R) * k + l[2]) / (c * c)];
},
periodU: TAU, periodV: 0, scale: [Math.max(Math.abs(R), 1e-6), Math.hypot(k, 1)], flat: false,
singular: (q) => { const l = local(F, q); return Math.hypot(l[0], l[1]) < 1e-9 * Math.max(1, Math.abs(R), Math.abs(l[2])) ? 'u' : null; } };
}
Sphere
64-bit
The point at (longitude, latitude) on a sphere, and the inverse.
S(u, v) = o + r cos v cos u · x + r cos v sin u · y + r sin v · z
Products of sines and cosines in 64-bit. The latitude of a point is atan2(l_z, hypot(l_x, l_y)), which needs no clamp. A point within 1e-9 × r of the axis is a pole.
step/geom.js
lines 253 to 258
BimCraft 0.8.1
export function sphere(F, r) {
return { kind: 'sphere', F, r, at: (u, v) => world(F, r * Math.cos(v) * Math.cos(u), r * Math.cos(v) * Math.sin(u), r * Math.sin(v)),
project: (q) => { const l = local(F, q); let a = Math.atan2(l[1], l[0]); if (a < 0) a += TAU; return [a, Math.atan2(l[2], Math.hypot(l[0], l[1]))]; },
periodU: TAU, periodV: 0, scale: [r, r], flat: false,
singular: (q) => { const l = local(F, q); return Math.hypot(l[0], l[1]) < 1e-9 * r ? 'u' : null; } };
}
Torus
64-bit
The point at two angles on a torus, and the inverse.
c = R + r cos v; S(u, v) = o + c cos u · x + c sin u · y + r sin v · z
64-bit closed form. The inverse takes the tube angle as atan2(l_z, hypot(l_x, l_y) − R); that subtraction leaves an absolute rounding of the size of R's last digit, which weighs more on the angle the larger R is compared with the tube radius r.
step/geom.js
lines 259 to 268
BimCraft 0.8.1
export function torus(F, R, r) {
return { kind: 'torus', F, at: (u, v) => { const c = R + r * Math.cos(v); return world(F, c * Math.cos(u), c * Math.sin(u), r * Math.sin(v)); },
project: (q) => {
const l = local(F, q);
let a = Math.atan2(l[1], l[0]); if (a < 0) a += TAU;
let b = Math.atan2(l[2], Math.hypot(l[0], l[1]) - R); if (b < 0) b += TAU;
return [a, b];
},
periodU: TAU, periodV: TAU, scale: [R + r, r], singular: () => null, flat: false };
}
Surface of revolution
64-bit
The point of a profile curve turned about an axis by an angle, and the inverse.
S(u, v) = o + h z + rr cos u + (z × rr) sin u, h = (C(v) − o)·z, rr = (C(v) − o) − h z
The profile point is split into its part along the axis and its radial part by a dot product and a subtraction, then rotated. The inverse turns the point back into the profile's plane and asks the profile curve for its parameter. Its mesh scale comes from 16 samples of the profile, which only sizes triangles.
step/geom.js
lines 269 to 293
BimCraft 0.8.1
/** The swept curve turned about the axis by u; v is the curve's parameter. */
export function revolution(curve, o, axis) {
const z = norm(axis);
const at = (u, v) => {
const p = curve.at(v), d = sub(p, o), h = dot(d, z), rr = sub(d, mul(z, h));
const c = Math.cos(u), s = Math.sin(u);
return add(add(o, mul(z, h)), add(mul(rr, c), mul(cross(z, rr), s)));
};
// the curve's own first point fixes u = 0 (the meridian plane through it)
const p0 = curve.at(curve.range ? curve.range[0] : 0), d0 = sub(p0, o), r0 = sub(d0, mul(z, dot(d0, z)));
const ex = len(r0) > 1e-12 ? norm(r0) : frame(o, z).x, ey = cross(z, ex);
const span = curve.range ? curve.range[1] - curve.range[0] : 1;
let L = 0; { const m = 16; let prev = curve.at(curve.range ? curve.range[0] : 0); for (let i = 1; i <= m; i++) { const q = curve.at((curve.range ? curve.range[0] : 0) + span * i / m); L += len(sub(q, prev)); prev = q; } }
let rmax = 1e-6; { const m = 16; for (let i = 0; i <= m; i++) { const q = curve.at((curve.range ? curve.range[0] : 0) + span * i / m), d = sub(q, o); rmax = Math.max(rmax, len(sub(d, mul(z, dot(d, z))))); } }
return { kind: 'revolution', at, periodU: TAU, periodV: 0, scale: [rmax, L / (span || 1)], flat: false,
project: (q) => {
const d = sub(q, o), h = dot(d, z), rr = sub(d, mul(z, h));
let a = Math.atan2(dot(rr, ey), dot(rr, ex)); if (a < 0) a += TAU;
// turn q back into the meridian plane, then find it on the curve
const back = add(add(o, mul(z, h)), mul(ex, len(rr)));
// the curve's points sit in their own meridian (angle 0 at the curve's start): the turned-back point lies there
return [a, curve.project(back)];
},
singular: (q) => { const d = sub(q, o); return len(sub(d, mul(z, dot(d, z)))) < 1e-9 * Math.max(1, rmax) ? 'u' : null; } };
}
In detail
Method
Placements as Part 42 build_axes; conics and the angle surfaces by sine and cosine; B-splines by the Cox–de Boor triangular table over the knot span found by binary search; rational forms divide by the summed weight. The file header says it was written from the ISO 10303-42 schemas.
Number types
64-bit numbers in plain arrays; Float64Array for the basis-function tables.
Tolerances
Placement: with no reference direction given, (0,1,0) is used when the axis is within 1e-12 of (1,0,0) per component, else (1,0,0); a reference direction is replaced when its part square to the axis is shorter than 1e-12 (absolute). B-spline curve closed when its two ends are within 1e-9 × max(1, largest control point distance from the origin). Surface seam and collapsed-row tests: 1e-7 × max(1, the point's distance from the origin). Pole test for B-spline surfaces: 1e-6 × max(1, largest control point distance). Cone and sphere pole tests: 1e-9 relative. Revolution: the profile's first point counts as on the axis below 1e-12 mm (absolute).
Limits
Tolerances are scaled to distance from the origin, not to the size of the part, so a small part modeled far from the origin gets looser closure tests. A B-spline parameter outside its range is clamped (or wrapped when the curve is periodic).
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.