Resources Numerical accuracy Calculation policy

Placing and pasting elements in an IFC model

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.

ifc-place.js BimCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-bimcraft.mjs

Placement of an item relative to its story

64-bit

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.

  • ifc-place.js
  • lines 199 to 202
  • BimCraft 0.8.1
const Wm = line.ObjectPlacement ? this.api.GetWorldTransformMatrix(this.mid, line.ObjectPlacement.value) : null;
out.push({
  id, name: val(line.Name) || '', cls: line.constructor?.name || '', storey: so.get(id) ?? null, typeId: rt.get(id)?.type ?? null,
  x: Wm ? Wm[12] * this.unitMm : 0, y: Wm ? Wm[13] * this.unitMm : 0, rot: Wm ? Math.round(Math.atan2(Wm[1], Wm[0]) / D2R * 1000) / 1000 : 0,

An item's body in the file's unit

64-bit

Writes a library item's mesh or box as IFC geometry in the file's length unit.

coordinate in file units = millimeters ÷ (millimeters per file unit)

Each coordinate is divided once by the unit factor and written without rounding. A box body becomes 8 corners from x, y, z and x + w, y + d, z + h.

  • ifc-place.js
  • lines 284 to 310
  • BimCraft 0.8.1
bodyItems(body) {
  const tri = body.type === 'box' ? boxMesh(body) : { positions: body.positions, indices: body.indices };
  const P = tri.positions.map((v) => this.u(v)), I = tri.indices;
  if (this.v4) {
    const coords = [];
    for (let i = 0; i < P.length; i += 3) coords.push([P[i], P[i + 1], P[i + 2]]);
    const list = this.x3 ? this.make('IFCCARTESIANPOINTLIST3D', coords, null) : this.make('IFCCARTESIANPOINTLIST3D', coords);
    const PI = (n) => this.T('IFCPOSITIVEINTEGER', n);
    const idx = [];
    for (let i = 0; i < I.length; i += 3) idx.push([PI(I[i] + 1), PI(I[i + 1] + 1), PI(I[i + 2] + 1)]);
    const closed = isClosed(I);
    const fs = this.x3 ? this.make('IFCTRIANGULATEDFACESET', this.H(list), null, this.T('IFCBOOLEAN', closed), idx, null)
      : this.make('IFCTRIANGULATEDFACESET', this.H(list), null, this.T('IFCBOOLEAN', closed), idx, null);
    return { items: [fs], type: 'Tessellation' };
  }
  const pts = [];
  for (let i = 0; i < P.length; i += 3) pts.push(this.point3(P[i], P[i + 1], P[i + 2]));
  const faces = [];
  for (let i = 0; i < I.length; i += 3) {
    if (I[i] === I[i + 1] || I[i + 1] === I[i + 2] || I[i] === I[i + 2]) continue;
    const loop = this.make('IFCPOLYLOOP', [this.H(pts[I[i]]), this.H(pts[I[i + 1]]), this.H(pts[I[i + 2]])]);
    const bound = this.make('IFCFACEOUTERBOUND', this.H(loop), this.T('IFCBOOLEAN', true));
    faces.push(this.H(this.make('IFCFACE', [this.H(bound)])));
  }
  if (isClosed(I)) return { items: [this.make('IFCFACETEDBREP', this.H(this.make('IFCCLOSEDSHELL', faces)))], type: 'Brep' };
  return { items: [this.make('IFCSHELLBASEDSURFACEMODEL', [this.H(this.make('IFCOPENSHELL', faces))])], type: 'SurfaceModel' };
}

Plan outline as polylines

64-bit

Turns the lines, arcs, circles, ellipses and bulged polylines of an item's plan into the straight segments written as its footprint.

arc: n = max(4, ⌈sweep ÷ 10°⌉) segments; bulge: θ = 4 atan(b), r = chord ÷ (2 sin(θ ÷ 2))

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);
}
ifc-elements.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests bin/test-bimcraft.mjs

Move and turn of pasted elements

64-bit

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.

Every policy that takes this approach