Resources Numerical accuracy Calculation policy

Plan view of elements drawn from their mesh

Cuts imported and library elements at the plan's cut height and joins the cut lines into outlines to draw.

How the precision is kept

Elements the drawing tools did not make are drawn on the plan from the view's 32-bit mesh: vertices are taken into the story's plan axes in 64-bit, each triangle is cut by the horizontal plane, and the segments are chained into loops for the fill. The result is drawn on screen and on the plan PDF; no dimension or quantity is taken from it.

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.

model-plan.js BimCraft 0.8.1 32-bit, display only 3 calculations Reviewed

Tests tools/bimcraft-test/model-test.mjs

Cutting a story's elements

32-bit, display only

The cut segments of every element the cut plane passes through, and the edges of what lies below or above.

Reads the 32-bit mesh positions and transforms them into three Float64Array of plan coordinates. An element is cut when its lowest and highest vertices straddle the cut height.

  • model-plan.js
  • lines 30 to 79
  • BimCraft 0.8.1
/**
 * Cut every element of `storey` (a storey expressID; batch.storey) at plan height cutZ (mm above the storey's origin).
 * `skip` = keys drawn some other way. Elements wholly below the cut (and not more than `depth` below `bottom`, 0 = the level)
 * give their feature edges instead (30°), flattened; with `top` (0.6 DF.19, a view range), elements wholly above the cut and
 * starting under `top` give theirs as `above` (drawn dashed: overhead). → { cut, below, above, bounds: { x0, y0, x1, y1 } | null }
 */
export function planCut(model, { M, storey, cutZ = 1200, skip = null, depth = 50, bottom = 0, top = null, edges = true } = {}) {
  const cut = new Map(), below = new Map(), above = new Map();
  const bb = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity };
  const grow = (x, y) => { if (x < bb.x0) bb.x0 = x; if (x > bb.x1) bb.x1 = x; if (y < bb.y0) bb.y0 = y; if (y > bb.y1) bb.y1 = y; };
  for (const b of model.batches || []) {
    if (b.space || b.storey !== storey) continue;
    const P = b.positions, I = b.indices, E = b.elems;
    // the batch's vertices in plan axes, once
    const n = P.length / 3, X = new Float64Array(n), Y = new Float64Array(n), Z = new Float64Array(n);
    for (let i = 0; i < n; i++) {
      const x = P[i * 3], y = P[i * 3 + 1], z = P[i * 3 + 2];
      X[i] = M[0] * x + M[4] * y + M[8] * z + M[12];
      Y[i] = M[1] * x + M[5] * y + M[9] * z + M[13];
      Z[i] = M[2] * x + M[6] * y + M[10] * z + M[14];
    }
    for (let r = 0; r < E.ids.length; r++) {
      const key = E.ids[r];
      if (skip?.has(key)) continue;
      const t0 = E.t0[r], tn = E.tn[r];
      let zmin = Infinity, zmax = -Infinity;
      for (let v = E.v0[r], ve = E.v0[r] + E.vn[r]; v < ve; v++) { if (Z[v] < zmin) zmin = Z[v]; if (Z[v] > zmax) zmax = Z[v]; }
      if (zmin <= cutZ && zmax >= cutZ) {
        const out = [];
        for (let t = t0; t < t0 + tn; t++) {
          const a = I[t * 3], c = I[t * 3 + 1], d = I[t * 3 + 2];
          triCut(X, Y, Z, a, c, d, cutZ, out);
        }
        if (out.length) { const prev = cut.get(key); cut.set(key, prev ? concat(prev, out) : Float64Array.from(out)); for (let i = 0; i < out.length; i += 2) grow(out[i], out[i + 1]); }
      } else if (edges && ((zmax < cutZ && zmax > bottom - depth) || (top !== null && zmin > cutZ && zmin < top))) {
        const into = zmax < cutZ ? below : above;
        const raw = runEdges(P, I, t0, tn, 30, []), out = [];
        for (let i = 0; i < raw.length; i += 3) {
          const x = raw[i], y = raw[i + 1], z = raw[i + 2];
          out.push(M[0] * x + M[4] * y + M[8] * z + M[12], M[1] * x + M[5] * y + M[9] * z + M[13]);
        }
        // drop edges seen end on (vertical ones)
        const keep = [];
        for (let i = 0; i < out.length; i += 4) if (Math.hypot(out[i + 2] - out[i], out[i + 3] - out[i + 1]) > 0.5) { keep.push(out[i], out[i + 1], out[i + 2], out[i + 3]); grow(out[i], out[i + 1]); }
        if (keep.length) { const prev = into.get(key); into.set(key, prev ? concat(prev, keep) : Float64Array.from(keep)); }
      }
    }
  }
  return { cut, below, above, bounds: Number.isFinite(bb.x0) ? bb : null };
}

Cutting one triangle

32-bit, display only

The segment where a triangle crosses the cut plane.

t = (h − zᵢ) ÷ (zⱼ − zᵢ); point = pᵢ + t (pⱼ − pᵢ)

Each crossing edge is interpolated from the end that sorts first by position, so two triangles sharing an edge compute the same point bit for bit. The division is by the height difference of two vertices on opposite sides of the plane.

  • model-plan.js
  • lines 82 to 97
  • BimCraft 0.8.1
/** One triangle against the plane z = h: the segment where they cross (x0 y0 x1 y1 appended), if any. */
function triCut(X, Y, Z, a, b, c, h, out) {
  const v = [a, b, c], above = v.map((i) => Z[i] > h);
  const n = above.filter(Boolean).length;
  if (n === 0 || n === 3) return;
  const pts = [];
  for (let k = 0; k < 3; k++) {
    const p = v[k], q = v[(k + 1) % 3];
    if (above[k] === above[(k + 1) % 3]) continue;
    // the same edge gives the same point from either triangle: order its ends by position, not by index
    const [i, j] = X[p] < X[q] || (X[p] === X[q] && (Y[p] < Y[q] || (Y[p] === Y[q] && Z[p] < Z[q]))) ? [p, q] : [q, p];
    const t = (h - Z[i]) / (Z[j] - Z[i]);
    pts.push(X[i] + (X[j] - X[i]) * t, Y[i] + (Y[j] - Y[i]) * t);
  }
  if (pts.length === 4 && (pts[0] !== pts[2] || pts[1] !== pts[3])) out.push(pts[0], pts[1], pts[2], pts[3]);
}

Joining cut segments into loops

32-bit, display only

Closed outlines from the cut segments, for the fill.

Ends are matched by rounding to 0.01 mm; a chain that does not come back to its start is left out.

  • model-plan.js
  • lines 99 to 131
  • BimCraft 0.8.1
/** Join cut segments into closed loops (and leave out what does not close). Points match to 0.01 mm. */
export function chainLoops(segs) {
  const key = (x, y) => Math.round(x * 100) + ',' + Math.round(y * 100);
  const adj = new Map();
  const n = segs.length / 4, used = new Uint8Array(n);
  for (let i = 0; i < n; i++) {
    for (const e of [0, 1]) {
      const k = key(segs[i * 4 + e * 2], segs[i * 4 + e * 2 + 1]);
      if (!adj.has(k)) adj.set(k, []);
      adj.get(k).push(i * 2 + e);
    }
  }
  const loops = [];
  for (let s = 0; s < n; s++) {
    if (used[s]) continue;
    used[s] = 1;
    const loop = [segs[s * 4], segs[s * 4 + 1]];
    const startK = key(segs[s * 4], segs[s * 4 + 1]);
    let x = segs[s * 4 + 2], y = segs[s * 4 + 3];
    for (let guard = 0; guard < n + 2; guard++) {
      const k = key(x, y);
      loop.push(x, y);
      if (k === startK) break;
      const next = (adj.get(k) || []).find((h) => !used[h >> 1]);
      if (next === undefined) break;
      used[next >> 1] = 1;
      const i = next >> 1, e = next & 1, o = 1 - e;
      x = segs[i * 4 + o * 2]; y = segs[i * 4 + o * 2 + 1];
    }
    if (loop.length >= 8 && key(loop[0], loop[1]) === key(loop[loop.length - 2], loop[loop.length - 1])) loops.push(loop);
  }
  return loops;
}

In detail

Method
Triangle against the plane z = cut height by linear interpolation on its two crossing edges; loops by matching segment ends.
Number types
Float32Array positions in; Float64Array plan coordinates and segments.
Tolerances
Segment ends match when they round to the same 0.01 mm. Edges of elements below the cut shorter than 0.5 mm in plan are dropped.

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