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