Appearance
Connections engine
Architecture and contributing notes for the steel-connections system (@civilkit/connections plus the Connection Designer feature in the Studio). For what the feature does, see the user guide; this page is about how it's built and the conventions its tests enforce.
One layout stream, five readers
The central design decision: layout3d.js::connectionLayout(conn) turns conn.geometry into placed primitives (plates, bolts, welds, member stubs, pedestals) in a local joint frame, and everything downstream reads that one stream:
jointsolid.js- the 3D Model tab solids (one THREE solid per primitive, no per-template branches);svgiso.js/svg.js- isometric and 2D detail drawings;dxf.js- DXF export;connfea.js/jointmodel.js- the FEA mesher input (ConnFeaInput);detailaudit.js- the buildability audit.
Consequence: if a template looks wrong, the layout is wrong - never correct it in a view. This replaced an earlier renderer that drew one generic topology per template from conn.geometry and was structurally wrong for 7 of 11 templates.
Module map (packages/connections/)
Pure JS, SI units (metres, pascals), no DOM. The engineering capacities live in Rust/WASM; JS builds check inputs and normalises results.
| Module | Role |
|---|---|
library.js | The 11 templates as data; the declared appliesTo envelope; applyTemplate / seedGeometry; ply-floor derivations |
domain.js | checkDomain / templateDomain; refusal construction + remedy wording (mmUp / mmDown, twoColumnTerms, TWO_COLUMN_REFUSAL) |
layout3d.js | connectionLayout: geometry → placed primitives; per-connection infeasibility (boltHalfGauge, infeasibleTwoColumns) |
generate.js | Solved-model topology → connections; resolves end forces, roles, class; isSupport |
classify.js | Deterministic joint classification from topology + forces |
recipes.js | Parametric sizing of a template to a section; catalogue cross-multiplication |
closedform.js | Class → WASM check-name mapping, input building, result normalisation |
solver.js | Backend dispatch (closedForm | cbfem) writing conn.result |
boltrules.js | The one home of the AS 4100:2020 spacing/edge rules (minPitch 2.5df, minEdge 1.5df, maxPitch 15t, maxEdge 12t); a second standard is intended to be an adapter here |
detailing.js | enforceDetailingMinima: raises pitch/edge to the minima, notes-only |
detailaudit.js | Buildability audit over layout primitives (below) |
mindesign.js | AS 4100 Cl 9.1.4 minimum design actions (0.5 φMs / min(0.15 φVv, 40 kN) / 0.3 φN) |
memberstub.js | Member → shell plates (web + flanges + welds) in mesher form |
connfea.js, jointmodel.js, jointfea.js | Layout → ConnFeaInput for the Rust mesher/solver |
cbfemmesh.js, cbfemsolve.js | The older 2D plane-stress CBFEM (superseded in role by conn_fea, still a backend) |
gussetgeom.js, gussetangles.js, sweepmatrix.js | Gusset plane maths; the gusset angle population; the shared 198-combination sweep |
autosize.js | Iterates bolt dia/count, plate t, weld leg to a passing utilisation |
operations.js | Fabrication operations list |
svg.js, svgiso.js, platedraw.js, dxf.js, ifc.js, worksheet.js, calctrace.js, setout.js, shopnotes.js, takeoff.js | Drawings, calc documents, fabrication output, estimating |
Studio side (web_demo/studio/features/connections/): designerworkspace.js (the workspace: single ACTIONS table, panels, staleness, refusal surfacing), jointsolid.js (Model tab), jointview.js (3D pane), connectionsdesign.js (model-level auto-design / apply / recheck), connectionrender.js (viewport host), wizardtiles.js (wizard tiles). Rust side: rust_solver/fem-core/src/conn_fea.rs (mesh + elastic solve + weld ties + bolt chains), exposed via fem-wasm as conn_fea_mesh / conn_fea_solve, run in the existing solver worker.
The declared validity envelope
Each template declares the domain it is valid in (appliesTo): bolt diameter range, a ply-thickness floor derived by inverting AS 4100 Cl 9.5.3/9.5.4 through boltrules.js at the template's own default pitch/edge, a two-column flange-width requirement (minPitch + 2×minEdge on a declared ply), and a named alternative template.
The discipline that shaped it, and the one to keep: every bound is derived from the engineering rule, never fitted to what the code currently draws. When declarations and implementation disagree, fix geometry - not the declaration. Two deliberately separate term pairs:
- declared bolt-driven width
W2 = minPitch + 2·minEdge(a catalogue fact) vs. the member-dependent web clearanceWweb = tw + 4·minEdge(not a catalogue fact, so not declared); - declared ply floor
declaredPlyFloor(bolt-driven) vs. the per-connectionplyFloorFor(dia, pitch, edge)applied at layout time.
Refusal semantics
checkDomain returns every applicable refusal, each carrying a remedy as data (not prose), where the remedy quotes a number that works when followed to the letter (round the safe direction: mmUp for widths to build to, mmDown for ceilings). A refused joint emits no geometry, does not solve, and keeps no verdict or detailing pass.
Two refusal families exist, and their surfacing is currently split:
- Declared refusals (from
checkDomain) reach all surfaces: Checks panel, Model/Mesh tabs, schedule, report, operations rail, FEA gating. - Layout refusals (per-connection infeasibility found by
layout3d.jsitself) currently gate only the analysis path (jointmodel.js,connfea.js); the designer can still display such a joint as solvable. Most of the surviving sweep-baseline lines are this class. Known gap - fix before extending the refusal vocabulary.
Detailing audit
detailaudit.js::auditDetail is a pure audit over layout primitives: bolt groups asymmetric about their own gauge line (per-member memberTag / gaugeDir tagging), bolts gripping fewer than two plies or with short shanks, plates overhanging their member, coincident plies, orphan plates, spacing/edge violations vs boltrules.js, bolts running along a web, stubs under 1.2 section depths. Known blind spots (recorded, unfixed): no cross-member bolt-proximity check, and auto-generated moment connections carry no pitch/gauge/edge on their bolt group so the spacing checks are inert for them.
Testing conventions
These are load-bearing conventions - follow them when adding to the engine:
- Gates fail in both directions. A gate that can only fail red can be silently emptied; e.g.
SILENT_BLANK_KNOWN = {gusset}fails if another template starts rendering blank or if gusset stops being blank without the constant being revisited. - Instruments are not gates. Measurement tools (
domain_measure.mjs,gusset_angle_measure.mjs) exit 0 always and are never named*.test.mjs; they measure a population before any gate is tightened. The domain work recorded 46 disagreements before any was resolved. - The sweep is a ratchet.
library_sweep.test.mjsruns the audit over the 198-combination matrix (11 templates × 3 section tiers × 6 bolt diameters) and compares againstlibrary_sweep.baseline.txt(currently 23 known failures, each attributed to a cause class). It fails if the count goes up or down without the baseline being deliberately updated - never--updatea red run green. - Rendering is pinned as text where possible.
template_svg_check.mjspins the isometric SVG of all 11 templates;template_gallery_check.mjsis the GL screenshot gate over the Model tab (42 checks, all 11 templates, no exclusion list). - A control fixture must not be able to pass on zeros. The Cl 9.1.4 minimum-design-actions module exists in tests because a destructuring bug once designed every generated joint against zero actions - and the FEA "converged" with every number zero.
- Convergence ≠ correctness. Four buildability faults once shipped inside reports where every joint converged to 1e-10. Capacity checks, the detailing audit and the FEA are three instruments answering three different questions; keep them separate.
CBFEM engine notes
Bolts are Timoshenko beam chains (one per ply pair) acting between tributary-weighted hole-rim means; a plain 3-direction spring is provably wrong (it fails equilibrium on a lap splice). Tension stiffness uses the elongation length (grip + ~0.8d) - deliberately not the drawn grip. Welds are rigid node-to-surface ties that state whose edge they own. sigma_vm_max_joint excludes load/support regions grown by 1.5× their half-extent; convergence is measured on the loaded member's end-face mean, never u_max.
Known open issue: a six-bolt end plate reports a non-physical joint peak stress (~37× fy) with sensible bolt forces - forces right, stress field wrong; expected to be addressed when contact lands. The FEA is verified: false until checked against a published benchmark (IDEA StatiCa verification examples or AISC worked joints).
Open owner decisions
Do not treat these as resolved (see docs/tasks/NEXT-SESSION.md and docs/connection-detailing-rules.md for the current state):
- the Domain shape has no bolt-row-depth or member-web-term fields;
- the layout-refusal surfacing gap (above);
- two independent, both-destructive implementations of template-apply (
designerworkspace.js::applyTemplateIdvslibrary.js::applyTemplate); - the gusset angle thresholds recorded in
docs/gusset-angle-finding.md; - the fate of the now-dead
buildJointGrouprenderer.