Resources Numerical accuracy Calculation policy

Clash check between parts

Finds steel parts and footings that run into each other and reports how deep and where.

How the precision is kept

64-bit arithmetic. Boxes round the parts are compared with the separating-axis test; for boxes that meet, sample points across one part's section are tested inside the other's true section. The reported depth is the largest found at those sample points, so it depends on the sampling, and it is rounded to 0.1 mm in the result.

Calculations under this policy

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

clash.js DetailCraft 0.8.1 64-bit 9 calculations Reviewed

Tests bin/test-detailcraft.mjs

Box round each part and footing

64-bit

The center, axes and half sizes of the box used for the first, coarse test.

c = o + x̂·L ÷ 2 + v̂·voff; h = (L ÷ 2, width ÷ 2, depth ÷ 2)

Halves and sums of stored sizes. A footing's axes come from its rotation in degrees × Math.PI ÷ 180 through Math.cos and Math.sin.

  • clash.js
  • lines 34 to 57
  • DetailCraft 0.8.1
/** A solid to check: { ref, id, kind, phase, frame {o, x, u, v}, L, loops (section, centered), voff, cuts, box (obb) }. */
export function solids(state, built, phases) {
  const keep = (ph) => !phases || phases.has(ph || 1);
  const out = [];
  for (const p of built.parts) {
    if (!keep(p.phase)) continue;
    const w = p.profile.width, d = p.profile.depth;
    const c = add(p.frame.o, add(mul(p.frame.x, p.length / 2), mul(p.frame.v, p.voff)));
    out.push({ ref: p.member ? { kind: 'member', id: p.member } : p.stair ? { kind: 'stair', id: p.stair } : { kind: 'conn', id: p.conn }, id: p.id, kind: 'steel', phase: p.phase || 1, conn: p.conn, member: p.member, stair: p.stair || null,
      frame: p.frame, L: p.length, loops: p.profile.loops, voff: p.voff, cuts: p.cuts.filter((k) => k.kind === 'cope'), depth: d, lug: !!p.conn && p.id === p.conn + '.lug',
      box: { c, ax: [p.frame.x, p.frame.u, p.frame.v], h: [p.length / 2, w / 2, d / 2] } });
  }
  for (const f of state.footings) {
    if (!keep(f.phase)) continue;
    const a = (Number(f.rotation) || 0) * Math.PI / 180;
    const x = [Math.cos(a), Math.sin(a), 0], u = [-Math.sin(a), Math.cos(a), 0], v = [0, 0, 1];
    const o = add([f.x, f.y, f.top - f.H / 2], mul(x, -f.L / 2));
    const loop = [[-f.B / 2, -f.H / 2], [f.B / 2, -f.H / 2], [f.B / 2, f.H / 2], [-f.B / 2, f.H / 2]];
    // the footing as a "part" along its length: section B × H (u across, v up)
    out.push({ ref: { kind: 'footing', id: f.id }, id: 'footing:' + f.id, kind: 'concrete', phase: f.phase || 1, frame: { o, x, u, v }, L: f.L, loops: [loop], voff: 0, cuts: [], depth: f.H,
      box: { c: [f.x, f.y, f.top - f.H / 2], ax: [x, u, v], h: [f.L / 2, f.B / 2, f.H / 2] } });
  }
  return out;
}

Do two boxes overlap

64-bit

Whether two oriented boxes overlap by more than the tolerance.

separated on axis l when |T·l| > r_A + r_B − tol

Each of up to 15 axes is normalized by one division before the projections. Cross-product axes with length 1e-9 or less are left out, which removes the degenerate axes of parallel boxes. The tolerance is subtracted from the sum of the two radii.

  • clash.js
  • lines 59 to 71
  • DetailCraft 0.8.1
/** Oriented boxes overlap (separating-axis test, 15 axes), allowing `tol` of contact. */
export function obbOverlap(A, B, tol = 0) {
  const T = sub(B.c, A.c);
  const axes = [...A.ax, ...B.ax];
  for (const a of A.ax) for (const b of B.ax) { const c = cross(a, b); if (Math.hypot(...c) > 1e-9) axes.push(c); }
  for (const L of axes) {
    const n = Math.hypot(...L), l = mul(L, 1 / n);
    const ra = A.h.reduce((s, h, i) => s + h * Math.abs(dot(A.ax[i], l)), 0);
    const rb = B.h.reduce((s, h, i) => s + h * Math.abs(dot(B.ax[i], l)), 0);
    if (Math.abs(dot(T, l)) > ra + rb - tol) return false;
  }
  return true;
}

Point inside a section outline

64-bit

Whether a point lies inside a closed outline.

Even-odd crossing count with no tolerance: a point exactly on an edge can fall either way. The division by the edge's rise only runs when the edge straddles the point's height, so it is never by zero.

  • clash.js
  • lines 73 to 80
  • DetailCraft 0.8.1
function inPoly(p, loop) {
  let inside = false;
  for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) {
    const a = loop[i], b = loop[j];
    if ((a[1] > p[1]) !== (b[1] > p[1]) && p[0] < ((b[0] - a[0]) * (p[1] - a[1])) / (b[1] - a[1]) + a[0]) inside = !inside;
  }
  return inside;
}

Distance to the nearest outline edge

64-bit

The shortest distance from a point to any edge of an outline.

t = clamp((p − a)·(b − a) ÷ |b − a|², 0, 1); d = |p − a − t·(b − a)|

The projection parameter is clamped to the edge and the distance is Math.hypot. An edge of zero length is treated as its start point.

  • clash.js
  • lines 81 to 89
  • DetailCraft 0.8.1
function edgeDist(p, loop) {
  let best = Infinity;
  for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) {
    const a = loop[j], b = loop[i], dx = b[0] - a[0], dy = b[1] - a[1], L2 = dx * dx + dy * dy;
    const t = L2 ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L2)) : 0;
    best = Math.min(best, Math.hypot(p[0] - a[0] - t * dx, p[1] - a[1] - t * dy));
  }
  return best;
}

How far a point is inside a part

64-bit

The distance from a point to the outside of a part, zero when it is outside.

depth = min(x, L − x, distance to each outline edge)

The point is first put in the part's own coordinates, then tested against the section at that station with copes cut away. A point on the end faces or outside the outline returns 0.

  • clash.js
  • lines 100 to 109
  • DetailCraft 0.8.1
/** How far a world point is inside solid S (≤ 0: outside). */
export function depthIn(S, w) {
  const l = toLocal(S.frame, w, S.voff);
  if (l.x <= 0 || l.x >= S.L) return 0;
  const loops = loopsAt(S, l.x);
  if (!loops.length) return 0;
  const p = [l.u, l.v];
  if (!inPoly(p, loops[0]) || loops.slice(1).some((h) => inPoly(p, h))) return 0;
  return Math.min(l.x, S.L - l.x, ...loops.map((lp) => edgeDist(p, lp)));
}

Ray to the far side of the material

64-bit

How far a ray from an edge runs before it meets another edge of the section, to place a sample point in the middle of a flange or web.

t = (w × e) ÷ (d × e), u = (w × d) ÷ (d × e)

Edges nearly parallel to the ray (|denominator| < 1e-12) are skipped. Hits closer than 1e-6 along the ray are ignored, and the edge parameter is accepted within 1e-9 of its ends; all three are absolute.

  • clash.js
  • lines 110 to 122
  • DetailCraft 0.8.1
/** How far a ray from p along d runs before it meets an edge of the loops (Infinity if never). */
function rayExit(p, d, loops) {
  let best = Infinity;
  for (const loop of loops) for (let i = 0, j = loop.length - 1; i < loop.length; j = i++) {
    const a = loop[j], b = loop[i], e = [b[0] - a[0], b[1] - a[1]];
    const den = d[0] * e[1] - d[1] * e[0];
    if (Math.abs(den) < 1e-12) continue;
    const w = [a[0] - p[0], a[1] - p[1]];
    const t = (w[0] * e[1] - w[1] * e[0]) / den, u = (w[0] * d[1] - w[1] * d[0]) / den;
    if (t > 1e-6 && u >= -1e-9 && u <= 1 + 1e-9) best = Math.min(best, t);
  }
  return best;
}

Sample points across and along a part

64-bit

The points of one part that are tested inside the other.

stations n = clamp(⌈(t1 − t0) ÷ 10⌉, 2, 40)

The points just inside each edge sit tol + 1 mm in. The inner grid is 8 × 8 over the outline's bounding box. The number of stations is capped at 40 intervals, so the spacing grows beyond 10 mm on long overlaps.

  • clash.js
  • lines 123 to 157
  • DetailCraft 0.8.1
/** Points across A's section (corners, edge midpoints, an inner grid) at stations over [t0, t1]. */
function samples(A, t0, t1, tol) {
  const pts = [];
  const sec = [];
  const inside = (q) => inPoly(q, A.loops[0]) && !A.loops.slice(1).some((h) => inPoly(q, h));
  const step = tol + 1;                                        // just inside each edge: thin webs and flanges are sampled too
  for (const loop of A.loops) for (let i = 0; i < loop.length; i++) {
    const a = loop[i], b = loop[(i + 1) % loop.length], m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
    sec.push(a, m);
    const L = Math.hypot(b[0] - a[0], b[1] - a[1]);
    if (L < 1e-9) continue;
    const n = [-(b[1] - a[1]) / L, (b[0] - a[0]) / L];
    for (const sg of [1, -1]) {
      const q = [m[0] + sg * n[0] * step, m[1] + sg * n[1] * step];
      if (!inside(q)) continue;
      sec.push(q);
      // halfway across the material from this edge (the middle of a flange or web)
      const d = [sg * n[0], sg * n[1]], tx = rayExit(m, d, A.loops);
      if (tx > 2 * step) sec.push([m[0] + d[0] * tx / 2, m[1] + d[1] * tx / 2]);
    }
  }
  let x0 = Infinity, x1 = -Infinity, y0 = Infinity, y1 = -Infinity;
  for (const [u, v] of A.loops[0]) { x0 = Math.min(x0, u); x1 = Math.max(x1, u); y0 = Math.min(y0, v); y1 = Math.max(y1, v); }
  const N = 7;
  for (let i = 0; i <= N; i++) for (let j = 0; j <= N; j++) {
    const q = [x0 + (x1 - x0) * i / N, y0 + (y1 - y0) * j / N];
    if (inside(q)) sec.push(q);
  }
  const n = Math.max(2, Math.min(40, Math.ceil((t1 - t0) / 10)));
  for (let k = 0; k <= n; k++) {
    const x = t0 + (t1 - t0) * k / n;
    for (const [u, v] of sec) pts.push(toWorld(A.frame, x, u, v, A.voff));
  }
  return pts;
}

Stretch of a part that can reach the other

64-bit

The range along one part's axis where it can meet the other part's box.

t0 = max(0, min x − pad), t1 = min(L, max x + pad); pad = √(h_u² + h_v²)

The other box's eight corners are projected on this part's axis and the range is padded by this part's half diagonal. 64-bit sums and dot products.

  • clash.js
  • lines 158 to 167
  • DetailCraft 0.8.1
/** The stretch of A (along its axis) whose box can reach B's box: [t0, t1] or null. */
function reach(A, B) {
  // B's box corners in A's frame; A's x-range clipped to them, padded by A's own half size
  const corners = [];
  for (const sx of [-1, 1]) for (const su of [-1, 1]) for (const sv of [-1, 1]) corners.push(add(B.box.c, add(mul(B.box.ax[0], sx * B.box.h[0]), add(mul(B.box.ax[1], su * B.box.h[1]), mul(B.box.ax[2], sv * B.box.h[2])))));
  const xs = corners.map((c) => dot(sub(c, A.frame.o), A.frame.x));
  const pad = Math.hypot(A.box.h[1], A.box.h[2]);
  const t0 = Math.max(0, Math.min(...xs) - pad), t1 = Math.min(A.L, Math.max(...xs) + pad);
  return t1 > t0 ? [t0, t1] : null;
}

Reported depth and location

64-bit

The depth and the point each clash is reported with.

depth = round(depth × 10) ÷ 10

The deepest sample is kept in 64-bit, then the depth and each coordinate of the point are rounded to 0.1 mm with Math.round before they are returned. The list is sorted on the rounded depth.

  • clash.js
  • line 205
  • DetailCraft 0.8.1
if (best) clashes.push({ a: A.ref, b: B.ref, aPart: A.id, bPart: B.id, depth: Math.round(best.depth * 10) / 10, at: best.at.map((v) => Math.round(v * 10) / 10), kind: A.kind === 'concrete' || B.kind === 'concrete' ? 'concrete' : 'steel' });

In detail

Method
Separating-axis test on oriented boxes (15 axes), then point-in-polygon and distance-to-edge tests at sampled points, both ways round.
Number types
64-bit numbers in plain arrays.
Tolerances
Clash tolerance 1 mm by default, absolute. Parallel box axes skipped when the cross product is 1e-9 or shorter. Ray test: 1e-12 on the denominator, 1e-6 and 1e-9 on the parameters. Zero-length outline edges under 1e-9 mm skipped.
Limits
Sampling: section points at corners, edge midpoints, just inside each edge, mid-material and an 8 × 8 grid; stations along the part at most 40 intervals, so on an overlap longer than 400 mm they are more than 10 mm apart. A clash that falls between sample points can be missed or reported shallower than it is. Boxes use the section's overall width and depth.

Approach: 64-bit, stated limit

The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.

Every policy that takes this approach