Builds the meshes of extruded, revolved and swept solids, swept disks such as rebar and pipes, the basic primitives, and faceted boundary shapes.
How the precision is kept
Vertices are computed in 64-bit from the profile points and the file's directions, depths and angles. Straight extrusions of straight-edged profiles are exact up to rounding. Round shapes are faceted: 36 steps per full turn for revolutions, disks and cylinders, and a fixed 12 by 24 grid for a sphere, so their surface and volume are those of the faceted body.
Calculations under this policy
10 calculations in 2 source files. 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 solid of a profile swept along a direction by a depth, with end caps and sides.
top = bottom + direction × depth
Each top vertex is its bottom vertex plus one vector, in 64-bit; the direction is a unit vector as read. The face winding follows the sign of the direction's Z component.
ifc/geo-solid.js
lines 23 to 46
BimCraft 0.8.1
/** Extrude 2D shapes (z = 0) along dir × depth. Faces point outward whatever the direction's sign. */
export function extrudeShapes(shapes, dir, depth, endShapes = null) {
const m = new Mesh();
const d = scale(dir, depth);
const up = d[2] >= 0; // shapes are counter-clockwise seen from +z
shapes.forEach((s, si) => {
if (s.open) return;
const e = endShapes && endShapes[si] && sameCounts(s, endShapes[si]) ? endShapes[si] : s;
const bottom = [s.outer.map((p) => P3(p)), ...s.holes.map((h) => h.map((p) => P3(p)))];
const top = [e.outer.map((p) => add(P3(p), d)), ...e.holes.map((h) => h.map((p) => add(P3(p), d)))];
// bottom faces down (reversed), top faces up — swapped when extruding downward
m.polygon(up ? bottom.map((r) => r.slice().reverse()) : bottom);
m.polygon(up ? top : top.map((r) => r.slice().reverse()));
for (let r = 0; r < bottom.length; r++) {
const A = bottom[r], B = top[r], n = A.length;
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
const q = [A[i], A[j], B[j], B[i]];
quad(m, up ? q : q.slice().reverse());
}
}
});
return m;
}
Side face of a sweep
64-bit
Adds a four-sided face as one flat face or as two triangles.
The face is treated as flat when the normalized normals of its two triangles have a dot product above 0.9999, which allows about 0.8 degree of twist. An edge shorter than 1e-12 turns it into a single triangle.
ifc/geo-solid.js
lines 50 to 57
BimCraft 0.8.1
/** A planar or slightly twisted quad as two triangles (one polygon when planar). */
function quad(m, q) {
const [a, b, c, d] = q;
if (len(sub(a, b)) < 1e-12 || len(sub(c, d)) < 1e-12) { m.tri(a, c, d); return; }
const n1 = cross(sub(b, a), sub(c, a)), n2 = cross(sub(c, a), sub(d, a));
if (dot(norm(n1), norm(n2)) > 0.9999) m.polygon([q]);
else { m.tri(a, b, c); m.tri(a, c, d); }
}
Revolved solid
64-bit
The solid of a profile turned about an axis by an angle.
p_k = o + R(axis, angle × k ÷ steps) · (p − o), steps = 36 per full turn
The angle is multiplied by the file's angle unit. Every ring is rotated from the original profile, not from the previous ring, so rotation errors do not build up. An angle within 1e-9 rad of a full turn is made exactly 2π and gets no end caps.
ifc/geo-solid.js
lines 68 to 106
BimCraft 0.8.1
/** IfcRevolvedAreaSolid: the profile turned about Axis (in the profile plane) by Angle. */
export function revolved(g, id) {
const shapes = profileShapes(g, g.ref(g.a(id, 'SweptArea')));
const ax = g.ref(g.a(id, 'Axis'));
const o = g.point(g.ref(g.a(ax, 'Location')));
const axis = g.dir(g.ref(g.a(ax, 'Axis')), [0, 1, 0]);
let angle = g.num(g.a(id, 'Angle'), 0) * g.angleUnit;
const full = Math.abs(angle) >= 2 * Math.PI - 1e-9;
if (full) angle = 2 * Math.PI * Math.sign(angle || 1);
const steps = arcSegments(Math.abs(angle), 1);
const rot = (k) => { const R = rotation(axis, angle * k / steps); return (p) => add(o, apply(R, ...sub(P3(p), o))); };
const m = new Mesh();
for (const sh of shapes) {
if (sh.open) continue;
const rings = [sh.outer, ...sh.holes];
// which way the profile leaves its plane: points move along sign(angle) × axis × (p − o); the profile is
// counter-clockwise about +z, so that sign fixes the winding of the sides and caps
const c = sh.outer.reduce((acc, p) => [acc[0] + p[0] / sh.outer.length, acc[1] + p[1] / sh.outer.length], [0, 0]);
const s = Math.sign(angle) * cross(axis, sub(P3(c), o))[2] < 0 ? -1 : 1;
for (const ring of rings) {
const n = ring.length;
for (let k = 0; k < steps; k++) {
const fa = rot(k), fb = rot(k + 1);
for (let i = 0; i < n; i++) {
const j = (i + 1) % n;
const q = [fa(ring[i]), fa(ring[j]), fb(ring[j]), fb(ring[i])];
quad(m, s < 0 ? q.slice().reverse() : q);
}
}
}
if (!full) {
const start = rings.map((r) => r.map(rot(0)));
const end = rings.map((r) => r.map(rot(steps)));
m.polygon(s < 0 ? start : start.map((r) => r.slice().reverse()));
m.polygon(s < 0 ? end.map((r) => r.slice().reverse()) : end);
}
}
return m.transform(g.axis2(g.ref(g.a(id, 'Position'))));
}
Frames along a path
64-bit
The tangent, normal and binormal at each point of a 3D polyline, and the miter stretch at each bend.
The normal is carried from one point to the next and made square to the new tangent each time, so its direction is the result of a chain of rotations along the path. The miter factor is capped at 4.
ifc/geo-solid.js
lines 108 to 129
BimCraft 0.8.1
/** Frames along a 3D polyline by parallel transport: [{ p, t, n, b }]. */
export function frames(path) {
const out = [];
const n = path.length;
if (n < 2) return out;
let t0 = norm(sub(path[1], path[0]));
let nrm = perp(t0);
for (let i = 0; i < n; i++) {
const tin = i > 0 ? norm(sub(path[i], path[i - 1])) : null;
const tout = i < n - 1 ? norm(sub(path[i + 1], path[i])) : null;
let t = tin && tout ? norm(add(tin, tout)) : tin || tout;
if (!t[0] && !t[1] && !t[2]) t = tout || tin;
// transport the normal from the previous tangent to this one
if (i > 0) { const prev = out[i - 1].t; nrm = transport(nrm, prev, t); }
else nrm = norm(sub(nrm, scale(t, dot(nrm, t))));
const b = cross(t, nrm);
// mitre: scale the section across the bend so the wall keeps its thickness
const mit = tin && tout ? 1 / Math.max(0.25, dot(tin, t)) : 1;
out.push({ p: path[i], t, n: nrm, b, mit, tin, tout });
}
return out;
}
Normal carried round a bend
64-bit
Turns the section's normal by the rotation that takes the previous tangent to the next.
angle = atan2(|a × b|, a · b); v' = R(a × b, angle) v, made square to b
The angle uses atan2 of the cross and dot products, which keeps its digits for both small and large turns. A cross product shorter than 1e-12 is treated as no turn.
ifc/geo-solid.js
lines 131 to 138
BimCraft 0.8.1
function transport(v, a, b) {
const ax = cross(a, b), s = len(ax);
if (s < 1e-12) return norm(sub(v, scale(b, dot(v, b))));
const ang = Math.atan2(s, dot(a, b));
const R = rotation(ax, ang);
const w = apply(R, v[0], v[1], v[2]);
return norm(sub(w, scale(b, dot(w, b))));
}
Swept disk
64-bit
A round bar or tube along a path, such as rebar, with an optional inner radius and bend radius.
section = 36-sided polygon of the radius; swept on the path's frames
The round section is a 36-sided polygon with its corners on the circle, so the tube's cross-section area is 0.9949 of the true one. Path points closer than 1e-9 are merged first.
ifc/geo-solid.js
lines 178 to 189
BimCraft 0.8.1
/** IfcSweptDiskSolid (and the polygonal variant with bends): a tube along the directrix. */
export function sweptDisk(g, id) {
let path = curvePoints(g, g.ref(g.a(id, 'Directrix')));
path = dedupe(path);
const r = g.num(g.a(id, 'Radius'), 0);
const ri = g.num(g.a(id, 'InnerRadius'), 0);
const bend = g.num(g.a(id, 'FilletRadius'), 0);
if (bend > 0 && path.length > 2) path = filletPath(path, bend);
const circle = (rad) => { const n = arcSegments(2 * Math.PI, rad); return Array.from({ length: n }, (_, i) => [rad * Math.cos(2 * Math.PI * i / n), rad * Math.sin(2 * Math.PI * i / n)]); };
const holes = ri > 0 && ri < r ? [circle(ri).reverse()] : [];
return sweepRings(path, [{ outer: circle(r), holes }]);
}
Bends of a swept path
64-bit
Rounds the corners of a 3D polyline with arcs of a given radius.
d = min(r ÷ tan(half), 0.999 × each leg); center = corner + bisector × r' ÷ sin(half)
The corner angle and the arc's sweep are each an acos of a clamped dot product. The tangent distance is cut to 0.999 of the shorter leg, which reduces the bend radius without a note. Arc points are each rotated from the arc's start, not from the previous point.
ifc/geo-solid.js
lines 197 to 217
BimCraft 0.8.1
/** Round the corners of a 3D polyline with arcs of radius r (IfcSweptDiskSolidPolygonal bends). */
export function filletPath(path, r) {
const out = [path[0]];
for (let i = 1; i + 1 < path.length; i++) {
const p = path[i], a = norm(sub(path[i - 1], p)), b = norm(sub(path[i + 1], p));
const cosT = dot(a, b);
const half = Math.acos(Math.max(-1, Math.min(1, cosT))) / 2;
if (half < 1e-6 || half > Math.PI / 2 - 1e-6) { out.push(p); continue; }
const d = Math.min(r / Math.tan(half), len(sub(path[i - 1], p)) * 0.999, len(sub(path[i + 1], p)) * 0.999);
const rr = d * Math.tan(half);
const s = add(p, scale(a, d)), e = add(p, scale(b, d));
const c = add(p, scale(norm(add(a, b)), rr / Math.sin(half)));
const u = norm(sub(s, c)), w = norm(sub(e, c));
const sweep = Math.acos(Math.max(-1, Math.min(1, dot(u, w))));
const ax = norm(cross(u, w));
const n = Math.max(2, arcSegments(sweep, rr));
for (let k = 0; k <= n; k++) { const R = rotation(ax, sweep * k / n); out.push(add(c, scale(apply(R, u[0], u[1], u[2]), rr))); }
}
out.push(path[path.length - 1]);
return out;
}
Sphere
64-bit
The mesh of an IfcSphere.
(r sin θ cos φ, r sin θ sin φ, r cos θ), θ = π i ÷ 12, φ = 2π j ÷ 24
A fixed grid of 12 by 24 facets with corners on the sphere, whatever the radius. The faceted body encloses less volume than the sphere.
ifc/geo-solid.js
lines 295 to 302
BimCraft 0.8.1
} else if (t === 'IFCSPHERE') {
const r = n('Radius'), st = 12, sl = 24;
const pt = (i, j) => { const th = Math.PI * i / st, ph = 2 * Math.PI * j / sl; return [r * Math.sin(th) * Math.cos(ph), r * Math.sin(th) * Math.sin(ph), r * Math.cos(th)]; };
for (let i = 0; i < st; i++) for (let j = 0; j < sl; j++) {
const a = pt(i, j), b = pt(i + 1, j), c = pt(i + 1, j + 1), d = pt(i, j + 1);
if (i > 0) m.tri(a, b, d);
if (i < st - 1) m.tri(b, c, d);
}
The loops are compared by the length of their Newell sums, in 64-bit. The value only ranks the loops; it is not reported.
ifc/geo-brep.js
lines 68 to 75
BimCraft 0.8.1
function ringArea(r) {
let x = 0, y = 0, z = 0;
for (let i = 0; i < r.length; i++) {
const a = r[i], b = r[(i + 1) % r.length];
x += (a[1] - b[1]) * (a[2] + b[2]); y += (a[2] - b[2]) * (a[0] + b[0]); z += (a[0] - b[0]) * (a[1] + b[1]);
}
return Math.hypot(x, y, z);
}
Repeated points of a loop
64-bit
Drops consecutive points of a face loop that coincide, and a closing point equal to the first.
Points are merged only within 1e-12 (absolute, file units), so the loop keeps the file's own coordinates; near-duplicates farther apart than that stay as very short edges.
ifc/geo-brep.js
lines 86 to 91
BimCraft 0.8.1
function dedupe(pts) {
const out = [];
for (const p of pts) { const l = out[out.length - 1]; if (!l || len(sub(l, p)) > 1e-12) out.push(p); }
if (out.length > 1 && len(sub(out[0], out[out.length - 1])) < 1e-12) out.pop();
return out;
}
In detail
Method
Extrusion by adding direction × depth; revolution by rotating each profile point about the axis; sweeps on parallel-transport frames with mitered sections at bends.
Number types
64-bit numbers.
Tolerances
A revolution is a full turn when its angle is within 1e-9 rad of 2π. A side quad is one flat face when its two triangle normals agree to 0.9999. Path points closer than 1e-9 are merged; a bend with a half angle under 1e-6 rad is left sharp. All absolute.
Limits
Section stretch at a bend is capped at 4 times. Fillet radii along a path are reduced when a leg is too short. A sphere always has 12 by 24 facets.
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.