Writes the mesh, placements, quantities and parameter values of a part, an assembly or a library item into an IFC4 file.
How the precision is kept
Numbers are written as text by two formatters: coordinates with at most 6 decimals, directions with 12 decimals, quantities and property values with 12 significant digits. A part's vertex coordinates are read from the kernel mesh, whose positions are a Float32Array, so the exported points carry 32-bit precision before they are rounded to 6 decimals. Volume, area and the bounding box come from the kernel's own statistics in 64-bit numbers.
Open item. Write vertex coordinates from a 64-bit mesh and write quantities and parameter values with 17 significant digits instead of 12.
Calculations under this policy
6 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.
ifc-export.jsPartCraft 0.8.1 32-bit in part6 calculationsReviewed
Tests tools/partcraft-test/test-ifc-export.mjs
Coordinate and direction text
64-bit
Formats a coordinate or a direction component as a STEP real.
text = v.toFixed(digits), trailing zeros removed; digits = 6 for points, 12 for directions
toFixed(6) keeps a millionth of a millimeter; a value under 0.5e-6 in magnitude is written as 0. A non-finite value is written as 0 without a warning.
ifc-export.js
lines 57 to 63
PartCraft 0.8.1
/** A STEP real with at most `digits` decimals (coordinates, directions): always with a decimal point. */
export function R(v, digits = 6) {
if (!Number.isFinite(v)) return '0.';
let s = (Math.abs(v) < 0.5 * 10 ** -digits ? 0 : v).toFixed(digits).replace(/(\.\d*?)0+$/, '$1');
if (!s.includes('.')) s += '.';
return s === '-0.' ? '0.' : s;
}
Quantity and property value text
64-bit
Formats a volume, area, mass, length or parameter value as a STEP real.
text = Number(v.toPrecision(12))
The value is rounded to 12 significant digits before it is written, so three to five of the digits a 64-bit number holds are not exported. A non-finite value is written as 0.
ifc-export.js
lines 64 to 70
PartCraft 0.8.1
/** A STEP real to 12 significant digits (quantities, values), exponent as E-9. */
export function Rq(v) {
if (!Number.isFinite(v)) return '0.';
let s = String(Number(v.toPrecision(12)));
if (s.includes('e')) { const [m, e] = s.split('e'); return (m.includes('.') ? m : m + '.') + 'E' + e.replace('+', ''); }
return s.includes('.') ? s : s + '.';
}
Triangulated body
32-bit in part
Writes the vertices and triangles of the built mesh as an IfcTriangulatedFaceSet.
(x, y, z) → (x, −z, y), each written with 6 decimals
For a part or an assembly the positions come from regen.mesh.positions, a Float32Array filled in kernel.js, so each coordinate has 32-bit precision before formatting. For a library item the positions are 64-bit numbers from the item's body. The axis change is a swap and a negation, which adds no rounding.
ifc-export.js
lines 139 to 149
PartCraft 0.8.1
/** An IfcTriangulatedFaceSet of a mesh ({ positions, indices }), Z up. */
function faceSet(F, mesh) {
const { w } = F, P = mesh.positions, I = mesh.indices;
const pts = [];
for (let i = 0; i < P.length; i += 3) pts.push('(' + toZ([P[i], P[i + 1], P[i + 2]]).map((v) => R(v)).join(',') + ')');
const tris = [];
for (let i = 0; i < I.length; i += 3) tris.push('(' + (I[i] + 1) + ',' + (I[i + 1] + 1) + ',' + (I[i + 2] + 1) + ')');
F.counts.triangles += I.length / 3;
const list = w.add('IFCCARTESIANPOINTLIST3D((' + pts.join(',') + '),$)');
return w.add('IFCTRIANGULATEDFACESET(' + list + ',$,.T.,(' + tris.join(',') + '),$)');
}
Volume, area, weight and overall size
64-bit
The quantity sets of an element: net volume in m³, net surface area in m², net weight in kg and the box dimensions in mm.
Volume, area and box are the kernel's 64-bit statistics. The unit changes multiply by 1e-9 and 1e-6, which are not exactly representable, instead of dividing by 1e9 and 1e6; the difference is at most one rounding in the last digit and is below the 12 digits then written.
ifc-export.js
lines 172 to 187
PartCraft 0.8.1
/** The quantities of a built part: IfcElementQuantity sets (volume, area; mass, box). */
function quantities(F, key, stats, material) {
const { w } = F, out = [];
if (!stats) return out;
const q = (type, name, v) => (Number.isFinite(v) ? w.add('IFCQUANTITY' + type + '(' + S(name) + ',$,$,' + Rq(v) + ',$)') : null);
const std = [q('VOLUME', 'NetVolume', stats.volume * 1e-9), q('AREA', 'NetSurfaceArea', stats.area * 1e-6)].filter(Boolean);
if (std.length) out.push(w.add('IFCELEMENTQUANTITY(' + S(F.seed('qto|' + key)) + ",$,'Qto_BuildingElementProxyQuantities',$,$,(" + std.join(',') + '))'));
const lo = toZ(stats.bbox.min), hi = toZ(stats.bbox.max), size = [0, 1, 2].map((k) => Math.abs(hi[k] - lo[k]));
const own = [
material ? q('WEIGHT', 'NetWeight', stats.volume * material.density * 1e-6) : null, // mm³ × g/cm³ = 1e-3 g → kg ×1e-6
q('LENGTH', 'OverallLength', size[0]), q('LENGTH', 'OverallWidth', size[1]), q('LENGTH', 'OverallDepth', size[2]),
].filter(Boolean);
if (own.length) out.push(w.add('IFCELEMENTQUANTITY(' + S(F.seed('qpc|' + key)) + ",$,'PartCraft_Quantities',$,$,(" + own.join(',') + '))'));
F.counts.psets += out.length;
return out;
}
Volume and area of a library item's body
64-bit
Volume, surface area and bounding box of a library item's mesh for its quantities.
V = |Σ a·(b × c) ÷ 6|; A = Σ |(b − a) × (c − a)| ÷ 2
64-bit sums over the triangles in order, with no compensation. The tetrahedra are measured from the origin, so a body far from the origin adds large terms of both signs. The absolute value hides an inside-out mesh.
ifc-export.js
lines 258 to 272
PartCraft 0.8.1
/** Volume, area and box of a mesh (any axes) for the quantities. */
function meshStats(mesh) {
const P = mesh.positions, I = mesh.indices;
let vol = 0, area = 0;
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
for (let i = 0; i < P.length; i += 3) for (let k = 0; k < 3; k++) { min[k] = Math.min(min[k], P[i + k]); max[k] = Math.max(max[k], P[i + k]); }
for (let t = 0; t < I.length; t += 3) {
const a = I[t] * 3, b = I[t + 1] * 3, c = I[t + 2] * 3;
const ux = P[b] - P[a], uy = P[b + 1] - P[a + 1], uz = P[b + 2] - P[a + 2], vx = P[c] - P[a], vy = P[c + 1] - P[a + 1], vz = P[c + 2] - P[a + 2];
const nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
area += Math.hypot(nx, ny, nz) / 2;
vol += (P[a] * (P[b + 1] * P[c + 2] - P[b + 2] * P[c + 1]) - P[a + 1] * (P[b] * P[c + 2] - P[b + 2] * P[c]) + P[a + 2] * (P[b] * P[c + 1] - P[b + 1] * P[c])) / 6;
}
return { volume: Math.abs(vol), area, bbox: { min, max } };
}
Placement of an assembly instance
64-bit
Turns an instance's 4 × 4 matrix into an IFC axis placement: location, local z axis and local x axis.
The matrix columns are copied without arithmetic other than the axis swap. The location is written with 6 decimals and the two directions with 12 decimals, and the directions are not re-normalized after rounding.
ifc-export.js
lines 304 to 313
PartCraft 0.8.1
/** 4×4 column-major rigid matrix (Y up) → IFC axis placement (Z up): location, Axis (local z), RefDirection (local x). */
function placementOf(F, M) {
const col = (j) => [M[j * 4], M[j * 4 + 1], M[j * 4 + 2]];
const C = (v) => toZ(v);
// the matrix in IFC axes: C · M · C⁻¹; its columns are C of M's columns taken in IFC order (x, −z, y)
const ex = C(col(0)), ey = C(col(2)).map((v) => -v), ez = C(col(1)); // images of IFC's x, y, z
void ey;
const o = C(col(3));
return F.w.add('IFCAXIS2PLACEMENT3D(' + F.w.pt(o) + ',' + F.w.dir(ez) + ',' + F.w.dir(ex) + ')');
}
In detail
Method
ISO 10303-21 text written by hand. Y-up to Z-up by (x, y, z) → (x, −z, y), a swap and a sign change. Mesh volume of a library item by signed tetrahedra from the origin.
Number types
64-bit numbers formatted with toFixed and toPrecision; part mesh positions from a Float32Array; library item bodies from plain 64-bit arrays.
Tolerances
A coordinate smaller than half a unit of the last written decimal is written as 0. The file declares a geometric precision of 1.E-05.
Limits
Twelve significant digits for quantities and parameter values is fewer than the 15 to 17 a 64-bit number holds. Six decimals of a millimeter for coordinates.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.