Resources Numerical accuracy Calculation policy

Finding joints between members

Finds where a beam end meets a column, where two members meet end to end in line or at an angle, and where a brace lands on another member.

How the precision is kept

64-bit vector arithmetic on the members' stored end points. Every decision is a comparison against a fixed tolerance: 1 mm for distances and fixed cosine limits for directions. The tolerances are absolute and do not scale with the size of the model.

Calculations under this policy

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

connections.js DetailCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-detailcraft.mjs

Distance from a point to a member line

64-bit

The shortest distance from a point to the segment between a member's two ends.

k = clamp((P − a)·(b − a) ÷ |b − a|², 0, 1); d = |P − (a + k·(b − a))|

The division is guarded for a zero-length segment. The foot point is rebuilt from a + k·(b − a) before the final subtraction.

  • connections.js
  • lines 121 to 126
  • DetailCraft 0.8.1
/** Distance from point P to the segment a–b. */
function segDist(P, a, b) {
  const ab = sub(b, a), L2 = dot(ab, ab);
  const k = L2 > 0 ? Math.max(0, Math.min(1, dot(sub(P, a), ab) / L2)) : 0;
  return len(sub(P, add(a, mul(ab, k))));
}

Is a member vertical

64-bit

Whether a member stands vertical, the condition for a column to take a connection.

|Δx| < 1 mm and |Δy| < 1 mm and |Δz| ≥ 1 mm

Absolute 1 mm limits on the coordinate differences, whatever the member's length, so the lean allowed is under 1 mm in each plan direction over the full height.

  • connections.js
  • line 184
  • DetailCraft 0.8.1
const isVertical = (m) => Math.abs(m.start[0] - m.end[0]) < TOL && Math.abs(m.start[1] - m.end[1]) < TOL && Math.abs(m.start[2] - m.end[2]) >= TOL;

Members end to end in line

64-bit

Pairs of member ends that meet in line, with the gap between them.

along = d·d̂_a; off = |d − d̂_a·along|; in line when |d̂_a·d̂_b| ≥ 0.9999, off ≤ 1 mm, |d| ≤ 50 mm

Directions are normalized end differences. The offset from the line is found by subtracting the along part from the gap vector. The gap returned is |along|, unrounded.

  • connections.js
  • lines 160 to 182
  • DetailCraft 0.8.1
/**
 * Where two members meet end to end in line: [{ a, aEnd, b, bEnd, gap }] — a's end within 50 mm of b's end, the two
 * parallel and on one line (within 1 mm), both I-shapes of one profile is not required here (buildSplice checks).
 */
export function findSplices(state, ids = null) {
  const ms = state.members.filter((m) => !ids || ids.includes(m.id));
  const out = [];
  for (let i = 0; i < ms.length; i++) for (let j = i + 1; j < ms.length; j++) {
    const A = ms[i], B = ms[j];
    const da = norm(sub(A.end, A.start)), db = norm(sub(B.end, B.start));
    if (Math.abs(dot(da, db)) < 0.9999) continue;
    for (const aEnd of ['start', 'end']) for (const bEnd of ['start', 'end']) {
      const P = A[aEnd], Q = B[bEnd], d = sub(Q, P), along = dot(d, da), off = len(sub(d, mul(da, along)));
      if (off > TOL || len(d) > 50) continue;
      // b must lie beyond a's end, not overlap it
      const out_ = aEnd === 'end' ? 1 : -1;
      const bOther = B[bEnd === 'start' ? 'end' : 'start'];
      if (dot(sub(bOther, P), da) * out_ <= 0) continue;
      out.push({ a: A.id, aEnd, b: B.id, bEnd, gap: Math.abs(along) });
    }
  }
  return out;
}

Beam end at a column

64-bit

Which beam ends land on a column, and whether they meet its flange, its web or come in skewed.

flange when |away·v̂| > 0.9999; web when |away·û| > 0.9999; else skew

The beam end must be within 1 mm of the column line in plan and within its height plus 1 mm. The beam's plan direction is normalized; a beam with no plan length is skipped.

  • connections.js
  • lines 186 to 208
  • DetailCraft 0.8.1
/** Where a beam end meets a column: [{ beam, end, column, face: 'flange'|'web'|'skew', away }]. */
export function findJoints(state) {
  const cols = state.members.filter((m) => m.type === 'column' && isVertical(m));
  const out = [];
  for (const b of state.members) {
    if (b.type !== 'beam') continue;
    for (const end of ['start', 'end']) {
      const N = b[end], O = end === 'start' ? b.end : b.start;
      for (const c of cols) {
        const z0 = Math.min(c.start[2], c.end[2]), z1 = Math.max(c.start[2], c.end[2]);
        if (Math.abs(N[0] - c.start[0]) > TOL || Math.abs(N[1] - c.start[1]) > TOL || N[2] < z0 - TOL || N[2] > z1 + TOL) continue;
        const away = norm([O[0] - N[0], O[1] - N[1], 0]);
        if (len(away) < 0.5) continue;
        const fc = frame(c.start, c.end, c.rotation);
        const dv = dot(away, fc.v), du = dot(away, fc.u);
        const face = Math.abs(dv) > 0.9999 ? 'flange' : Math.abs(du) > 0.9999 ? 'web' : 'skew';
        out.push({ beam: b.id, end, column: c.id, face, away });
        break;
      }
    }
  }
  return out;
}

Members meeting at an angle

64-bit

Pairs of I-shaped member ends that share a point and are not in line, with the angle between them.

angle = acos(clamp(ê_A·ê_B, −1, 1)) × 180 ÷ π

The dot product is clamped to [−1, 1] before Math.acos. Near 0 and 180 degrees the arc cosine is steep, so the angle there carries fewer digits than the dot product; joints closer than cos = −0.9998 to straight are left out.

  • connections.js
  • lines 692 to 710
  • DetailCraft 0.8.1
/**
 * Where two I-shaped members meet end to end at an angle: [{ a, aEnd, b, bEnd, angle }] — a's end within 1 mm of b's end (their
 * reference lines share the node), not in line, among `ids` (all members if null). The angle is between the two members (90 = square).
 */
export function findMitres(state, ids = null) {
  const ms = state.members.filter((m) => m.type !== 'plate' && (!ids || ids.includes(m.id)) && getProfile(m.profile, state.profiles)?.kind === 'I');
  const out = [];
  for (let i = 0; i < ms.length; i++) for (let j = i + 1; j < ms.length; j++) {
    const A = ms[i], B = ms[j];
    for (const aEnd of ['start', 'end']) for (const bEnd of ['start', 'end']) {
      if (len(sub(A[aEnd], B[bEnd])) > TOL) continue;
      const eA = norm(sub(A[otherEnd(aEnd)], A[aEnd])), eB = norm(sub(B[otherEnd(bEnd)], B[bEnd]));
      const c = dot(eA, eB);
      if (c < -0.9998) continue;                                             // in line: a splice, not a mitre
      out.push({ a: A.id, aEnd, b: B.id, bEnd, angle: Math.acos(Math.max(-1, Math.min(1, c))) * 180 / Math.PI });
    }
  }
  return out;
}

In detail

Method
Point-to-segment distance, dot products of unit directions, differences of coordinates.
Number types
64-bit numbers in plain arrays.
Tolerances
TOL = 1 mm for all distance tests. In line: |cos| ≥ 0.9999 (about 0.81 degrees). Flange or web face: |cos| > 0.9999. Splice gap up to 50 mm. Mitre: not in line when cos > −0.9998 (more than about 1.15 degrees from straight).
Limits
A column is found only if it is vertical within 1 mm in plan over its whole length.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach