Resources Numerical accuracy Calculation policy

Connection plates, holes, bolts and cut-backs

Works out where each connection's plates, holes, bolts and welds go and how far the joined members are cut back, from the connection's parameters.

How the precision is kept

Positions are 64-bit vector sums built from the members' frames and the connection's parameters, with nothing rounded. Two things are rounded on purpose: bolt lengths go up to the next 5 mm, and every plate is handed on as a profile name such as PL20x180 written with one decimal, so a plate's thickness and width reach the part rounded to 0.1 mm while its position uses the unrounded value. The spacing checks are warnings against EN 1993-1-8 Table 3.3 minimums and compare without a tolerance.

Open item. Pass a plate's thickness and width to the part as numbers instead of through a profile name rounded to 0.1 mm, and compare spacing limits with a small tolerance.

Calculations under this policy

24 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 24 calculations Reviewed

Tests bin/test-detailcraft.mjs

Plate size written into the profile name

64-bit

The thickness and width digits of a plate's profile name, which is how a connection plate's section reaches the built part.

text = Number(v.toFixed(1))

The value is rounded to one decimal with toFixed before it becomes part of the name. A 9.525 mm plate is named 9.5, and the 6.35 mm washer thickness from the Design Guide table is named 6.3, while the plate's position is still computed from the unrounded number.

  • connections.js
  • line 867
  • DetailCraft 0.8.1
const num = (v) => String(Number(Number(v).toFixed(1)));

Bolt length rounded up to 5 mm

64-bit

The length of a bolt from its grip and the allowance.

length = ⌈(grip + allowance) ÷ 5 − 1e-9⌉ × 5

A deliberate round-up to the next 5 mm. The 1e-9 is absolute on the ratio and stops a length that is already a multiple of 5 from gaining 5 mm.

  • connections.js
  • line 255
  • DetailCraft 0.8.1
const ceil5 = (v) => Math.ceil(v / 5 - 1e-9) * 5;

Anchor rod hole and washer sizes

Exact

The hole diameter, washer size and washer thickness for an anchor rod, from the table in AISC Design Guide 1.

mm = inches × 25.4

The table is written in inches as exact fractions (1 + 5 ÷ 16 and so on) and converted with the exact factor 25.4. A metric rod takes the smallest tabulated rod at least as big, with a 0.5 mm allowance on that comparison.

  • connections.js
  • lines 77 to 81
  • DetailCraft 0.8.1
/** The DG1 row for a rod (mm): the smallest tabulated rod at least as big (a metric rod takes the next inch size up). */
export function dg1Row(rodD) {
  const r = DG1_TABLE_2_3.find(([d]) => d * 25.4 >= rodD - 0.5);
  return r ? { rod: r[0] * 25.4, hole: r[1] * 25.4, washer: r[2] * 25.4, washerT: r[3] * 25.4, inch: r } : null;
}

End plate, its holes and bolts

64-bit

Where an end plate sits against the column and where each bolt row goes.

center = N + away·(faceOff + tp ÷ 2); z_i = z_c + ((rows − 1) ÷ 2 − i)·pitch; hole = face ± ŵ·gauge ÷ 2

Rows are placed about the plate's mid height, so the pattern is symmetric by construction. The plate's rotation is Math.atan2 of the beam direction in degrees. The number of rows is the parameter rounded to a whole number.

  • connections.js
  • lines 282 to 306
  • DetailCraft 0.8.1
const faceOff = onWeb ? cp.tw / 2 : cp.d / 2;            // column axis → the face the plate bears on (flange outside, or web)
const beamTop = N[2] + B.voff + bp.d / 2;
const hp = bp.d + p.extTop + p.extBottom;
const zBot = beamTop - bp.d - p.extBottom, zTop = beamTop + p.extTop;
const ctr = add(N, mul(away, faceOff + p.tp / 2));
const rot = Math.atan2(away[1], away[0]) * 180 / Math.PI;
const plate = { key: conn.id + '.plate', role: t('conn.role.endplate'), profileName: 'PL' + num(p.tp) + 'x' + num(p.width), material: p.plateGrade || 'A36',
  start: [ctr[0], ctr[1], zBot], end: [ctr[0], ctr[1], zTop], rotation: rot, position: 'middle', attachTo: conn.beam };
const w = norm([away[1], -away[0], 0]);                 // across the joint, horizontal (= the plate's u)
const zc = (zBot + zTop) / 2;
const rows = Math.max(1, Math.round(p.rows));
const holes = [], bolts = [];
const faceName = onWeb ? 'web' : dv > 0 ? 'top' : 'bottom';   // which column face (section +v = 'top' flange)
const flangeFace = add(N, mul(away, faceOff));
const grip = p.tp + (onWeb ? cp.tw : cp.tf);
const boltLen = ceil5(grip + p.allowance);
for (let i = 0; i < rows; i++) {
  const z = zc + ((rows - 1) / 2 - i) * p.pitch;
  for (const sgn of [1, -1]) {
    const onFace = [flangeFace[0] + w[0] * sgn * p.gauge / 2, flangeFace[1] + w[1] * sgn * p.gauge / 2, z];
    holes.push({ target: plate.key, world: add(onFace, mul(away, p.tp / 2)), face: 'plate', d: p.hole });
    holes.push({ target: conn.column, world: onFace, face: faceName, d: p.hole });
    bolts.push({ conn: conn.id, d: p.boltD, length: boltLen, grip, grade: p.grade, head: add(onFace, mul(away, p.tp)), axis: mul(away, -1), kind: 'BOLT' });
  }
}

Edge distance and spacing checks

64-bit

Warnings when bolt edge distances, pitch or gauge are under the EN 1993-1-8 Table 3.3 minimums.

e1, e2 ≥ 1.2·d0; p1 ≥ 2.2·d0; p2 ≥ 2.4·d0

Strict less-than comparisons with no tolerance. The limits are products of decimal factors in binary: 2.2 × 22 is 48.400000000000006, so a value typed as exactly the minimum can still be flagged. The figures in the message are rounded to one decimal.

  • connections.js
  • lines 309 to 313
  • DetailCraft 0.8.1
const e1 = (hp - (rows - 1) * p.pitch) / 2, e2 = (p.width - p.gauge) / 2;
if (e1 < 1.2 * d0) problems.push(t('conn.warn.e1', { e: num(e1), min: num(1.2 * d0) }));
if (e2 < 1.2 * d0) problems.push(t('conn.warn.e2', { e: num(e2), min: num(1.2 * d0) }));
if (rows > 1 && p.pitch < 2.2 * d0) problems.push(t('conn.warn.pitch', { pitch: num(p.pitch), min: num(2.2 * d0) }));
if (p.gauge < 2.4 * d0) problems.push(t('conn.warn.gauge', { gauge: num(p.gauge), min: num(2.4 * d0) }));

Shear tab: beam cut-back

64-bit

How far a square, skewed or sloped beam is cut back so every corner of its end clears the column face by the gap.

setback = max over corners of (faceOff + gap − c·n) ÷ cos A; cos A = dir·n

The division by cos A is guarded: connections with cos A under 0.5 are refused before it. The four corners use the section's overall width and depth.

  • connections.js
  • lines 362 to 380
  • DetailCraft 0.8.1
const cosA = dot(dir, n);                                                    // how square the beam meets the face
if (cosA < 0.5) return { problems: [t('conn.err.st-too-skewed')] };
const problems = [];
// the beam's own axes at this end (its frame), pointing away from the column
const bf = B.frame;
const U = bf.u, V = bf.v;                                  // the beam's own section axes (a channel's web stays on −u)
// cut-back: every corner of the beam's end section clear of the face by gap
const w2 = bp.width / 2, d2 = bp.depth / 2;
let setback = 0;
for (const [u, v] of [[-w2, -d2], [w2, -d2], [w2, d2], [-w2, d2]]) {
  const off = dot(add(mul(U, u), mul(V, v + B.voff)), n);
  setback = Math.max(setback, (faceOff + p.gap - off) / cosA);
}
const at = (s, u, v) => add(N, add(mul(dir, s), add(mul(U, u), mul(V, v + B.voff))));
// the tab: against the web on +u, from the face (its nearest corner) out by width, centered on the bolt group
const uWeb = bp.kind === 'C' ? -bp.bf / 2 + bp.tw : bp.tw / 2;              // the web face the tab bears on (a channel's inside)
const uMid = uWeb + p.tp / 2;
let sFace = 0;
for (const u of [uWeb, uWeb + p.tp]) sFace = Math.max(sFace, (faceOff - dot(mul(U, u), n)) / cosA);

Shear tab: plate and bolt row

64-bit

The tab's two end points, its rotation, and the position of each bolt in the row.

length = (rows − 1)·pitch + 2·edge; v_i = ev + ((rows − 1) ÷ 2 − i)·pitch; s_bolt = s_face + eh

Points are sums along the beam's own axes from the joint. The rotation comes from Math.atan2 of two dot products with a frame rebuilt from the plate's own end points.

  • connections.js
  • lines 382 to 397
  • DetailCraft 0.8.1
const vc = p.ev, len_ = (rows - 1) * p.pitch + 2 * p.edge;
const sMid = sFace + p.width / 2, sBolt = sFace + p.eh;
const c0 = at(sMid, uMid, vc - len_ / 2), c1 = at(sMid, uMid, vc + len_ / 2);
// plate frame: length along the beam's v; its thickness (the plate section's v) along the beam's u
const f0 = frame(c0, c1, 0);
const rot = Math.atan2(-dot(U, f0.u), dot(U, f0.v)) * 180 / Math.PI;
const plate = { key: conn.id + '.plate', role: t('conn.role.sheartab'), profileName: 'PL' + num(p.tp) + 'x' + num(p.width), material: p.plateGrade || 'A36',
  start: c0, end: c1, rotation: rot, position: 'middle', attachTo: conn.column };
const holes = [], bolts = [];
const grip = p.tp + bp.tw, boltLen = ceil5(grip + p.allowance);
for (let i = 0; i < rows; i++) {
  const v = vc + ((rows - 1) / 2 - i) * p.pitch;
  holes.push({ target: conn.beam, world: at(sBolt, uWeb - bp.tw / 2, v), face: 'web', d: p.hole });
  holes.push({ target: plate.key, world: at(sBolt, uMid, v), face: 'plate', d: p.hole });
  bolts.push({ conn: conn.id, d: p.boltD, length: boltLen, grip, grade: p.grade, head: at(sBolt, uWeb + p.tp, v), axis: mul(U, -1), kind: 'BOLT' });
}

Shear tab: skew and slope

64-bit

The beam's skew in plan and its slope, reported with the connection.

skew = acos(min(1, away·n)); slope = asin(min(1, |dir_z|))

Both arguments are clamped at 1 before the inverse function. The lower end is not clamped; the earlier cos A ≥ 0.5 test keeps the skew argument positive.

  • connections.js
  • line 408
  • DetailCraft 0.8.1
const skew = Math.acos(Math.min(1, dot(awayH, n))) * 180 / Math.PI, slope = Math.asin(Math.min(1, Math.abs(dir[2]))) * 180 / Math.PI;

Base plate and anchor rods

64-bit

The base plate's position under the column and the four rod positions and lengths.

rod = B ± ĉ_v·sx ÷ 2 ± ĉ_u·sy ÷ 2; rod length = embed + grout + tp + projection

The column's two section axes are flattened to the plan and normalized again. Rod length is a plain sum of the parameters, not rounded.

  • connections.js
  • lines 427 to 440
  • DetailCraft 0.8.1
const cv = norm([fc.v[0], fc.v[1], 0]), cu = norm([fc.u[0], fc.u[1], 0]);
const zc = Bn[2] + p.tp / 2;
const s = add(add(Bn, mul(cv, -p.length / 2)), [0, 0, p.tp / 2]), e = add(add(Bn, mul(cv, p.length / 2)), [0, 0, p.tp / 2]);
// plate x runs along the column depth; its u must be the column's u (or −u): rotation 0 gives u = Z × x
const plate = { key: conn.id + '.plate', role: t('conn.role.baseplate'), profileName: 'PL' + num(p.tp) + 'x' + num(p.width), material: p.plateGrade || 'A36',
  start: [s[0], s[1], zc], end: [e[0], e[1], zc], rotation: 0, position: 'middle', attachTo: conn.column };
const holes = [], bolts = [];
const grout = p.grout || 0;
const rodLen = p.embed + grout + p.tp + p.projection;
for (const a of [-1, 1]) for (const b of [-1, 1]) {
  const pt = add(add(Bn, mul(cv, a * p.sx / 2)), mul(cu, b * p.sy / 2));
  holes.push({ target: plate.key, world: [pt[0], pt[1], zc], face: 'plate', d: p.hole });
  bolts.push({ conn: conn.id, d: p.rodD, length: rodLen, grip: p.tp, grade: p.grade, head: [pt[0], pt[1], Bn[2] + p.tp + p.projection], axis: [0, 0, -1], kind: 'ANCHORBOLT' });
}

Plate washers on the rods

64-bit

The size, thickness and position of each plate washer and the hole in it.

hole = rod + 25.4 ÷ 16; z = top of plate + thickness ÷ 2

The washer's hole is the rod diameter plus 1/16 inch written as 25.4 ÷ 16, which is exact. The washer's height uses the unrounded thickness, but the washer part itself is made from a name with one decimal, so a 6.35 mm washer is built 6.3 mm thick.

  • connections.js
  • lines 472 to 480
  • DetailCraft 0.8.1
const ws = p.washerSize || row?.washer, wt = p.washerT || row?.washerT;
if (!ws || !wt) problems.push(t('conn.warn.washer-size'));
else {
  bolts.forEach((bt, i) => {
    const c0 = [bt.head[0], bt.head[1]], z = top + wt / 2;
    const w = { key: conn.id + '.w' + (i + 1), role: t('conn.role.washer'), profileName: 'PL' + num(wt) + 'x' + num(ws), material: p.plateGrade || 'A36',
      start: [c0[0] - cv[0] * ws / 2, c0[1] - cv[1] * ws / 2, z], end: [c0[0] + cv[0] * ws / 2, c0[1] + cv[1] * ws / 2, z], rotation: 0, position: 'middle', attachTo: conn.column, loose: true };
    plates.push(w);
    holes.push({ target: w.key, world: [c0[0], c0[1], z], face: 'plate', d: p.rodD + 25.4 / 16 });

Splice: joint middle and cut-backs

64-bit

The middle of the joint between two spliced members and how far each is cut back to leave the joint gap.

mid = J + out·(dJ·out) ÷ 2; setback = max(0, gap ÷ 2 − (dJ·out) ÷ 2)

Both members get the same cut-back, half the difference between the wanted gap and the drawn gap, never negative.

  • connections.js
  • lines 511 to 514
  • DetailCraft 0.8.1
const mid = add(J, mul(out_, dot(dJ, out_) / 2));                                 // the middle of the joint
const W = (s, u, v) => add(mid, add(mul(out_, s), add(mul(U, u), mul(V, v + A.voff))));
const d2 = ap.d / 2, gap = p.jointGap;
const setbacks = [{ member: conn.a, end: conn.aEnd, by: Math.max(0, gap / 2 - dot(dJ, out_) / 2) }, { member: conn.b, end: bNear, by: Math.max(0, gap / 2 - dot(dJ, out_) / 2) }];

Splice: flange plates and bolts

64-bit

The length of the flange cover plates and the position of every flange bolt.

L_F = gap + 2·(2·edge + (n − 1)·pitch); s = ±(gap ÷ 2 + edge + i·pitch); gauge = bf − 80 when not given

Bolt stations are mirrored about the joint middle. When no gauge is given it is the flange width minus a fixed 80 mm.

  • connections.js
  • lines 526 to 539
  • DetailCraft 0.8.1
const nF = Math.max(1, Math.round(p.flangeRows));
const LF = gap + 2 * (2 * p.flangeEdge + (nF - 1) * p.flangePitch);
const gF = p.flangeGauge || ap.bf - 80;
for (const sg of [1, -1]) {
  const pl = plateAlong(sg > 0 ? 'ft' : 'fb', t(sg > 0 ? 'conn.role.sp-flange-top' : 'conn.role.sp-flange-bottom'), p.flangeT, ap.bf, LF, 0, sg * (d2 + p.flangeT / 2), U);
  const grip = ap.tf + p.flangeT, bl = ceil5(grip + p.allowance);
  for (const side of [-1, 1]) for (let i = 0; i < nF; i++) for (const gu of [-gF / 2, gF / 2]) {
    const sx = side * (gap / 2 + p.flangeEdge + i * p.flangePitch);
    const memberId = side < 0 ? conn.a : conn.b, part = side < 0 ? A : B;
    holes.push({ target: memberId, world: W(sx, gu, sg * (d2 - ap.tf / 2)), face: faceOf(part, mul(V, sg)), d: p.hole });
    holes.push({ target: pl.key, world: W(sx, gu, sg * (d2 + p.flangeT / 2)), face: 'plate', d: p.hole });
    bolts.push({ conn: conn.id, d: p.boltD, length: bl, grip, grade: p.grade, head: W(sx, gu, sg * (d2 + p.flangeT)), axis: mul(V, -sg), kind: 'BOLT' });
  }
}

Splice: web plates and bolts

64-bit

The height and length of the web cover plates and the position of every web bolt.

clear = d − 2·(tf + r); H_W = ⌊(clear − 20) ÷ 5⌋·5 when not given; L_W = gap + 2·(2·edge + (cols − 1)·pitch_x)

The default plate height is rounded down to 5 mm on purpose. Rows are centered on the web.

  • connections.js
  • lines 541 to 553
  • DetailCraft 0.8.1
const clear = ap.d - 2 * (ap.tf + (ap.r || 0));
const nW = Math.max(1, Math.round(p.webRows)), cW = Math.max(1, Math.round(p.webCols));
const HW = p.webH || Math.floor((clear - 20) / 5) * 5;
const LW = gap + 2 * (2 * p.webEdge + (cW - 1) * p.webPitchX);
const wps = [];
for (const sg of [1, -1]) wps.push(plateAlong(sg > 0 ? 'w1' : 'w2', t('conn.role.sp-web'), p.webT, HW, LW, sg * (ap.tw / 2 + p.webT / 2), 0, V));
const gripW = ap.tw + 2 * p.webT, blW = ceil5(gripW + p.allowance);
for (const side of [-1, 1]) for (let j = 0; j < cW; j++) for (let i = 0; i < nW; i++) {
  const sx = side * (gap / 2 + p.webEdge + j * p.webPitchX), v = ((nW - 1) / 2 - i) * p.webPitch;
  holes.push({ target: side < 0 ? conn.a : conn.b, world: W(sx, 0, v), face: 'web', d: p.hole });
  for (const [k, pl] of wps.entries()) holes.push({ target: pl.key, world: W(sx, (k === 0 ? 1 : -1) * (ap.tw / 2 + p.webT / 2), v), face: 'plate', d: p.hole });
  bolts.push({ conn: conn.id, d: p.boltD, length: blW, grip: gripW, grade: p.grade, head: W(sx, ap.tw / 2 + p.webT, v), axis: mul(U, -1), kind: 'BOLT' });
}

Gusset: plane of the plate

64-bit

The gusset's normal and the direction across it, from the brace and the support.

n_P = unit(d × x̂_S); w = unit(n_P × x̂_S)

A brace nearly along its support is refused when the cross product is shorter than 0.2 (under about 11.5 degrees), so the normalization never divides by a small number.

  • connections.js
  • lines 581 to 585
  • DetailCraft 0.8.1
const cr = [d[1] * sx[2] - d[2] * sx[1], d[2] * sx[0] - d[0] * sx[2], d[0] * sx[1] - d[1] * sx[0]];
if (len(cr) < 0.2) return { problems: [t('conn.err.gu-along')] };
const nP = norm(cr);                                                     // the gusset's normal
let wv = norm([nP[1] * sx[2] - nP[2] * sx[1], nP[2] * sx[0] - nP[0] * sx[2], nP[0] * sx[1] - nP[1] * sx[0]]);
if (dot(wv, d) < 0) wv = mul(wv, -1);                                    // across the gusset, away from the support

Gusset: brace cut-back

64-bit

How far the brace is cut back to clear every member at the joint and a base plate there.

need = max over corners of (off + gap − c·n) ÷ (d·n); setback = largest need

A face the brace runs nearly along (d·n < 0.05) is not used, which keeps the division away from zero. The clearance direction is the part of the brace direction square to the other member, normalized when longer than 1e-6.

  • connections.js
  • lines 614 to 630
  • DetailCraft 0.8.1
const need = (n, off) => { const dn = dot(d, n); if (dn < 0.05) return Infinity; return Math.max(0, ...corners.map((c) => (off + p.braceGap - dot(c, n)) / dn)); };
for (const X of others) {
  const xa = X.frame.x, nl = sub(d, mul(xa, dot(d, xa)));
  const xc = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([a, b]) => add(mul(X.frame.u, a * X.profile.width / 2), mul(X.frame.v, b * X.profile.depth / 2 + X.voff)));
  let sX = Infinity;
  if (len(nl) > 1e-6) { const n = norm(nl); sX = need(n, Math.max(...xc.map((c) => dot(c, n)))); }
  for (const e of ['start', 'end']) {                                      // or past the member's end, if it ends here
    if (len(sub(X.m[e], N)) > TOL) continue;
    const ax = e === 'end' ? xa : mul(xa, -1);
    sX = Math.min(sX, need(ax, 0));
  }
  if (Number.isFinite(sX)) setback = Math.max(setback, sX);
}
const base = (ctx.conns || []).find((c) => c.type === 'baseplate' && c.column === S.m.id);
const sBottom = S.m.start[2] <= S.m.end[2] ? 'start' : 'end';
const onBase = base && len(sub(S.m[sBottom], N)) <= TOL;
if (onBase) setback = Math.max(setback, need([0, 0, 1], base.params.tp));

Gusset: bolt line

64-bit

The position of each bolt along the brace and its matching hole in the gusset.

s_k = setback + edge + k·pitch; Q = P + n_P·(o − (P − N)·n_P)

Each hole in the brace is projected onto the gusset's mid plane by one dot product and one scaled addition.

  • connections.js
  • lines 640 to 649
  • DetailCraft 0.8.1
const away = Math.sign(o - dot(at(0, hole.u, hole.v), nP)) || 1;     // from the brace toward the gusset
const grip = legT + tg, bl = ceil5(grip + p.allowance);
for (let k = 0; k < n; k++) {
  const sK = setback + p.braceEdge + k * p.bracePitch;
  const P = at(sK, hole.u, hole.v), Q = onPlane(P);
  holes.push({ target: conn.brace, world: P, face: hole.face, d: p.hole });
  holes.push({ target: gkey, world: Q, face: 'plate', d: p.hole });
  bolts.push({ conn: conn.id, d: p.boltD, length: bl, grip, grade: p.grade, head: add(Q, mul(nP, -away * (tg / 2 + legT))), axis: mul(nP, away), kind: 'BOLT' });
  pts.push(G(Q));
}

Gusset: plate size

64-bit

How far the gusset runs along and across its support, and the width in its profile name.

a0 = min a − margin, a1 = max a + margin; width = round(w1 − w0)

The plate's width is rounded to a whole millimeter with Math.round before it goes into the profile name, while the plate's center line uses the unrounded (w0 + w1) ÷ 2. The fit checks use 1e-6 mm.

  • connections.js
  • lines 658 to 684
  • DetailCraft 0.8.1
const w0 = Math.max(...sc.map((c) => dot(c, wv)));
// margin round what the gusset carries: the bolts' end distance; for a tube, room for the welds where it leaves the slot
const mg = mode === 'bolt' ? p.braceEdge : 2 * p.weld + 10;
let a0 = Math.min(...pts.map((q) => q[0])) - mg, a1 = Math.max(...pts.map((q) => q[0])) + mg;
const w1 = Math.max(...pts.map((q) => q[1])) + mg;
const need0 = Math.min(...pts.map((q) => q[0])), need1 = Math.max(...pts.map((q) => q[0]));
// stay on the support (above a base plate), and clear of a member crossing the joint (a beam over the gusset)
const sA = [dot(sub(S.m.start, N), sx), dot(sub(S.m.end, N), sx)];
let lo = Math.min(...sA), hi = Math.max(...sA);
if (onBase) { if (sBottom === 'start') lo = Math.max(lo, dot(sub(S.m.start, N), sx) + base.params.tp); else hi = Math.min(hi, dot(sub(S.m.end, N), sx) - base.params.tp); }
for (const X of others) {
  if (X === S || Math.abs(dot(X.frame.x, wv)) < 0.5) continue;
  const xs = [[-1, -1], [1, -1], [1, 1], [-1, 1]].map(([a, b]) => dot(add(add(sub(X.m.start, N), mul(X.frame.u, a * X.profile.width / 2)), mul(X.frame.v, b * X.profile.depth / 2 + X.voff)), sx));
  const xlo = Math.min(...xs), xhi = Math.max(...xs);
  if (need1 <= xlo) hi = Math.min(hi, xlo); else if (need0 >= xhi) lo = Math.max(lo, xhi);
}
// on a base plate the gusset sits down on it (welded to it as well as to the column)
if (onBase && sBottom === 'start' && dot(d, sx) > 0) a0 = lo;
if (onBase && sBottom === 'end' && dot(d, sx) < 0) a1 = hi;
a0 = Math.max(a0, lo); a1 = Math.min(a1, hi);
const room = mode === 'bolt' ? Math.min(p.braceEdge, 1.2 * p.hole) : p.weld;
if (a0 > need0 - room + 1e-6 || a1 < need1 + room - 1e-6 || a1 - a0 < 20) problems.push(t('conn.warn.gu-fit'));
if (a1 - a0 < 20) return { problems: [...problems, t('conn.err.gu-room')] };
const wm = (w0 + w1) / 2, Qa = (a) => add(N, add(mul(nP, o), add(mul(sx, a), mul(wv, wm))));
const c0 = Qa(a0), c1 = Qa(a1), f0 = frame(c0, c1, 0);
const rot = Math.atan2(dot(wv, f0.v), dot(wv, f0.u)) * 180 / Math.PI;
plates.push({ key: gkey, role: t('conn.role.gusset'), profileName: 'PL' + num(tg) + 'x' + num(Math.round(w1 - w0)), material: p.plateGrade || 'A36', start: c0, end: c1, rotation: rot, position: 'middle', attachTo: conn.support });

Mitre: where the two center lines meet

64-bit

The closest points of the two members' center lines and the joint point between them.

s_A = (b·e − d) ÷ (1 − b²), s_B = (e − b·d) ÷ (1 − b²); b = x̂_A·x̂_B, d = x̂_A·r, e = x̂_B·r; J = (P_A + P_B) ÷ 2

1 − b² subtracts nearly equal numbers when the members are close to parallel; the joint is refused when it is under 1e-8, and later when the members are within about 1.15 degrees of in line or under 15 degrees apart. The two closest points must be within 1 mm.

  • connections.js
  • lines 741 to 747
  • DetailCraft 0.8.1
const bxd = dot(xA, xB), den = 1 - bxd * bxd;
if (den < 1e-8) return { problems: [t('conn.err.mi-line')] };
const r = sub(oA, oB), da = dot(xA, r), db = dot(xB, r);
const sA = (bxd * db - da) / den, sB = (db - bxd * da) / den;
const PA = add(oA, mul(xA, sA)), PB = add(oB, mul(xB, sB));
if (len(sub(PA, PB)) > TOL) return { problems: [t('conn.err.mi-meet', { d: num(len(sub(PA, PB))) })] };
const J = mul(add(PA, PB), 0.5);

Mitre: cut plane

64-bit

The angle between the two members and the plane that halves it.

angle = acos(clamp(ê_A·ê_B, −1, 1)); n = unit(ê_B − ê_A); out = unit(−(ê_A + ê_B)); w = unit(ê_A × ê_B)

The dot product is clamped before Math.acos. With the angle held between 15 degrees and about 178.85 degrees, none of the three normalized vectors is close to zero length.

  • connections.js
  • lines 756 to 759
  • DetailCraft 0.8.1
const cosT = dot(eA, eB), angle = Math.acos(Math.max(-1, Math.min(1, cosT))) * 180 / Math.PI;
if (cosT < -0.9998) return { problems: [t('conn.err.mi-line')] };
if (angle < MITRE_MIN_ANGLE) return { problems: [t('conn.err.mi-sharp', { a: num(angle), min: MITRE_MIN_ANGLE })] };
const n = norm(sub(eB, eA)), out = norm(mul(add(eA, eB), -1)), w = norm(cross(eA, eB));

Mitre: the two cut faces

64-bit

The point each member's cut plane passes through, set back for the gap and the plates.

face_A = J − n·off, face_B = J + n·off; off = gap ÷ 2 + plate thickness

Two scaled additions from the joint point; nothing is rounded.

  • connections.js
  • lines 768 to 770
  • DetailCraft 0.8.1
const off = p.mGap / 2 + (plates ? p.tp : 0);
const faceA = sub(J, mul(n, off)), faceB = add(J, mul(n, off));
const planes = [{ member: conn.a, end: conn.aEnd, n, p: faceA }, { member: conn.b, end: conn.bEnd, n: mul(n, -1), p: faceB }];

Mitre: where a section edge meets the cut

64-bit

Where a line along the member through a point of its section crosses the cut plane, in the plane's own two directions.

k = n·(face − Q) ÷ (n·x̂); X = Q + x̂·k − face

The division by n·x̂ is not tested here; it is the cosine of the cut angle, which the 15 degree limit on the joint keeps at about 0.13 or more.

  • connections.js
  • lines 771 to 777
  • DetailCraft 0.8.1
// a member's section edge lines (u, v from its center) cut by its face: [along out, across w] from the face's point
const cutAt = (P, face, nn, u, v) => {
  const Q = add(add(P.frame.o, mul(P.frame.u, u)), mul(P.frame.v, v + P.voff));
  const k = dot(nn, sub(face, Q)) / dot(nn, P.frame.x);
  const X = sub(add(Q, mul(P.frame.x, k)), face);
  return [dot(X, out), dot(X, w)];
};

Mitre: bolt rows and gauge

64-bit

The rows of bolts between the flanges of the two end plates and their gauge.

rows spread from c0 + weld + d0 to c1 − weld − d0; gauge = max(⌈2.4·d0 ÷ 5⌉·5, round(width ÷ 2 ÷ 5)·5) when not given

With one row the position is the middle; otherwise rows are evenly spaced by (b − a) ÷ (rows − 1). The default gauge is rounded to 5 mm on purpose.

  • connections.js
  • lines 800 to 818
  • DetailCraft 0.8.1
const inner = (P, face, nn) => [-1, 1].map((sg) => cutAt(P, face, nn, 0, sg * (P.profile.depth / 2 - P.profile.tf))[0]);
const ia = inner(A, faceA, n), ib = inner(B, faceB, mul(n, -1));
const c0 = Math.max(Math.min(...ia), Math.min(...ib)), c1 = Math.min(Math.max(...ia), Math.max(...ib));   // clear of both members' flanges
const rows = Math.max(1, Math.round(p.rows));
const ts = [];
if (p.mPitch > 0) for (let i = 0; i < rows; i++) ts.push((c0 + c1) / 2 + ((rows - 1) / 2 - i) * p.mPitch);
else {
  const a = c0 + p.weld + d0, b = c1 - p.weld - d0;
  for (let i = 0; i < rows; i++) ts.push(rows === 1 ? (a + b) / 2 : a + i * (b - a) / (rows - 1));
}
if (p.outRow) ts.push(o1 + p.extOuter / 2);
const g = p.mGauge || Math.max(Math.ceil(2.4 * d0 / 5) * 5, Math.round(width / 2 / 5) * 5);
const grip = 2 * p.tp + p.mGap, boltLen = ceil5(grip + p.allowance);
for (const tt of ts) for (const sg of [1, -1]) {
  const M = add(J, add(mul(out, tt), mul(w, wm + sg * g / 2)));
  out_.holes.push({ target: pa.key, world: sub(M, mul(n, p.mGap / 2 + p.tp / 2)), face: 'plate', d: d0 });
  out_.holes.push({ target: pb.key, world: add(M, mul(n, p.mGap / 2 + p.tp / 2)), face: 'plate', d: d0 });
  out_.bolts.push({ conn: conn.id, d: p.boltD, length: boltLen, grip, grade: p.grade, head: sub(M, mul(n, p.mGap / 2 + p.tp)), axis: n, kind: 'BOLT' });
}

Mitre: weld length round the cut section

64-bit

The length of the all-round weld between a member's slanted end and its plate.

length = perimeter ÷ |n·x̂|

The whole section perimeter is divided by the cosine of the cut angle. On a slanted cut only the edges that run in the direction of the slant get longer, so this is an upper estimate, not the exact outline length of the cut face.

  • connections.js
  • lines 820 to 821
  • DetailCraft 0.8.1
const cosX = Math.abs(dot(n, P.frame.x));
out_.welds.push({ conn: conn.id, kind: 'fillet', size: p.weld, between: [P.m.id, key], length: P.profile.perimeter / cosX, where: t('conn.weld.mitre'), allRound: true });

Mitre: stiffener position and size

64-bit

Where the pair of web stiffeners sits past the cut and how big each is.

at = far + dist + t ÷ 2; height = d − 2·tf; width = (bf − tw) ÷ 2

The far end of the cut is the largest of the four section corners projected along the member. The room check allows 1e-6 mm. The stiffener width goes into a profile name with one decimal.

  • connections.js
  • lines 841 to 850
  • DetailCraft 0.8.1
const far = Math.max(...corners(P).map(([u, v]) => {
  const Q = add(add(P.frame.o, mul(P.frame.u, u)), mul(P.frame.v, v + P.voff));
  const k = dot(nn, sub(face, Q)) / dot(nn, P.frame.x);
  return dot(sub(add(Q, mul(P.frame.x, k)), J), e);
}));
const at = far + p.stiffDist + p.stiffT / 2;
const reachFar = dot(sub(add(o, mul(P.frame.x, end === 'start' ? P.frame.len : 0)), J), e);   // the member's other end, from the joint
if (at + p.stiffT / 2 > reachFar - 1e-6) { problems.push(t('conn.warn.mi-stiff-room')); continue; }
const hS = pr.d - 2 * pr.tf, wS = (pr.bf - pr.tw) / 2;
const C = add(J, mul(e, at));

In detail

Method
Closed-form placement in each member's frame: face offsets, bolt rows at a pitch about a center, cut-backs from the corners of the end section, mitre planes on the bisector of the two members.
Number types
64-bit numbers in plain arrays; plate sizes pass through text with one decimal.
Tolerances
1 mm on joint position; cosine limits 0.9999, 0.99, 0.5 and 0.05 on directions; 1e-6 mm on several fit checks; 1e-9 on the round-up to 5 mm; mitre refused when 1 − cos² < 1e-8 or the angle is under 15 degrees.
Limits
No strength is checked, as the file header states. Spacing limits such as 2.2 × d0 are computed in binary, where 2.2 × 22 evaluates to 48.400000000000006, so a pitch of exactly 48.4 mm with a 22 mm hole is reported as under the minimum.

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