Skip to content

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.

ModuleRole
library.jsThe 11 templates as data; the declared appliesTo envelope; applyTemplate / seedGeometry; ply-floor derivations
domain.jscheckDomain / templateDomain; refusal construction + remedy wording (mmUp / mmDown, twoColumnTerms, TWO_COLUMN_REFUSAL)
layout3d.jsconnectionLayout: geometry → placed primitives; per-connection infeasibility (boltHalfGauge, infeasibleTwoColumns)
generate.jsSolved-model topology → connections; resolves end forces, roles, class; isSupport
classify.jsDeterministic joint classification from topology + forces
recipes.jsParametric sizing of a template to a section; catalogue cross-multiplication
closedform.jsClass → WASM check-name mapping, input building, result normalisation
solver.jsBackend dispatch (closedForm | cbfem) writing conn.result
boltrules.jsThe 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.jsenforceDetailingMinima: raises pitch/edge to the minima, notes-only
detailaudit.jsBuildability audit over layout primitives (below)
mindesign.jsAS 4100 Cl 9.1.4 minimum design actions (0.5 φMs / min(0.15 φVv, 40 kN) / 0.3 φN)
memberstub.jsMember → shell plates (web + flanges + welds) in mesher form
connfea.js, jointmodel.js, jointfea.jsLayout → ConnFeaInput for the Rust mesher/solver
cbfemmesh.js, cbfemsolve.jsThe older 2D plane-stress CBFEM (superseded in role by conn_fea, still a backend)
gussetgeom.js, gussetangles.js, sweepmatrix.jsGusset plane maths; the gusset angle population; the shared 198-combination sweep
autosize.jsIterates bolt dia/count, plate t, weld leg to a passing utilisation
operations.jsFabrication operations list
svg.js, svgiso.js, platedraw.js, dxf.js, ifc.js, worksheet.js, calctrace.js, setout.js, shopnotes.js, takeoff.jsDrawings, 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 clearance Wweb = tw + 4·minEdge (not a catalogue fact, so not declared);
  • declared ply floor declaredPlyFloor (bolt-driven) vs. the per-connection plyFloorFor(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.js itself) 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.mjs runs the audit over the 198-combination matrix (11 templates × 3 section tiers × 6 bolt diameters) and compares against library_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 --update a red run green.
  • Rendering is pinned as text where possible. template_svg_check.mjs pins the isometric SVG of all 11 templates; template_gallery_check.mjs is 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::applyTemplateId vs library.js::applyTemplate);
  • the gusset angle thresholds recorded in docs/gusset-angle-finding.md;
  • the fate of the now-dead buildJointGroup renderer.