Works out where a placed library item or a pasted copy sits in the file: its position and direction relative to the story, its shape in the file's unit, and its plan outline.
How the precision is kept
Positions are converted from model millimeters to the file's length unit by one division and moved into the story's axes with 64-bit 4 × 4 matrices. Before the placement is written, its location is rounded to 6 decimals of the file's unit and its direction to 9 decimals. Mesh coordinates of an item's body are written unrounded. Curves in an item's plan are written as straight segments of at most 10°.
Open item. Write the placement location and direction as computed, without rounding to 6 and 9 decimals.
Calculations under this policy
7 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 location and X direction written to the file for a placed item.
(lx, ly) = W⁻¹ × (x ÷ unit, y ÷ unit, story z); direction = W⁻¹ rotation × (cos r, sin r)
The point goes through the general 4 × 4 inverse of the story's world matrix in 64-bit. The location is then rounded to 6 decimals of the file's unit and the direction to 9 decimals before they are written, so the stored placement is not the computed one.
ifc-place.js
lines 479 to 488
BimCraft 0.8.1
/** IfcLocalPlacement relative to the storey: model mm → file units → the storey's own axes. */
localPlacement(st, opts) {
const Ws = this.api.GetWorldTransformMatrix(this.mid, st.ObjectPlacement.value), Wi = inv4(Ws);
const [lx, ly] = apply4(Wi, this.u(opts.x), this.u(opts.y), Ws[14]);
const r = (opts.rot || 0) * D2R;
const dl = [Wi[0] * Math.cos(r) + Wi[4] * Math.sin(r), Wi[1] * Math.cos(r) + Wi[5] * Math.sin(r)];
const loc = this.point3(round(lx), round(ly), Number(opts.z) ? round(this.u(Number(opts.z))) : 0); // 0.6 DC.117: z above the storey
const ax = this.make('IFCAXIS2PLACEMENT3D', this.H(loc), this.H(this.dir(0, 0, 1)), this.H(this.dir(round(dl[0], 9), round(dl[1], 9), 0)));
return this.make('IFCLOCALPLACEMENT', st.ObjectPlacement, this.H(ax));
}
Position and rotation read back from a placement
64-bit
The x, y and rotation of each placed item as listed in the equipment schedule and used by copy and paste.
x = W[12] × unit; y = W[13] × unit; rot = round(atan2(W[1], W[0]) in degrees × 1000) ÷ 1000
Position is one multiplication of the world matrix's translation by the millimeters per file unit. The rotation is rounded to 0.001° before it is returned.
Points on a curve are computed from the center, radius and evenly divided angle in 64-bit, so they lie on the curve; the chords between them do not. A segment spans at most 10°. A bulge below 1e-9 in size is treated as straight.
ifc-place.js
lines 790 to 823
BimCraft 0.8.1
/** Plan entities → polylines ([[x,y]…]) for the footprint; text and points left out. */
export function planPolylines(plan) {
const out = [];
const arc = (cx, cy, r, a0, a1) => { let s = a1 - a0; while (s <= 0) s += 360; const n = Math.max(4, Math.ceil(s / 10)); return Array.from({ length: n + 1 }, (_, i) => { const a = (a0 + s * i / n) * D2R; return [cx + r * Math.cos(a), cy + r * Math.sin(a)]; }); };
for (const e of plan?.entities || []) {
if (e.type === 'line') out.push([[e.x1, e.y1], [e.x2, e.y2]]);
else if (e.type === 'circle') out.push(arc(e.x, e.y, e.r, 0, 360));
else if (e.type === 'arc') out.push(arc(e.x, e.y, e.r, e.a0, e.a1));
else if (e.type === 'ellipse') {
let s = e.t1 - e.t0; while (s <= 0) s += 360;
const len = Math.hypot(e.mx, e.my), rot = Math.atan2(e.my, e.mx), c = Math.cos(rot), sn = Math.sin(rot), n = Math.max(8, Math.ceil(s / 10));
out.push(Array.from({ length: n + 1 }, (_, i) => { const t = (e.t0 + s * i / n) * D2R, x = len * Math.cos(t), y = len * e.ratio * Math.sin(t); return [e.cx + x * c - y * sn, e.cy + x * sn + y * c]; }));
} else if (e.type === 'poly') {
const pts = [];
const n = e.pts.length;
for (let i = 0; i < n; i++) {
const a = e.pts[i]; pts.push([a[0], a[1]]);
if (i === n - 1 && !e.closed) break;
const b = e.pts[(i + 1) % n], bulge = a[2] || 0;
if (Math.abs(bulge) > 1e-9) {
const th = 4 * Math.atan(bulge), ch = Math.hypot(b[0] - a[0], b[1] - a[1]);
if (ch > 0) {
const r = ch / (2 * Math.sin(th / 2)), mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2, h = r * Math.cos(th / 2);
const cx = mx - (b[1] - a[1]) / ch * h, cy = my + (b[0] - a[0]) / ch * h, a0 = Math.atan2(a[1] - cy, a[0] - cx), steps = Math.max(2, Math.ceil(Math.abs(th) / (10 * D2R)));
for (let k = 1; k < steps; k++) { const t = a0 + th * k / steps; pts.push([cx + Math.abs(r) * Math.cos(t), cy + Math.abs(r) * Math.sin(t)]); }
}
}
}
if (e.closed) pts.push([e.pts[0][0], e.pts[0][1]]);
out.push(pts);
} else if (e.type === 'hatch') for (const l of e.loops) { const p = []; for (let i = 0; i < l.length; i += 2) p.push([l[i], l[i + 1]]); p.push([l[0], l[1]]); out.push(p); }
}
return out;
}
Viewer point to model millimeters and back
64-bit
Converts a point between the viewer's Y-up meters and the model's Z-up millimeters.
model = (x × 1000, −z × 1000, y × 1000) of C⁻¹ × v; viewer = C × (x ÷ 1000, z ÷ 1000, −y ÷ 1000)
One 4 × 4 inverse of the coordination matrix, one matrix product and a multiplication by 1000, all in 64-bit. The factor 1000 is exact.
ifc-place.js
lines 851 to 859
BimCraft 0.8.1
export function fromViewer(C, v) {
const Ci = inv4(C);
const [x, y, z] = apply4(Ci, v[0], v[1], v[2]);
return [x * 1000, -z * 1000, y * 1000];
}
export function toViewer(C, p) {
const [x, y, z] = [p[0] / 1000, p[2] / 1000, -p[1] / 1000];
return apply4(C, x, y, z);
}
The matrix that carries copied elements to where they are pasted, with a turn about a point and a change of story.
M = T(about + d) × Rz(rot) × T(−about); dz = elevation of target story − elevation of source story
The offsets are divided once by the unit factor; the matrix is filled in directly from cos and sin of the turn, and the translation is formed as t − R × about in 64-bit. Each element's new world matrix is M times its old one, then taken into the parent's axes with a 4 × 4 inverse.
ifc-elements.js
lines 185 to 188
BimCraft 0.8.1
const r = (opts.rot || 0) * D2R, c = Math.cos(r), s = Math.sin(r);
// M = T(about + d) · Rz · T(−about), column-major
const ax = about[0], ay = about[1], tx = ax + u(opts.dx || 0), ty = ay + u(opts.dy || 0);
const M = [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, tx - (c * ax - s * ay), ty - (s * ax + c * ay), dz, 1];
Placement written for a pasted element
64-bit
The location, Z axis and X axis of the new placement, from its matrix.
location = round to 6 decimals; axis = column ÷ its length, rounded to 9 decimals
The location is rounded to 6 decimals of the file's unit and each axis to 9 decimals after it is divided by its length, so the written placement differs from the computed matrix by up to those amounts. A zero-length axis is divided by 1.
ifc-elements.js
lines 249 to 254
BimCraft 0.8.1
/** IfcAxis2Placement3D of a column-major matrix (no scale): location, Z axis, X axis. */
axis(m) {
const rd = (v) => Math.round(v * 1e9) / 1e9, rl = (v) => Math.round(v * 1e6) / 1e6;
const n = (x, y, z) => { const l = Math.hypot(x, y, z) || 1; return [rd(x / l), rd(y / l), rd(z / l)]; };
return this.make('IFCAXIS2PLACEMENT3D', this.H(this.point3(rl(m[12]), rl(m[13]), rl(m[14]))), this.H(this.dir(...n(m[8], m[9], m[10]))), this.H(this.dir(...n(m[0], m[1], m[2]))));
}
In detail
Method
local = inverse(story world matrix) × point; paste: world′ = T(about + d) × Rz(rot) × T(−about) × world, then into the parent's axes by the inverse matrix.
Number types
64-bit numbers; matrices are plain arrays of 16 numbers, column-major.
Tolerances
Location rounded to 1e-6 file units, direction ratios to 1e-9, both absolute. The rotation read back from a placement is rounded to 0.001°. A bulge below 1e-9 is a straight segment.
Checked by
bin/test-bimcraft.mjs places library items and reads them back with placements() in Node.
Limits
In a file whose unit is the meter the stored location has a resolution of 0.001 mm; in a file in millimeters, 0.000001 mm. Arcs, circles and ellipses of the plan outline become polylines.
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.