Resources Numerical accuracy Calculation policy

Measuring on the IFC view's mesh

Distances, face areas, room areas and volumes, and how far an element moved between two versions of a model, all measured on the triangle mesh the IFC view draws.

How the precision is kept

The view's mesh is built once per model: each vertex is placed with 64-bit arithmetic and then stored in a Float32Array, which is the only copy of the positions the view keeps. Measure, the spaces schedule (when the file states no area or volume), the model comparison and the copy box read those 32-bit positions back and do their own arithmetic in 64-bit. A 32-bit number has a 24-bit significand, so a coordinate between 64 m and 128 m from the scene origin is held in steps of about 0.008 mm. The model is opened with the engine's COORDINATE_TO_ORIGIN setting.

Open item. Keep the vertex positions in a Float64Array for measuring, the spaces schedule and the comparison, and convert to 32-bit only for the buffers that are drawn.

Calculations under this policy

10 calculations in 5 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-model.js BimCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests bin/test-bimcraft.mjs

Placing each vertex of an element

32-bit in part

Moves an element's vertices and normals from its own axes into the scene with its placement matrix.

p′ = M × p; n′ = (rotation part of M) × n ÷ |…|

The vertices arrive from the engine as a 32-bit array. Each one is multiplied by the column-major 4 × 4 placement in 64-bit and pushed to a plain array, which finishBatch then stores as 32-bit again. The normal is divided by its length, with a zero length replaced by 1.

  • ifc-model.js
  • lines 238 to 261
  • BimCraft 0.8.1
function appendGeometry(b, verts, idx, m, id) {
  // verts: x,y,z,nx,ny,nz per vertex; m: column-major 4×4 placement. `id` is the element's key.
  const base = b.vcount;
  const r = b.runs, last = r.ids.length - 1;
  if (last >= 0 && r.ids[last] === id) { r.tn[last] += idx.length / 3; r.vn[last] += verts.length / 6; }
  else { r.ids.push(id); r.t0.push(b.idx.length / 3); r.tn.push(idx.length / 3); r.v0.push(base); r.vn.push(verts.length / 6); }
  for (let i = 0; i < verts.length; i += 6) {
    const x = verts[i], y = verts[i + 1], z = verts[i + 2];
    b.pos.push(
      m[0] * x + m[4] * y + m[8] * z + m[12],
      m[1] * x + m[5] * y + m[9] * z + m[13],
      m[2] * x + m[6] * y + m[10] * z + m[14],
    );
    const nx = verts[i + 3], ny = verts[i + 4], nz = verts[i + 5];
    let tx = m[0] * nx + m[4] * ny + m[8] * nz;
    let ty = m[1] * nx + m[5] * ny + m[9] * nz;
    let tz = m[2] * nx + m[6] * ny + m[10] * nz;
    const l = Math.hypot(tx, ty, tz) || 1;
    b.nrm.push(tx / l, ty / l, tz / l);
  }
  for (let i = 0; i < idx.length; i++) b.idx.push(base + idx[i]);
  for (let i = 0; i < idx.length; i += 3) b.ids.push(id);
  b.vcount += verts.length / 6;
}

Storing the mesh

32-bit in part

Turns the collected positions, normals, indices and element runs of a batch into typed arrays.

Float32Array.from rounds every coordinate to the nearest 32-bit number. This is the only copy of the positions the view keeps, so everything measured on the view reads these values. Indices, element keys and run counts are 32-bit unsigned integers.

  • ifc-model.js
  • lines 263 to 275
  • BimCraft 0.8.1
function finishBatch(b) {
  return {
    storey: b.storey,
    space: b.space,          // IfcSpace volumes: hidden by default in the viewer
    color: b.color,
    positions: Float32Array.from(b.pos),
    normals: Float32Array.from(b.nrm),
    indices: Uint32Array.from(b.idx),
    triIds: Uint32Array.from(b.ids),   // element key per triangle (faceIndex → element)
    // one run per element: its triangles [t0, t0 + tn) and vertices [v0, v0 + vn) in this batch
    elems: { ids: Uint32Array.from(b.runs.ids), t0: Uint32Array.from(b.runs.t0), tn: Uint32Array.from(b.runs.tn), v0: Uint32Array.from(b.runs.v0), vn: Uint32Array.from(b.runs.vn) },
  };
}
measure.js BimCraft 0.8.1 32-bit in part 3 calculations Reviewed

Tests bin/test-bimcraft.mjs

Area of a picked flat face

32-bit in part

The flat face a picked triangle belongs to and its area.

area = Σ |(b − a) × (c − a)| ÷ 2 over the triangles in the picked plane

Triangle corners are read from the 32-bit positions; each area is half the length of a 64-bit cross product and the areas are added in the order the flood fill reaches them. A neighbor joins when its normal is within 1° and its corners are within 1e-4 × span + 1e-6 m of the picked plane, where span is the distance between the first corners of the element's first and last triangles plus 1. Corners are matched after rounding to 0.0001 m.

  • measure.js
  • lines 31 to 67
  • BimCraft 0.8.1
export function faceRegion(batch, r, seedTri, tolDeg = 1) {
  const P = batch.positions, I = batch.indices, E = batch.elems;
  const t0 = E.t0[r], tEnd = t0 + E.tn[r];
  const v = (i) => [P[i * 3], P[i * 3 + 1], P[i * 3 + 2]];
  const tri = (t) => [v(I[t * 3]), v(I[t * 3 + 1]), v(I[t * 3 + 2])];
  const nrm = (t) => { const [a, b, c] = tri(t); const n = cross(sub(b, a), sub(c, a)); const l = len(n); return { n: l ? [n[0] / l, n[1] / l, n[2] / l] : [0, 0, 0], area: l / 2 }; };
  const seed = nrm(seedTri);
  if (seedTri < t0 || seedTri >= tEnd || !seed.area) return { area: 0, tris: [], normal: seed.n };
  const cos = Math.cos(tolDeg * Math.PI / 180), d0 = dot(seed.n, tri(seedTri)[0]);
  const span = len(sub(v(I[t0 * 3]), v(I[(tEnd - 1) * 3]))) + 1;
  const flat = (t) => { const x = nrm(t); return x.area > 0 && dot(x.n, seed.n) >= cos && tri(t).every((p) => Math.abs(dot(seed.n, p) - d0) <= 1e-4 * span + 1e-6); };
  // edges by quantised position: the element's triangles that touch each edge
  const q = (i) => Math.round(P[i * 3] * 1e4) + ',' + Math.round(P[i * 3 + 1] * 1e4) + ',' + Math.round(P[i * 3 + 2] * 1e4);
  const byEdge = new Map();
  for (let t = t0; t < tEnd; t++) {
    const k = [q(I[t * 3]), q(I[t * 3 + 1]), q(I[t * 3 + 2])];
    for (let j = 0; j < 3; j++) {
      const a = k[j], b = k[(j + 1) % 3];
      if (a === b) continue;
      const key = a < b ? a + '|' + b : b + '|' + a;
      if (!byEdge.has(key)) byEdge.set(key, []);
      byEdge.get(key).push(t);
    }
  }
  const out = new Set([seedTri]), todo = [seedTri];
  let area = 0;
  while (todo.length) {
    const t = todo.pop();
    area += nrm(t).area;
    const k = [q(I[t * 3]), q(I[t * 3 + 1]), q(I[t * 3 + 2])];
    for (let j = 0; j < 3; j++) {
      const a = k[j], b = k[(j + 1) % 3];
      for (const u of byEdge.get(a < b ? a + '|' + b : b + '|' + a) || []) if (!out.has(u) && flat(u)) { out.add(u); todo.push(u); }
    }
  }
  return { area, tris: [...out], normal: seed.n };
}

Distance between two picked points

32-bit in part

The straight-line distance Measure reports.

d = √(Δx² + Δy² + Δz²)

One 64-bit Math.hypot of the three coordinate differences. The two points are handed over by the viewer, which picks them on the 32-bit mesh, so the distance carries that resolution.

  • measure.js
  • line 95
  • BimCraft 0.8.1
export const distance = (a, b) => len(sub(b, a));

Distance split along the IFC axes

32-bit in part

The measured difference as ΔX, ΔY and ΔZ in the file's Z-up axes.

ΔX = Δx; ΔY = −Δz; ΔZ = Δy (the viewer is Y up)

Three 64-bit subtractions and a sign change on the same two picked points, so each part has the resolution of the 32-bit mesh.

  • measure.js
  • line 120
  • BimCraft 0.8.1
export const ifcDelta = (a, b) => [b[0] - a[0], -(b[2] - a[2]), b[1] - a[1]];
ifc-qto.js BimCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests bin/test-bimcraft.mjs

Room floor area and volume from its shape

32-bit in part

The floor area and volume of a space measured from its triangles, used when the file states neither.

area = Σ −n_y ÷ 2 over triangles with n_y ÷ |n| < −0.5; volume = |Σ a · (b × c) ÷ 6|

Reads the 32-bit vertex positions and sums in 64-bit. Each tetrahedron is taken from the scene origin, not from a point on the room, so its terms grow with the room's distance from the origin and cancel in the sum. The floor area counts every triangle whose normal is within 60° of straight down, projected on the horizontal plane.

  • ifc-qto.js
  • lines 75 to 97
  • BimCraft 0.8.1
/** Floor area (downward faces, projected) and volume (signed tetrahedra) of each key's triangles, viewer metres. */
function measureShapes(batches, want) {
  const out = new Map();
  for (const b of batches) {
    const E = b.elems, P = b.positions, I = b.indices;
    for (let r = 0; r < E.ids.length; r++) {
      const k = E.ids[r];
      if (!want.has(k)) continue;
      let m = out.get(k);
      if (!m) out.set(k, (m = { area: 0, volume: 0 }));
      for (let t = E.t0[r]; t < E.t0[r] + E.tn[r]; t++) {
        const a = I[t * 3] * 3, c1 = I[t * 3 + 1] * 3, c2 = I[t * 3 + 2] * 3;
        const ux = P[c1] - P[a], uy = P[c1 + 1] - P[a + 1], uz = P[c1 + 2] - P[a + 2];
        const vx = P[c2] - P[a], vy = P[c2 + 1] - P[a + 1], vz = P[c2 + 2] - P[a + 2];
        const ny = uz * vx - ux * vz, len = Math.hypot(uy * vz - uz * vy, ny, ux * vy - uy * vx);
        if (len > 0 && ny / len < -0.5) m.area += -ny / 2;   // facing down: its area seen from below (viewer Y is up)
        m.volume += (P[a] * (P[c1 + 1] * P[c2 + 2] - P[c1 + 2] * P[c2 + 1]) - P[a + 1] * (P[c1] * P[c2 + 2] - P[c1 + 2] * P[c2]) + P[a + 2] * (P[c1] * P[c2 + 1] - P[c1 + 1] * P[c2])) / 6;
      }
    }
  }
  for (const m of out.values()) m.volume = Math.abs(m.volume);
  return out;
}

Room area and volume in the spaces schedule

32-bit in part

Chooses the file's stated value or the measured one for each room, and rounds the measured one.

measured value kept as Number(value.toFixed(4))

A value stated in the file's quantity set is used as read. A measured value is rounded to 4 decimals before it enters the schedule and its totals; the code's own comment gives the reason, that the vertices are 32-bit.

  • ifc-qto.js
  • lines 110 to 112
  • BimCraft 0.8.1
// measured from float32 vertices: kept to 0.0001 m² / m³ (no more digits are real)
area: a !== undefined ? a : sh ? Number(sh.area.toFixed(4)) : '', areaFrom: a !== undefined ? 'file' : sh ? 'shape' : '',
volume: v !== undefined ? v : sh ? Number(sh.volume.toFixed(4)) : '', volumeFrom: v !== undefined ? 'file' : sh ? 'shape' : '' };
ifc-compare.js BimCraft 0.8.1 32-bit in part 2 calculations Reviewed

Tests bin/test-bimcraft.mjs

Bounding box of each element in a shared frame

32-bit in part

The box and triangle count of every element of a model, in coordinates both versions share.

Reads the 32-bit positions, applies the optional 4 × 4 matrix in 64-bit and keeps the minimum and maximum on each axis. Triangle counts are whole numbers.

  • ifc-compare.js
  • lines 19 to 37
  • BimCraft 0.8.1
function boxes(model, matrix) {
  const M = matrix, tris = new Map(), box = new Map();
  for (const b of model.batches) {
    const E = b.elems, P = b.positions;
    for (let r = 0; r < E.ids.length; r++) {
      const k = E.ids[r];
      tris.set(k, (tris.get(k) || 0) + E.tn[r]);
      let bb = box.get(k);
      if (!bb) box.set(k, (bb = [Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]));
      for (let v = E.v0[r] * 3, end = (E.v0[r] + E.vn[r]) * 3; v < end; v += 3) {
        let x = P[v], y = P[v + 1], z = P[v + 2];
        if (M) { const X = M[0] * x + M[4] * y + M[8] * z + M[12], Y = M[1] * x + M[5] * y + M[9] * z + M[13], Z = M[2] * x + M[6] * y + M[10] * z + M[14]; x = X; y = Y; z = Z; }
        if (x < bb[0]) bb[0] = x; if (y < bb[1]) bb[1] = y; if (z < bb[2]) bb[2] = z;
        if (x > bb[3]) bb[3] = x; if (y > bb[4]) bb[4] = y; if (z > bb[5]) bb[5] = z;
      }
    }
  }
  return { tris, box };
}

How far an element moved, and whether its shape changed

32-bit in part

The move of an element between two versions and a yes or no on a change of size or triangle count.

moved = |center of box B − center of box A|, reported when > tolerance; reshaped when a box side differs by > tolerance

The box centers are differenced per axis before Math.hypot, in 64-bit, on boxes built from 32-bit vertices. The 0.001 m default tolerance is absolute and is not scaled to the model's size. The CSV writes the move rounded to 4 decimals.

  • ifc-compare.js
  • lines 66 to 72
  • BimCraft 0.8.1
const p = sa.box.get(ka), q = sb.box.get(kb);
let moved = 0, reshaped = false;
if (p && q) {
  const d = Math.hypot((p[0] + p[3] - q[0] - q[3]) / 2, (p[1] + p[4] - q[1] - q[4]) / 2, (p[2] + p[5] - q[2] - q[5]) / 2);
  if (d > tolerance) moved = d;
  reshaped = [0, 1, 2].some((i) => Math.abs((p[i + 3] - p[i]) - (q[i + 3] - q[i])) > tolerance) || sa.tris.get(ka) !== sb.tris.get(kb);
}
ifc-vis.js BimCraft 0.8.1 32-bit in part 1 calculation Reviewed

Tests bin/test-bimcraft.mjs

Bounding box of chosen elements

32-bit in part

The smallest box round the vertices of the chosen elements.

Minimum and maximum of the 32-bit vertex coordinates, with no arithmetic. Copy uses this box for the clip's anchor and size when the frame computed from the file's own geometry is not available.

  • ifc-vis.js
  • lines 160 to 176
  • BimCraft 0.8.1
/** Bounding box of the elements' vertices over every batch (viewer space), or null when none has geometry. */
export function boundsOf(batches, keys) {
  const want = keys instanceof Set ? keys : new Set(keys);
  const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
  let any = false;
  for (const b of batches) {
    const E = b.elems, P = b.positions;
    for (let r = 0; r < E.ids.length; r++) {
      if (!want.has(E.ids[r])) continue;
      for (let v = E.v0[r], vEnd = E.v0[r] + E.vn[r]; v < vEnd; v++) {
        for (let k = 0; k < 3; k++) { const x = P[v * 3 + k]; if (x < min[k]) min[k] = x; if (x > max[k]) max[k] = x; }
      }
      any = true;
    }
  }
  return any ? { min, max } : null;
}

In detail

Method
Distance: straight line between two picked points. Face: the picked triangle and its flat neighbors, areas summed. Room: floor area from downward-facing triangles projected on the horizontal plane, volume from signed tetrahedra. Comparison: the move of the middle of each element's bounding box.
Number types
Vertex positions and normals in Float32Array; triangle indices and element keys in Uint32Array; the arithmetic on them in 64-bit numbers.
Tolerances
Face: normals within 1° of the picked triangle and every corner within 1e-4 × span + 1e-6 m of its plane; corners matched after rounding to 0.0001 m. Room floor: triangles whose unit normal has a vertical part below −0.5. Comparison: 0.001 m by default, absolute, for a move and for a change of box size.
Checked by
bin/test-bimcraft.mjs runs faceRegion (--measure), spaceSchedule (--qto) and compareModels (--compare) in Node.
Limits
Results carry the mesh's 32-bit resolution and its faceting of curved surfaces. Areas and volumes measured from a room's shape are kept to 0.0001 m² or m³.

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.

Every policy that takes this approach