Resources Numerical accuracy Calculation policy

Section volumes in the 3D view

Works out the planes, corners and edges of the free-form section volumes that cut the 3D view.

How the precision is kept

A section volume decides what the 3D view shows; it changes no model value. Planes and corners are computed in 64-bit in meters from the model's minimum corner and then handed to the viewer's clipping planes.

Calculations under this policy

3 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.

section-volume.js BimCraft 0.8.1 32-bit, display only 3 calculations Reviewed

Tests bin/test-bimcraft.mjs

Planes of a section cell

32-bit, display only

The outward normal and a point of each plane that bounds a section cell, with tilted faces and extra planes.

n = unit(n₀ + tan a × e_u + tan b × e_v); extra: n = (cos el cos az, cos el sin az, sin el)

64-bit trigonometry on angles in degrees times π ÷ 180. The result positions the view's clipping planes only.

  • section-volume.js
  • lines 79 to 100
  • BimCraft 0.8.1
/** Outward normal and point of each plane bounding the cell. */
export function cellPlanes(cell) {
  const { min, max } = cell.box, c = centreOf(cell.box), out = [];
  for (const f of FACES) {
    const k = 'xyz'.indexOf(f[0]), hi = f[1] === '1';
    const n0 = [0, 0, 0]; n0[k] = hi ? 1 : -1;
    const p = [...c]; p[k] = hi ? max[k] : min[k];
    const [ta, tb] = cell.tilt?.[f] || [0, 0];
    let n = n0;
    if (ta || tb) {
      const [u, v] = FACE_AXES[f];
      n = [...n0]; n[u] += Math.tan(rad(ta)); n[v] += Math.tan(rad(tb));
      n = unit(n);
    }
    out.push({ n, p, face: f });
  }
  (cell.planes || []).forEach((q, i) => {
    const n = unit([Math.cos(rad(q.el)) * Math.cos(rad(q.az)), Math.cos(rad(q.el)) * Math.sin(rad(q.az)), Math.sin(rad(q.el))]);
    out.push({ n, p: add(c, mul(n, q.off)), extra: i });
  });
  return out;
}

Corners and edges of a section cell

32-bit, display only

The corners, faces and edges of the convex cell its planes bound, drawn as the cell's frame.

corner = (d_a (n_b × n_c) + d_b (n_c × n_a) + d_c (n_a × n_b)) ÷ (n_a · (n_b × n_c))

Every triple of planes is intersected and the points inside all planes are kept, with a tolerance scaled to the largest plane coordinate. The frame is drawn on screen; no stored value depends on it.

  • section-volume.js
  • lines 113 to 143
  • BimCraft 0.8.1
/**
 * The convex polyhedron the planes bound: its corners, each plane's face as a loop of corner indices (counter-clockwise
 * seen from outside; empty when the plane does not touch the cell) and the edges (pairs, once each). Empty cell → no verts.
 */
export function polyhedron(planes) {
  const verts = [], on = [];
  const scale = planes.reduce((m, q) => Math.max(m, Math.abs(q.p[0]), Math.abs(q.p[1]), Math.abs(q.p[2])), 1);
  const tol = scale * 1e-7 + EPS;
  for (let i = 0; i < planes.length; i++) for (let j = i + 1; j < planes.length; j++) for (let k = j + 1; k < planes.length; k++) {
    const x = meet(planes[i], planes[j], planes[k]);
    if (!x || !inside(planes, x, tol * 10)) continue;
    let at = verts.findIndex((v) => Math.hypot(v[0] - x[0], v[1] - x[1], v[2] - x[2]) <= tol * 100);
    if (at < 0) { at = verts.length; verts.push(x); on.push(new Set()); }
    on[at].add(i).add(j).add(k);
  }
  const loops = planes.map((q, i) => {
    const ids = [];
    verts.forEach((v, vi) => { if (on[vi].has(i) || Math.abs(dot(q.n, sub(v, q.p))) <= tol * 100) ids.push(vi); });
    if (ids.length < 3) return [];
    const c = mul(ids.reduce((s, vi) => add(s, verts[vi]), [0, 0, 0]), 1 / ids.length);
    const u = unit(sub(verts[ids[0]], c)), w = cross(q.n, u);
    return ids.map((vi) => { const d = sub(verts[vi], c); return [vi, Math.atan2(dot(d, w), dot(d, u))]; })
      .sort((a, b) => a[1] - b[1]).map((x) => x[0]);
  });
  const seen = new Set(), edges = [];
  for (const L of loops) for (let i = 0; i < L.length; i++) {
    const a = L[i], b = L[(i + 1) % L.length], key = a < b ? a + ',' + b : b + ',' + a;
    if (!seen.has(key)) { seen.add(key); edges.push([a, b]); }
  }
  return { verts, loops, edges };
}

A plane through a point as an extra plane

32-bit, display only

Turns a plane given by a normal and a point into the azimuth, elevation and offset a section cell keeps.

az = atan2(u_y, u_x); el = asin(clamp(u_z, −1, 1)); off = u · (p − center)

The argument of the arcsine is clamped to [−1, 1]. The result sets a clipping plane of the view.

  • section-volume.js
  • lines 145 to 149
  • BimCraft 0.8.1
/** A plane through point p with outward normal n, as an extra plane of a cell whose box is `box`. */
export function planeFromPoint(n, p, box) {
  const u = unit(n), c = centreOf(box);
  return { az: ((deg(Math.atan2(u[1], u[0])) % 360) + 360) % 360, el: deg(Math.asin(clamp(u[2], -1, 1))), off: dot(u, sub(p, c)) };
}

In detail

Method
A cell is a box whose faces can be tilted and cut by extra planes; its corners are the points where three planes meet that lie inside all the others.
Number types
64-bit numbers in this file; the viewer clips on the GPU.
Tolerances
Three planes meet when their triple product is at least 1e-9. Corner tolerance = largest plane coordinate × 1e-7 + 1e-7; corners within 100 times that are merged. Tilts are limited to 80 degrees.

Approach: Display only

Computed to draw the screen, often in 32-bit on the graphics processor. No number a person reads, saves or exports comes from it.

Every policy that takes this approach