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.jsBimCraft 0.8.1 32-bit in part6 calculationsReviewed
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.
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.
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.