Resources Numerical accuracy Calculation policy

MEP clearances and openings through walls and slabs

How close a pipe or duct comes to a wall, slab, beam or column, and where a run passes through a wall or slab with the opening size it needs.

How the precision is kept

The distance and crossing formulas in this file are 64-bit. In the released app the triangles of the MEP and structural elements are handed in by mep/structure-ui.js from clash.js elementsOf, which stores them in a Float32Array in the viewer's meters, so the reported gap and the crossing positions start from 32-bit coordinates. A run's center line is 64-bit plan millimeters taken to the viewer's frame, with each bend followed as 8 straight pieces.

Open item. Give the clearance and opening checks the model's 64-bit coordinates instead of the viewer's 32-bit triangle arrays, and add an edge-to-edge distance test.

Calculations under this policy

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

mep/structure.js BimCraft 0.8.1 32-bit in part 6 calculations Reviewed

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

Distance from a point to a triangle

64-bit

The shortest distance from a point to a triangle, on a corner, an edge or the face.

64-bit dot products choose the region by comparing with zero exactly, with no tolerance. The edge cases divide by differences such as d1 − d3 and the face case by va + vb + vc, none of them guarded, so a triangle with a zero-length edge can give 0 ÷ 0. The clearance loop keeps a distance only when it compares smaller, so a result that is not a number is skipped.

  • mep/structure.js
  • lines 52 to 69
  • BimCraft 0.8.1
/** The closest point of triangle (a, b, c) to p (Ericson, Real-Time Collision Detection 5.1.5), its distance. */
function pointTri(p, a, b, c) {
  const ab = sub(b, a), ac = sub(c, a), ap = sub(p, a);
  const d1 = dot(ab, ap), d2 = dot(ac, ap);
  if (d1 <= 0 && d2 <= 0) return len(ap);
  const bp = sub(p, b), d3 = dot(ab, bp), d4 = dot(ac, bp);
  if (d3 >= 0 && d4 <= d3) return len(bp);
  const vc = d1 * d4 - d3 * d2;
  if (vc <= 0 && d1 >= 0 && d3 <= 0) return len(sub(p, add(a, mul(ab, d1 / (d1 - d3)))));
  const cp = sub(p, c), d5 = dot(ab, cp), d6 = dot(ac, cp);
  if (d6 >= 0 && d5 <= d6) return len(cp);
  const vb = d5 * d2 - d1 * d6;
  if (vb <= 0 && d2 >= 0 && d6 <= 0) return len(sub(p, add(a, mul(ac, d2 / (d2 - d6)))));
  const va = d3 * d6 - d5 * d4;
  if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) return len(sub(p, add(b, mul(sub(c, b), (d4 - d3) / ((d4 - d3) + (d5 - d6))))));
  const den = 1 / (va + vb + vc);
  return len(sub(p, add(a, add(mul(ab, vb * den), mul(ac, vc * den)))));
}

Segment crossing a triangle

64-bit

Where along a straight piece of a run it passes through one triangle of a wall or slab.

t = (e₂ · (s × e₁)) ÷ (e₁ · (d × e₂))

A determinant smaller than 1e-14 in size is treated as parallel; this is an absolute value while the determinant scales with the triangle's size and the segment's length. The barycentric coordinates and the parameter are accepted 1e-9 outside their range and the parameter is then clamped to [0, 1].

  • mep/structure.js
  • lines 70 to 80
  • BimCraft 0.8.1
/** Where segment p→q crosses triangle (a, b, c) (Möller–Trumbore): its parameter in [0, 1], or null. */
function segTri(p, q, a, b, c) {
  const d = sub(q, p), e1 = sub(b, a), e2 = sub(c, a), h = cross(d, e2), det = dot(e1, h);
  if (Math.abs(det) < 1e-14) return null;
  const f = 1 / det, s = sub(p, a), u = f * dot(s, h);
  if (u < -1e-9 || u > 1 + 1e-9) return null;
  const qv = cross(s, e1), v = f * dot(d, qv);
  if (v < -1e-9 || u + v > 1 + 1e-9) return null;
  const t = f * dot(e2, qv);
  return t >= -1e-9 && t <= 1 + 1e-9 ? Math.min(1, Math.max(0, t)) : null;
}

Clearance between an MEP element and the structure

32-bit in part

The gap reported when a pipe or duct is closer to a structural element than its insulation plus the typed clearance.

need = (insulation + gap) ÷ 1000; gap = min over vertices p and triangles T of dist(p, T), both ways

The arithmetic is 64-bit, but in the released app the triangles come from a Float32Array, so the reported gap carries 32-bit positions. Only vertex-to-triangle distances are taken, so an edge-to-edge closest approach is not measured. A pair is listed when the gap is above 1e-6 m and below the need minus 1e-6 m.

  • mep/structure.js
  • lines 82 to 101
  • BimCraft 0.8.1
/** Soft clearances. mep / structure: elementsOf's [{ key, box, parts }] (metres); insulationOf(key) → mm; skip(a, b) → true for
 *  pairs not to report (a run that passes through the element: an opening, listed there). */
export function clearances({ mep, structure, gapMm = 0, insulationOf = () => 0, skip = () => false }) {
  const out = [];
  for (const a of mep) {
    const need = (insulationOf(a.key) + gapMm) / 1000;
    if (!(need > 0)) continue;
    let va = null, ta = null;
    for (const b of structure) {
      if (a.key === b.key || !boxesNear(a.box, b.box, need) || skip(a.key, b.key)) continue;
      va ??= vertsOf(a); ta ??= trisOf(a);
      const tb = trisOf(b), vb = vertsOf(b);
      let d = Infinity;
      for (const p of va) for (const t of tb) { const x = pointTri(p, t[0], t[1], t[2]); if (x < d) d = x; }
      for (const p of vb) for (const t of ta) { const x = pointTri(p, t[0], t[1], t[2]); if (x < d) d = x; }
      if (d > 1e-6 && d < need - 1e-6) out.push({ a: a.key, b: b.key, gap: d, need });
    }
  }
  return out.sort((x, y) => x.gap - y.gap);
}

Run center line as straight pieces

64-bit

The center line of a run, part by part, that is tested against walls and slabs.

bend point k = center + (−n cos(θk ÷ 8) + d₁ sin(θk ÷ 8)) × R, k = 0 … 8

Straight parts and tees are single pieces from their 64-bit end points. Each bend is replaced by 8 chords of its arc, whatever its angle or radius.

  • mep/structure.js
  • lines 121 to 127
  • BimCraft 0.8.1
const pieces = [];
lay.parts.forEach((p, i) => {
  const key = keyOf(r.parts[i]);
  if (p.kind === 'bend') { const n = 8; let prev = null; for (let k = 0; k <= n; k++) { const a = p.th * k / n, c = Math.cos(a), s = Math.sin(a), q = [0, 1, 2].map((j) => p.center[j] + (-p.n[j] * c + p.d1[j] * s) * p.R); if (prev) pieces.push({ a: prev, b: q, key }); prev = q; } }
  else if (p.kind === 'tee') pieces.push({ a: sub(p.at, mul(p.d, p.m)), b: add(p.at, mul(p.d, p.m)), key });
  else pieces.push({ a: p.a, b: p.b, key });
});

Opening size

64-bit

The width and height of the opening a run needs in a wall or slab.

opening = outside size + 2 × insulation + 2 × gap

One 64-bit sum of the drawn size in millimeters, the insulation and the typed gap; nothing is rounded. A round section uses its diameter for both width and height.

  • mep/structure.js
  • lines 128 to 129
  • BimCraft 0.8.1
const ins = Number(r.insulation) || 0;
const ow = (sec.round ? sec.w : sec.w) + 2 * ins + 2 * gapMm, oh = (sec.round ? sec.w : sec.h) + 2 * ins + 2 * gapMm;

Where a run passes through an element

32-bit in part

The position of each opening on the run, its place in plan, and the length of run inside the wall or slab.

s = s₀ + u × piece length; through = s_leave − s_enter; at = midpoint of the two crossings

The run's pieces are 64-bit, but they are crossed with triangles that the released app supplies from a Float32Array, so the crossing parameter u carries 32-bit positions. Crossings are sorted along the run, those within 0.5 mm of the one before are dropped, and the rest are paired in order as enter and leave; an odd crossing left over is ignored.

  • mep/structure.js
  • lines 132 to 147
  • BimCraft 0.8.1
const hits = [];
let s0 = 0;
for (const pc of pieces) {
  const A = toViewer(storey, pc.a), B = toViewer(storey, pc.b), Lmm = len(sub(pc.b, pc.a));
  const bb = [Math.min(A[0], B[0]), Math.min(A[1], B[1]), Math.min(A[2], B[2]), Math.max(A[0], B[0]), Math.max(A[1], B[1]), Math.max(A[2], B[2])];
  if (boxesNear(bb, el.box, 1e-6)) for (const t of trisOf(el)) { const u = segTri(A, B, t[0], t[1], t[2]); if (u !== null) hits.push({ s: s0 + u * Lmm, at: lerp(pc.a, pc.b, u), key: pc.key }); }
  s0 += Lmm;
}
hits.sort((x, y) => x.s - y.s);
// one crossing per face: hits on a shared triangle edge come twice
const uniq = hits.filter((h, i) => !i || h.s - hits[i - 1].s > 0.5);
for (let i = 0; i + 1 < uniq.length; i += 2) {
  const a = uniq[i], b = uniq[i + 1];
  rows.push({ run: r.guid, system: r.system, kind: r.kind, sizeLabel: sec.label, shape: sec.round ? 'round' : sec.shape, w: ow, h: oh, insulation: ins, gap: gapMm,
    element: el.key, cls: el.cls, at: lerp(a.at, b.at, 0.5), storey, through: b.s - a.s, part: a.key, s: a.s });
}

In detail

Method
Closest point on a triangle to a point (Ericson, Real-Time Collision Detection 5.1.5) and segment against triangle (Möller-Trumbore), as the code's comments cite. A clearance is the least distance from the vertices of one element to the triangles of the other, taken both ways. An opening is a pair of crossings of the run's center line with one element's triangles.
Number types
64-bit arithmetic on triangle coordinates that the caller supplies in a Float32Array (viewer meters). Run points and opening sizes are 64-bit millimeters.
Tolerances
Segment against triangle: |det| < 1e-14 (absolute) is treated as parallel; barycentric coordinates and the segment parameter are accepted within 1e-9. Clearance reported when 1e-6 m < gap < need − 1e-6 m. Crossings closer than 0.5 mm along the run are taken as one. Bounding boxes are grown by the needed clearance, or by 1e-6 m for openings.
Limits
The clearance is a vertex-to-triangle distance: where two elements are closest edge to edge, the true gap can be smaller than the one reported. A bend is 8 chords, so a crossing inside an elbow is found on the chords. A 32-bit coordinate between 64 m and 128 m from the viewer's origin is spaced 0.0076 mm apart.

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