Resources Numerical accuracy Calculation policy

Triangle mesh built from IFC geometry

Holds every shape read from an IFC file as triangles, moves and joins them, and hands the result to the app.

How the precision is kept

Inside the reader the positions are 64-bit numbers in plain arrays, and transforms, the volume and the welding all work on those. The form the reader hands out, however, is one 32-bit array of positions and normals. The cc-ifc API returns that array from GetGeometry (ifc/api.js), and in BimCraft ifc-util.js rebuilds an element's triangles from it for calculations, so those start from 32-bit coordinates.

Open item. Hand the positions out as 64-bit numbers as well (a Float64Array beside the 32-bit one used for drawing), so measurements never start from 32-bit coordinates.

Calculations under this policy

5 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/geo-mesh.js BimCraft 0.8.1 32-bit in part 5 calculations Reviewed

Tests tools/ifc-test/test.mjs, tools/ifc-test/nurbs.mjs

Positions and normals handed to the app

32-bit in part

Packs the mesh into the vertex and index arrays the apps read.

vertices = Float32Array of (x, y, z, nx, ny, nz) per vertex

Every 64-bit position is stored into a Float32Array, which keeps about 7 significant digits. This array is what the reader's API returns, and it is read back for more than drawing, so lengths, areas and volumes worked out from it carry 32-bit coordinates.

  • ifc/geo-mesh.js
  • lines 147 to 154
  • BimCraft 0.8.1
interleaved() {
  const n = this.vertexCount, v = new Float32Array(n * 6);
  for (let i = 0; i < n; i++) {
    v[i * 6] = this.pos[i * 3]; v[i * 6 + 1] = this.pos[i * 3 + 1]; v[i * 6 + 2] = this.pos[i * 3 + 2];
    v[i * 6 + 3] = this.nrm[i * 3]; v[i * 6 + 4] = this.nrm[i * 3 + 1]; v[i * 6 + 5] = this.nrm[i * 3 + 2];
  }
  return { vertices: v, indices: Uint32Array.from(this.idx) };
}

Planar face

64-bit

Adds one planar face with holes as triangles, all with the face's normal.

The normal is Newell's sum over the outer ring, normalized; a zero normal adds nothing. The points are stored unchanged in 64-bit; the triangulation itself is done by geo-tri.js, which this review did not cover.

  • ifc/geo-mesh.js
  • lines 36 to 49
  • BimCraft 0.8.1
/** A planar face with holes; nothing for fewer than three points. Returns the number of triangles added. */
polygon(rings) {
  const r = rings.filter((x) => x && x.length >= 3);
  if (!r.length || r[0] !== rings[0]) return 0;
  if (r.length === 1 && r[0].length === 3) { this.tri(r[0][0], r[0][1], r[0][2]); return 1; }
  const n = norm(newell(r[0]));
  if (!n[0] && !n[1] && !n[2]) return 0;
  const tris = planeTriangulate(r);
  if (!tris.length) return 0;
  const base = this.vertexCount;
  for (const ring of r) for (const p of ring) this.vertex(p, n);
  for (const k of tris) this.idx.push(base + k);
  return tris.length / 3;
}

Mesh moved by a matrix

64-bit

Adds another mesh, transformed, with its normals turned to match.

p' = M p; n' = normalize((M⁻¹)ᵀ n); a mirror swaps the winding

Positions are transformed in 64-bit. Normals use the inverse transpose and are normalized again, with a zero length replaced by 1. A negative 3 × 3 determinant reverses each triangle.

  • ifc/geo-mesh.js
  • lines 64 to 88
  • BimCraft 0.8.1
append(other, m = null) {
  const base = this.vertexCount;
  if (!m) {
    for (const v of other.pos) this.pos.push(v);
    for (const v of other.nrm) this.nrm.push(v);
    for (const k of other.idx) this.idx.push(base + k);
    return this;
  }
  const nm = normalMatrix(m), mirror = det3(m) < 0;
  const P = other.pos, N = other.nrm;
  for (let i = 0; i < P.length; i += 3) {
    const x = P[i], y = P[i + 1], z = P[i + 2];
    this.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 = N[i], ny = N[i + 1], nz = N[i + 2];
    const tx = nm[0] * nx + nm[3] * ny + nm[6] * nz, ty = nm[1] * nx + nm[4] * ny + nm[7] * nz, tz = nm[2] * nx + nm[5] * ny + nm[8] * nz;
    const l = Math.hypot(tx, ty, tz) || 1;
    this.nrm.push(tx / l, ty / l, tz / l);
  }
  const I = other.idx;
  for (let i = 0; i < I.length; i += 3) {
    if (mirror) this.idx.push(base + I[i], base + I[i + 2], base + I[i + 1]);
    else this.idx.push(base + I[i], base + I[i + 1], base + I[i + 2]);
  }
  return this;
}

Signed volume

64-bit

The volume enclosed by the mesh, positive when the triangles face outward.

V = Σ a · (b × c) ÷ 6

Each triangle adds the triple product of its corners from the origin; the sum is divided by 6 once at the end. For a mesh far from the origin the terms are large and of both signs, so the sum loses digits. The solids in this library use only its sign, to turn faces outward.

  • ifc/geo-mesh.js
  • lines 110 to 119
  • BimCraft 0.8.1
/** Signed volume (divergence theorem); meaningful for closed, outward-facing meshes. */
volume() {
  const P = this.pos, I = this.idx;
  let v = 0;
  for (let i = 0; i < I.length; i += 3) {
    const a = I[i] * 3, b = I[i + 1] * 3, c = I[i + 2] * 3;
    v += 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]);
  }
  return v / 6;
}

Shared vertices

64-bit

Merges vertices that fall in the same grid cell, for boolean operations.

key = (round(x ÷ eps), round(y ÷ eps), round(z ÷ eps))

The grid only groups the points: the position kept is the first point's own 64-bit coordinates, not the rounded cell. The cell size is absolute unless the caller scales it. Triangles that collapse to fewer than three distinct vertices are dropped.

  • ifc/geo-mesh.js
  • lines 121 to 136
  • BimCraft 0.8.1
/** Shared vertices: points closer than eps (per axis grid) become one. Degenerate triangles are dropped. */
welded(eps = 1e-7) {
  const map = new Map(), pos = [], idx = [], remap = new Int32Array(this.vertexCount);
  const P = this.pos, inv = 1 / eps;
  for (let i = 0; i < P.length / 3; i++) {
    const key = Math.round(P[i * 3] * inv) + ',' + Math.round(P[i * 3 + 1] * inv) + ',' + Math.round(P[i * 3 + 2] * inv);
    let k = map.get(key);
    if (k === undefined) { k = pos.length / 3; map.set(key, k); pos.push(P[i * 3], P[i * 3 + 1], P[i * 3 + 2]); }
    remap[i] = k;
  }
  for (let i = 0; i < this.idx.length; i += 3) {
    const a = remap[this.idx[i]], b = remap[this.idx[i + 1]], c = remap[this.idx[i + 2]];
    if (a !== b && b !== c && a !== c) idx.push(a, b, c);
  }
  return { pos, idx };
}

In detail

Method
Planar faces triangulated in their plane; normals by Newell's method; signed volume by the divergence theorem; welding on a grid.
Number types
64-bit numbers in plain arrays; a Float32Array for the interleaved output; Uint32Array for indexes.
Tolerances
Welding snaps to a grid of eps per axis (1e-7 by default; the caller may pass another).
Limits
A 32-bit number keeps about 7 significant digits, so a coordinate of 100 000 in the geometry's own frame is held to about 0.01. Two points closer than eps can still fall in different grid cells and stay separate.

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