Skip to content

[4] Basic-mode interaction overhaul + PRO connectors / eccentric connection (Phase B)#51

Merged
diegokingston merged 69 commits into
mainfrom
pr/5-basic-mode-overhaul
Jun 11, 2026
Merged

[4] Basic-mode interaction overhaul + PRO connectors / eccentric connection (Phase B)#51
diegokingston merged 69 commits into
mainfrom
pr/5-basic-mode-overhaul

Conversation

@Batuis

@Batuis Batuis commented May 29, 2026

Copy link
Copy Markdown
Collaborator

Stack: [1]#45 <- [2]#47 <- [3]#49 <- [4]this.

Includes:

  • Basic mode interaction fixes: node-tool / select-tool / auto-split / node-snap precedence.
  • PRO Constraints: ConnectorElement UI + EccentricConnection (Phase B end-to-end).
  • Inherits the My/Mz axis-identity fix from pr/3 + pr/4.

Validation on local pr/5 (before opening):

  • 2155 tests passed / 5 skipped (72 files).
  • npm run build: success.
  • dev server at http://127.0.0.1:4000/ serving this branch.

Known engine CI red across the stack: harmonic_3d_5x5_plate_under_15s (solver/engine perf gate, 33.8s vs 30s) — out of scope for this PR.

Batuis added 30 commits May 29, 2026 13:30
Add a Design Summary section at the beginning of the PRO report (before
Model Data) when verification results exist:

- Overall pass/fail/warn status counts in a highlighted bar
- Design code reference
- Member Utilization Summary table sorted by utilization (most critical first):
  Element ID, Type, Section, Governing Check, Utilization %, Status
- Load combinations reference note

This is the "executive summary" that lets an engineer immediately see which
elements are critical and what the overall structural status is.

i18n: added 20+ report keys for en and es (designSummary, utilizationSummary,
governingCheck, flexure, shear, axialFlexure, beam, column, wall, etc.)
New example in the PRO Examples menu under "Energy & Offshore":
- 4-leg battered jacket (30m base → 18m top, 60m tall)
- X-bracing on all 4 faces across 6 bays (48 diagonal braces)
- Horizontal framing at 7 levels
- 4 central conductors
- Topsides deck at z=65m (24×24m, cantilevered beyond jacket)
- Flare boom arm with diagonal brace
- 6 steel sections (CHS legs, braces, horizontals, conductors + W-shape deck)
- 4 load cases (dead, live, wind, wave/current)
- 6 LRFD combinations

46 nodes, 121 elements, 8 supports, 20 loads.
Inspired by North Sea jacket platform reference.
Rebuilt the offshore example with much higher structural detail:

Jacket (80m tall, 8 bays):
- 4 battered legs (40m base → 20m top footprint)
- 64 X-braces (2 per face × 4 faces × 8 bays)
- 36 horizontal ring members (9 levels)
- 10 plan bracing members (every other level)
- 4 pile guide legs with mudline horizontal ring and cross bracing

Conductors (central core):
- 4 conductors segmented at 5 levels (0, 20, 40, 60, 80m)
- Horizontal tie rings and cross bracing at each conductor level
- Connected to jacket legs at matching levels

Topsides (3 deck levels: 82, 87, 92m):
- 24 deck nodes with perimeter and cross beams
- 16 inter-deck columns with diagonal bracing
- 4 transition columns from jacket top

Drilling derrick (92-122m):
- 4-leg lattice tower tapering upward
- Horizontal rings at 3 levels, X-bracing between levels

Flare boom: 3-member arm from deck to (52, -16, 100) with braces
Pedestal crane: column + arm to (-32, 12, 98) connected to deck

103 nodes, 316 elements, 12 supports, 50 loads.
All fixture validation tests pass (solves correctly).
Rebuilt jacket topology to match the reference image:

Jacket perimeter (12 vertical legs):
- 4 main corner legs (CHS 1200×40) at ±22m base → ±11m top
- 8 intermediate perimeter legs (CHS 900×30), 2 per face at 1/3 and 2/3
- Each face now has 4 vertical lines creating 3 bracing panels per face
- Total jacket height: 90m (z=-5 to z=85), 9 bays

Bracing density:
- 216 face X-braces (3 panels × 4 faces × 9 bays × 2 diagonals)
- 120 horizontal ring members (12 segments per level × 10 levels)
- 10 plan cross-braces at alternating levels
- 32 internal diagonal braces (corners to opposite-face intermediates)

Conductor core (8 conductors):
- 4 outer + 4 inner conductors at ±3m and ±1m offsets
- Segmented at 5 levels with horizontal ties and cross bracing
- Connected to jacket corner legs at matching levels

Topsides (3 levels: 87, 92, 97m):
- 8 nodes per deck (corners + mid-edges), cantilevered 15m
- 8 transition columns from jacket top (corners + intermediates)
- Full perimeter + cross beam framing per level

Drilling derrick: tapered 4-leg lattice (97-127m)
Flare boom: 3-segment arm to (58, -22, 100m)
Pedestal crane: column + arm to (-35, 13, 103m)

203 nodes, 668 elements, 20 supports, 74 loads.
Replaced the chaotic internal diagonals with structured internal planes:

Old pattern (wrong):
- Corners connected to opposite-face intermediates with ad hoc diagonals
- Created visually noisy crossing lines with no clear structural logic
- 32 arbitrary internal braces at 4 scattered levels

New pattern (correct):
- 2 orthogonal internal planes connecting parallel perimeter legs:
  - N-S plane: south intermediates ↔ north intermediates
  - E-W plane: east intermediates ↔ west intermediates
- Horizontal ties on both planes at every level (40 members)
- X-bracing on both planes between bays (72 members, 4 panels × 9 bays × 2 diags)
- Internal structure reads as 2 clear internal frames, not a web

The internal bracing now follows the same bay-by-bay X-brace logic
as the perimeter faces, just on the internal planes connecting
opposing parallel vertical lines.

203 nodes, 748 elements (+80 from v3), 20 supports, 74 loads.
…deck

Offshore example:
- Removed elements 740-748 (flare boom + crane) and their orphaned nodes
- Cleaned up floating node 203 with its load reference
- 197 nodes, 739 elements (down from 203/748)

Pitch deck:
- Complete visual redesign with Inter font, dark premium theme
- Grid/mesh background pattern, gradient backgrounds per slide
- Slide 1: product positioning with tagline
- Slide 2: capability grid (modeling, verification, reports) + example tags
- Slide 3: energy expansion with sector cards + offshore proof-of-concept bar
- Slide 4: Argentina focus with 2-column sector/opportunity layout
- Slide 5: team credentials (FIUBA/CEARE) + Patagonia field validation next step
Offshore example:
- Removed element 739 (orphaned boom member) and node 197
- Fixed disconnected structure: inner conductors (±1m) were isolated
  from outer conductor ring (±3m) — added 20 truss ties at each
  conductor level
- Fixed derrick disconnection: 4 derrick base nodes at z=97 were not
  tied to any deck nodes — added 4 frame connections to nearest deck
- 196 nodes, 762 elements, solves correctly

Pitch deck:
- Added real product screenshots: 3d-colormap.jpg (title slide),
  3d-deformed.jpg (capability slide), offshore-reference.webp (energy slide)
- Images integrated into slide layouts with shadows and borders
- Split-layout slides: text left, visual right
- Changed "Próximo paso clave" to "Próximos pasos" with 4 concrete items
  (Patagonia, expert testing, report review, industry pilots)
Offshore example fixes:
- Removed 20 intermediate-leg wave loads (kept corner-leg only), reduced
  magnitudes — lateral/gravity ratio improved from 1:2 to 1:6
- Upgraded deck beam from W24x68 (J=0.0000015) to RHS 500x300x16
  (J=0.000906) — 600x torsional stiffness improvement
- Upgraded derrick-to-deck connections from CHS 300x10 to CHS 800x25
- Converted 32 conductor frame ties to truss members (axial-only)
- Reduced wind loads from 150/50 kN to 50/20 kN per node

Root causes of wild deformed behavior:
1. Wave loads (Fx=2000 + Fy=1000 total) were nearly half the gravity load
2. Deck W-shape had near-zero torsional stiffness causing twist
3. Derrick connections used CHS 300 (weak bending) on long spans

Pitch deck:
- Replaced generic images with real landing-page screenshots:
  Slide 1: pro-verification.png (PRO RC building with rebar)
  Slide 2: 3d-industrial.png (Basic 3D industrial with color map)
  Slide 3: pro-features.png (PRO 3D model overview)
- Changed "Próximo paso clave" to "Próximos pasos" with 4 items
Captured the Offshore Jacket Platform example from the running PRO app
via Puppeteer (headless Chrome). Shows the full 196-node/762-element
battered jacket structure with dense bracing, topsides, and derrick.

Replaced the generic pro-features.png on the energy/offshore slide
with this real product screenshot, making the deck's offshore expansion
claim concrete and verifiable.
Main layout problems fixed:
- S1 hero: was fixed 480×360px absolute-positioned; now flex column
  with 580px max-width and 420px height img-frame
- S2 capability: was max-width 420px; now flex-1 up to 600px with
  440px height frame
- S3 offshore: was 380px fixed column with 360px max-width; now
  flex-1 up to 520px with 420px height and CSS transform scale(1.6)
  to zoom into the model center

All slides now use row flex layout (slide-inner flex-direction:row)
with text and visual columns balanced. Images use img-frame wrapper
with consistent border-radius, shadow, and border. Glow effects
positioned behind each image.

Offshore image uses object-position:center 40% + scale(1.6) to
crop/zoom into the jacket structure without re-capturing.
Pre-solve quality gate:
- handleSolve() now runs checkModel() before invoking the solver
- If model errors exist (severity: error), solve is BLOCKED
- Error message redirects user to the Diagnostics tab
- No cryptic solver crash — clear "N errors must be fixed" message

Quality-gate banner:
- Persistent amber banner appears above tab content when model has errors
- Shows error count + "Fix before solving" message
- Clicking the banner navigates to the Diagnostics tab
- Hides when viewing the Diagnostics tab (to avoid redundancy)

Reactive model error count:
- modelErrorCount derived tracks errors in real-time as model changes
- Powers both the banner visibility and the solve gate

Existing diagnostics infrastructure leveraged:
- checkModel() already has 30+ checks (connectivity, zero-length,
  missing refs, orphan loads, etc.)
- ProDiagnosticsTab already has clickable items that select entities
  and zoom-to-fit
- This change connects the pre-solve flow to that existing infrastructure

i18n: en + es (modelErrorsBlock, seeDiagnostics, qualityGate,
errorsFound, fixBeforeSolve)
Adds a "Generate LRFD Combinations" button in the PRO Loads tab that
auto-generates ASCE 7 / CIRSOC 101 ultimate load combinations from
existing load-case types.

The generator reads load-case types (D, L, Lr, S, W, E) and produces:
1. 1.4D
2. 1.2D + 1.6L + 0.5(Lr or S)
3. 1.2D + 1.6(Lr or S) + L
4. 1.2D + 1.6W + L + 0.5Lr (per wind case)
5. 1.2D + E + L (per seismic case)
6. 0.9D + 1.6W (per wind case)
7. 0.9D + E (per seismic case)

Features:
- Reads existing load-case type tags to determine which combos apply
- Names each combination with clear factor expression (e.g., "U3: 1.2D + 1.6Lr + L")
- Uses short wind/seismic case names in combo labels
- Generated combos are fully editable after generation
- Requires at least one D-type case (shows error toast otherwise)
- All additions wrapped in modelStore.batch() for single undo
- Button appears below the manual combo-add row with hint text

i18n: en + es (generateLRFD, generateLRFDHint, generateLRFDDesc,
combosGenerated, needDeadCase)
Replaces the one-click combo generator with a reviewed modal flow:

1. Click "Generate LRFD Combinations" → opens a review modal
2. Modal lists all candidate ASCE 7 / CIRSOC 101 combinations
3. Each candidate shows: name, factor expression, and whether an
   equivalent combination already exists
4. New combos are checked by default; existing ones are unchecked
   with an "already exists" badge (dimmed row)
5. User can override: check an existing one to intentionally duplicate
6. Click "Generate Selected" → only checked candidates are added

Equivalence detection:
- comboSignature() creates a canonical string from non-zero factors
  sorted by caseId, rounded to 2 decimals
- comboExists() compares candidate signature against all existing
  combination signatures
- Equivalence is content-based, not name-based — works even if
  existing combos were renamed

Modal UX:
- Dark themed, centered overlay with close/cancel/generate buttons
- Shows "N / M selected" counter in footer
- Generate button disabled when nothing is selected
- Backdrop click closes the modal

i18n: en + es (comboAlreadyExists, selected, generateSelected)
Example naming:
- Removed "(CIRSOC 102)" from 7-story Wind +X case name
- Fixed lowercase "Dead load"/"Live load" to "Dead Load"/"Live Load"
  in 3d-building.json and 3d-nave-industrial.json

LRFD equivalence fix:
- Root cause: generator added 0.5×Lr companion to wind combos (pattern 4)
  but existing 7-story combos don't include the Lr companion load,
  so signatures differed (5 non-zero terms vs 4)
- Fix: removed 0.5×Lr/S companion from wind/seismic combos to match
  standard practice where the companion load is optional
- Now the 7-story U4-U7 combos correctly match generated candidates

Display ordering:
- Factor expressions in the generator modal now sort by load-case type
  priority: D → L → Lr → S → W → E → other
- Within same type, sorted by caseId
- Produces stable professional ordering regardless of insertion order
Example naming cleanup:
- Removed redundant type prefixes from load-case names in 7-story
  and offshore examples (e.g., "D — Superimposed dead" → "Superimposed dead")
- Type is already shown by the UI's type column/dot; the name now
  carries only the descriptive content

Service/ASD combination generator:
- New "Generate Service Combinations" button alongside the existing LRFD one
- Produces ASCE 7 §2.4 / CIRSOC 101 ASD combinations:
  S1: D
  S2: D + L
  S3: D + Lr (or S)
  S4: D + 0.75L + 0.75Lr
  S5: D + W (per wind direction)
  S6: D + 0.7E (per seismic direction)
  S7: D + 0.75L + 0.75W
  S8: 0.6D + W
  S9: 0.6D + 0.7E
- Same reviewed modal workflow as LRFD (candidates, equivalence, check/uncheck)
- Service combos prefixed with "S" (vs "U" for LRFD)

Provenance badges:
- Combination cards now show small badges based on naming convention:
  - "LRFD" badge (red) for combos matching /^U\d+:/
  - "SVC" badge (teal) for combos matching /^S\d+:/
- Badges are visual-only; combos remain fully editable
- If user renames away from the convention, badge disappears (honest)

Other:
- Removed lightning emoji from generate button
- Modal header shows template name + ASCE 7 section reference
- i18n: en + es for generateService, generateServiceHint, serviceCombosGenerated
Preview formatting:
- Existing-equivalent combos in the modal now show only non-zero
  factors, each on its own row: factor | type | name
- Matches the mental model of the main combination card layout
- Zero-factor load cases are no longer shown (were cluttering the preview)
- Rows sorted by type priority: D → L → Lr → S → W → E

Numbering fix:
- Root cause: applySelectedCombos used combinations.length for the
  counter, so U1-U8 existing → first service combo was S9 instead of S1
- Fix: count only combos matching the active prefix (/^U\d+:/ or /^S\d+:/)
- LRFD and Service numbering are now fully independent families
- U1-U8 existing + no S combos → first service is S1
- S1-S4 existing + no U combos → first LRFD is U1
…Design tab

Report Design Summary:
- Added "Governing Combo" column to the Member Utilization Summary table
- Shows the combination name that produces the governing check for each element
- Combo selection logic: picks the combo corresponding to the governing check type
  (flexure → flexure combo, shear → shear combo, axial+flexure → axial combo)
- Displayed at 9px to fit the table width without overflow

ProDesignTab expandable check details:
- Clicking a design-check row now expands a detail panel below it
- Detail panel shows ALL individual checks for that member:
  Check name | Demand | Capacity | Ratio | Status | Combo
- Each check shows its own governing combination reference
- This enables per-check combo traceability (flexure may govern in U3
  while shear governs in U5 — both are now visible)
- Second click collapses the detail panel
- Click also selects the element in the viewport (existing behavior preserved)

These changes connect the combination workflow → verification → report chain:
- Combinations are generated/edited in the Loads tab
- The Design tab shows which combo governs each check (expandable)
- The Report summarizes governing combos per element (printable)

i18n: en + es (report.governingCombo)
The {@const nonZero = ...} was placed inside a <label> element.
In Svelte 5, {@const} must be the immediate child of a block tag
({#each}, {#if}, etc.), not an HTML element.

Moved the declaration to be immediately after the {#each} opening,
before the <label>. The inner {@const lc3 = ...} inside the nested
{#each nonZero as f} was already valid (direct child of #each).
Data model:
- ElementVerification.governingCombos now carries 6 force components:
  flexure, shear, axial, momentY, shearZ, torsion (was only 3)
- auto-verify.ts now attaches all 6 governing combo refs from
  GoverningPerElement3D when available
- normalizeCirsoc201() now passes comboName for torsion and biaxial
  checks (was missing — those checks showed '—')

Design tab expandable detail:
- When different checks are governed by different combos, a note
  appears: "Different checks governed by different combinations"
- Combo column header renamed to "Governing Combo" for clarity
- Combo cells are highlighted in teal when multiple combos are active
- uniqueCombos set computed per element to detect multi-combo cases

Report Design Summary:
- Governing combo column now marks elements where different checks
  have different governing combos with '*' and orange highlighting
- Footnote explains: "Different checks governed by different
  combinations — see detailed verification for full breakdown"

This addresses the structural-design principle that a member's
flexure, shear, and axial checks may each be governed by a different
load combination — the UI now makes this visible instead of collapsing
everything into a single "governing combo" label.

i18n: en + es (report.multiGovNote)
New module: station-design-forces.ts
The core product-layer foundation for elite-grade RC/steel design
traceability. Extracts forces at critical stations along each element,
for each load combination, preserving sign and the full force tuple.

Station strategy (buildCriticalStations):
- Element endpoints (t=0, t=1)
- Quarter and midpoint (t=0.25, 0.5, 0.75)
- All point-load positions from Y and Z load arrays
- Distributed-load start/end positions and midpoints
- Analytical shear zero-crossing point (interior moment peak)
  computed from endpoint shear sign change

Force extraction (extractForcesAtStation):
- Uses evaluateDiagramAt() for each force component at each station
- Returns full signed tuple: N, Vy, Vz, My, Mz, Torsion
- Exact analytical equilibrium — not interpolation

Per-combo extraction (extractElementStations):
- Iterates all combos in resultsStore.perCombo3D
- Extracts station forces for the requested element under each combo
- Preserves comboId, comboName, and full station force tuples

Governing demand extraction (extractGoverningDemands):
- From station-level per-combo data, finds the worst demand per category:
  Mz+, Mz- (sign-aware), My+, My-, Vy, Vz, N_compression, N_tension, Torsion
- Each demand preserves: value, combo, station, AND the full force tuple
  at that combo/station (critical for combined axial+moment checks)
- Does NOT create impossible envelopes by mixing combos/stations

Design tab integration:
- Expanded element rows now show "Station-Based Governing Demands" section
- Shows: category, value, station location, governing combo, and the
  concurrent N/Vy/Mz force tuple
- Only appears when per-combo results exist (combinations were solved)

Also extended:
- ElementVerification.governingCombos now tracks 6 components
  (added momentY, shearZ, torsion)
- normalizeCirsoc201 now passes comboName for torsion/biaxial checks

CURRENT LIMITATIONS (solver contract):
- No P-delta interior forces (endpoint-only for P-delta)
- No support-face offset (d from column face)
- No cracked-section iteration
- Torsion diagram is linear interpolation only
…d drawing reactivity

- Single "Run Design" button replaces two-step batch flow (design check + accept)
- Design edits now drive live drawings via structuredClone + $state.snapshot reactivity fix
- Column structured editor (corner/face bars, diameters) with zero in-place mutation
- All number inputs use oninput for immediate feedback
- Rich inline verification: CIRSOC memos, interaction diagram, check table, utilization
- Provided-verification utilization responds to edits via enrichedResults $derived
- Anchorage severity: warn vs fail distinction, descriptive messages
- Summary row coherence: governing check and utilization from provided verification
- Removed old RC Verification subtab and Analysis dropdown entry
- Removed duplicate design-vs-verification drawings (single trust surface)
- Removed redundant modified-reinforcement summary strip
- Filter cleanup: Un-designed, Modified (user-only), status filters for designed elements
- rc-qa-diagnostic fixture: balanced loads for meaningful QA
- QA feedback docs from QA2-QA4 rounds
Hoist MzMax/MyMax/VyAbs out of both station-demand and endpoint-fallback paths; map identity once after the if/else: const MuMax = MzMax; const MuyMax = MyMax; const VuMax = VyAbs. Restores SEAM-3. Unbreaks 4 web tests on #47. No solver code touched.
Phase 1 of verification architecture cleanup (SOLVER_APP_COVERAGE_MAP.md §13):

- New verification-service.ts: centralized station-demand computation and
  unified CIRSOC verification entry point (computeStationDemands,
  runUnifiedVerification)
- ProDesignTab: uses shared service instead of inline station computation
- ProVerificationTab: replaced 116-line endpoint-only force extraction loop
  with one call to runUnifiedVerification (fixes Bug #2: interior forces missed,
  Bug #4: two divergent paths)
- Serviceability: deflection now estimates midspan deflection when endpoint
  displacements are negligible (fixes Bug #3); service moment uses dead-load
  case when available instead of Mu/1.4 approximation (fixes Bug #5)
- Both tabs now produce identical verification results through the same
  station-based pipeline and store them in verificationStore.setConcrete()

What this does NOT do (blocked by no-solver-changes constraint):
- No new WASM exports (§13.5 verify_members still pending)
- No deletion of JS verification modules (§13.9 — still needed as primary path)
- No VerificationReport Rust types (§13.6 — solver-side)
- verification-service.ts: header now lists Phase 1 vs Phase 2 scope and
  exactly which app-side bridges remain (station extraction, JS CIRSOC, autoVerify)
- ProVerificationTab: service moment, midspan deflection estimate, and steel
  verification loop all marked as TEMPORARY Phase 1 bridges with Phase 2 targets
- No behavior changes — annotation only
…t type

Steel path reduction:
- Moved 65-line steel verification loop from ProVerificationTab into
  verification-service.ts as runSteelVerification()
- Steel path now uses station-based demands when available (same source as RC),
  falling back to endpoint extraction only when combos aren't computed
- Removed getEnvelopeSolicitations (endpoint-only legacy function) — no longer
  called by either RC or steel paths
- Cleaned unused imports from ProVerificationTab (verifySteelElement,
  SteelVerificationInput, SteelDesignParams)

VerificationReport type:
- New VerificationReport interface in verification-service.ts shaped to mirror
  the eventual solver-side output (§13.6 of SOLVER_APP_COVERAGE_MAP.md)
- Contains: codeId, codeName, elements (MemberDesignResult[]), summary,
  optional stationData, and legacy detail arrays for Phase 1 compatibility
- Components can consume this shape without knowing whether the source is
  JS-side or future WASM-side

Net effect on ProVerificationTab: -96 lines of inline computation removed.
Both RC and steel verification now flow through verification-service.ts.
…cess

- New runCirsocDesign() in verification-service.ts: single entry point that
  runs verification + normalizes + updates both verificationStore.setConcrete()
  and setDesignResults() — replaces 3 separate calls in ProDesignTab
- ProDesignTab: CIRSOC path reduced to one runCirsocDesign() call; removed
  direct imports of normalizeCirsoc201 and autoVerifyFromResults
- Replaced 6 O(n) verificationStore.concrete.find() scans with O(1)
  concreteMap.get() lookups throughout ProDesignTab
- Non-CIRSOC paths still handled inline (WASM payload assembly is code-specific)

Net: ProDesignTab no longer assembles or normalizes CIRSOC verification data —
it calls the service and renders the result.
…nsitional access

- constructibility, autoSplitRows, rowFits: now use getElemSection() (model data)
  + provided reinforcement stirrup diameter instead of reading from concreteMap.
  Eliminates 3 of 6 concreteMap accesses — these were geometry queries that
  belonged on the model, not verification results.

- Remaining 3 concreteMap accesses (acceptAutoDesign, getProvidedVerification,
  template memos/drawings) explicitly labeled as TRANSITIONAL with Phase 2
  migration targets documented inline.

- acceptAutoDesignAll also labeled as transitional iteration over concrete array.

Net: ProDesignTab's 6 concreteMap accesses → 3 (all labeled transitional).
The other 3 now depend only on model geometry + provided reinforcement.
Batuis and others added 15 commits May 29, 2026 14:50
…light

Mirrors what the next commit does for constraints, applied here to
ConnectorElement.

Without this fix, the JS preflight in validateAndSolve (2D + 3D) and the
live model-diagnostics orphan check would flag any node coupled solely
through a ConnectorElement as disconnected — even though the Rust solver
itself accepts the model. The fix:

  - solver-service.ts: include connector endpoints in both the orphan-set
    and the graph-component BFS, in both validateAndSolve2D and
    validateAndSolve3D (4 sites total)
  - model-diagnostics.ts: same orphan-check extension for the live
    diagnostics warning panel
  - model.svelte.ts: remapModelForPlane now passes connectors through
    (was silently dropping them on the way to validateAndSolve2D)

Solver itself is untouched. No new UI surface — this is a correctness
fix for the existing ConnectorElement plumbing.
…eflight

Same class of false-positive the connector slice fixed, now extended to
constraints. The Rust solver already accepts constraint-only-coupled
nodes; the JS preflight (`validateAndSolve2D`/`validateAndSolve3D`) and
the live model diagnostics were running orphan-node + single-component
checks over `model.elements` only, so a node coupled solely through a
rigidLink / equalDOF / eccentricConnection / diaphragm / linearMPC was
incorrectly blocked at solve time.

All five existing constraint kinds now count as connectivity:
  - rigidLink:           master ↔ slave
  - equalDOF:            master ↔ slave
  - eccentricConnection: master ↔ slave
  - diaphragm:           master ↔ each slaveNode[i]
  - linearMPC:           every node in the term set ↔ every other

No constraint kind is intentionally excluded.

Logic lives in a new tiny `constraint-connectivity.ts` helper, consumed
by both the preflight (4 sites: 2D + 3D × orphan-set + BFS-adjacency)
and the diagnostics panel. New unit-test file covers all 5 kinds in
both 2D and 3D, plus a negative control to keep the orphan check honest
for genuinely free-floating nodes.

Solver itself is untouched.
…ements

User-reported bug: with snap-to-grid enabled, drawing an element to an
existing node that does NOT sit on a grid intersection silently fails —
the cursor warps to the nearest grid intersection and the element either
doesn't connect or attaches to the wrong place. Workaround was to disable
grid snap, which is the wrong UX.

Root cause: two sites in the snap pipeline searched for nearby nodes
*from the grid-snapped position* instead of from the raw cursor. If the
off-grid node was further than the 0.5m node threshold from the nearest
grid intersection, the search missed it even though the cursor was
directly on top of the node.

  - lib/viewport/spatial-queries.ts:snapWithMidpoint — the function's
    own doc comment says "priority: existing node > element midpoint >
    grid", but the implementation grid-snapped first, then searched
    nodes from the snapped point. Now searches nodes from raw coords;
    grid snap is the fallback when no node and no midpoint match.

  - components/Viewport.svelte (element click handler) — used the
    grid-snapped position as the node search center. Now uses raw
    world.x/world.y, matching what the support and load tools already
    do.

Snapping precedence rule after the fix (matches the doc comment):
  1. existing node within nodeThreshold (0.5m) of raw cursor → wins
  2. element midpoint within midpointThreshold (0.4m) of raw cursor
  3. grid snap (fallback for free placement)

Side effects:
  - Visual snap highlight in Viewport.svelte now lights up correctly
    when hovering an off-grid node, because the upstream worldX/worldY
    that drives the highlight comes from snapWithMidpoint and now
    points at the node coords as designed.
  - The node-creation tool would, when hovering near an off-grid node,
    place a duplicate node at the same coordinates — same buggy
    behavior it had for grid-aligned nodes already, just consistent
    now. Out of scope for this slice.

6 unit tests covering: cursor on off-grid node (was missed), proximity
pick between two competing nodes, fallback to grid when no node match,
and the no-regression on-grid case.

Solver itself untouched.
…ment

When the user has the node tool active and clicks on the interior of an
existing element, subdivide that element into two with the new node
on it — instead of placing a free-floating node on top of the bar.

Off by default. Lives under Settings → Model as
"Auto-split elements when placing nodes on them" (es: "Subdividir
barras al colocar nodos sobre ellas"). Hidden in 3D/PRO since it's
wired into the Basic 2D Viewport for now.

Implementation:
  - uiStore.autoSplitOnNodePlace ($state<boolean>(false)).
  - Viewport.svelte node-tool create-mode handler: when the flag is
    on AND no existing node is within 0.5m of the raw cursor AND
    findNearestElement returns a hit within 0.3m perpendicular AND
    the projected parametric position t ∈ [0.05, 0.95], delegate to
    modelStore.splitElementAtPoint(elemId, t). On success, select the
    new node, clear results, toast "Bar subdivided".
  - Otherwise (flag off, no element hit, near-endpoint, or click on an
    existing node), the legacy addNode path runs unchanged.

Load handling: splitElementAtPoint already redistributes distributed,
point, and thermal loads — same code path the hinge-mode subdivide
flow has been using in production. The Playwright probe verifies a
uniform q=−10 kN/m on a 6m bar yields two distributed-load segments
with conserved total (−60 kN exact) after a t=0.5 split. The unit
test pins the same conservation property + the type/material/section/
thermal preservation contract.

Endpoint guard: the click handler enforces t ∈ [0.05, 0.95] before
calling splitElementAtPoint (which has its own internal 0.01/0.99
guard). Clicks too close to either end fall back to plain addNode so
no zero/sliver elements get created. Clicks on/near an existing node
also bypass the split (the existing node is the user's intent).

Verified via 5-case Playwright probe (off+interior, on+interior,
on+near-endpoint, on+existing-node, on+loaded-element) — all pass
with zero console errors.

Solver itself untouched.
User-reported rough edge made more visible by the recent snap-precedence
work: clicking the node tool on top of an existing node would silently
add a second coincident node at the same coordinates.

Root cause: the node-tool create-mode handler resolved the snap target
via snapWithMidpoint (which now correctly returns the existing node's
coords when the cursor is within nodeThreshold of one), then unguardedly
called addNode at that resolved position — producing a stacked duplicate.

Fix is local to the click handler in Viewport.svelte: before the
addNode fallback, run findNearestNode against the RAW cursor coords
within the same 0.5m threshold snapWithMidpoint uses; if a node is
found, treat the click as "select that node" instead of placing a
second one. addNode's contract is unchanged (still creates a node at
the requested coords for programmatic / scripted callers).

The guard sits in the same fallback branch the auto-split path lands
in when its own !nearNode pre-check rejects, so:

  - click on existing node, auto-split off → select existing, no dup ✓
  - click on existing node, auto-split on → no split (existing-node
    pre-check), then no dup (this guard) ✓
  - click in empty space → legacy addNode unchanged ✓
  - element drawing to off-grid node (snap-precedence slice) → unaffected ✓
  - snap-to-grid off → still no dup (raw-cursor search) ✓

Verified via 7-case Playwright probe + re-run of the prior snap and
auto-split probes.

3D Basic node tool (handleNodeTool in Viewport3D.svelte) is unaffected
by this slice — it uses ground-plane raycasting and never resolves to
an existing node, so the duplicate path doesn't apply there. Out of
scope per "keep scope tight"; flagged as a follow-up.

Solver itself untouched.
… rule

Part A — Basic 3D regressions
─────────────────────────────

1. AxialColor / colorMap / verification not visible in wireframe mode.
   syncColorMap3D was applying colors to per-element groups via
   setGroupColor — but in wireframe render mode those groups are
   essentially empty (the visible primary is the shared LineSegments2
   batched mesh, which carries its own per-segment color buffer that
   nothing was updating). Result: pressing "Axial Colours" silently
   did nothing in Basic 3D wireframe.

   Fix: also push the chosen color into ctx.elementsBatched via
   setBaseColor + flush, in every branch (axialColor, colorMap continuous
   gradient, colorMap shellVonMises, verification) AND in the restore
   path when the user leaves color mode. ResultsSyncContext now carries
   an elementsBatched reference; init wires it from sceneCtx.

2. Color map hides during camera motion (orbit/pan/zoom).
   applyLowDetail in lod.ts hid `elementsParent` during orbit. In solid /
   sections render mode the result colors live on the cylinders /
   extrusions inside elementsParent, so hiding it made the visualization
   disappear every camera move. Fix follows the existing exception for
   resultsParent: applyLowDetail now takes an optional
   `{ resultsColoringActive }` flag and skips hiding elementsParent when
   it's true. Viewport3D.setLowDetail computes the flag from the active
   diagramType + presence of results3D. Two new lod.test.ts cases pin
   the new behavior and the no-drift-when-flag-is-false case.

3. Trusses indistinguishable from frames in wireframe.
   In wireframe mode the batched mesh carries the visual; eb.upsert
   initializes new entries with COLORS.frame regardless of element type.
   syncSelection later applies the per-type color, but it only runs on
   selection changes, not on initial load. Result: every example loaded
   showing trusses in the cyan frame color until the user clicked
   something.

   Fix: syncElements now calls eb.setBaseColor(id, ...) with the
   per-type wireframe-vs-solid base color directly after upsert, so
   trusses (yellow #f0b848) and frames (cyan #6cb4ff) are visually
   distinct from first paint.

4. "Elements 10+ disappear" — could not reproduce.
   Loaded hinged-arch-3d (12 elem), space-truss (53), space-frame (388),
   tower-3d-4 (96), industrial (633) — all rendered with full element
   counts in my probe. The most plausible explanation is that fix #3
   above (trusses showing as cyan default) made some elements look
   "missing" against a busy structure where they blended into adjacent
   frames. The truss-color fix should resolve the perceived issue. If
   the user still sees elements missing under a specific interaction
   sequence, that needs a fresh repro.

Part B — auto-split + snap-to-grid refinement
─────────────────────────────────────────────

1. autoSplitOnNodePlace default flipped to ON.
   The natural intent of clicking with the node tool on top of an
   existing bar IS "I want a node on this bar". Defaulting OFF was
   conservative for the first slice; with the slice accepted, the
   natural default is ON.

2. Setting moved above Units in Settings → Model.
   ToolbarConfig.svelte now renders the auto-split checkbox right
   below "Show loads" and above the Units group, so the modeling
   options live together (loads visibility + auto-split) before unit
   choice.

3. Auto-split + snap-to-grid combined rule.
   Previously the projection used the raw cursor position, so the new
   split node landed at an arbitrary unsnapped point. Now: when
   uiStore.snapToGrid is on, we project the GRID-SNAPPED cursor onto
   the chosen element line, so the split happens on the element AT
   a grid-aligned position. For axis-aligned bars (the common case)
   this puts the new node exactly on a grid intersection. For diagonal
   bars, the projection of a grid point is the closest reachable
   grid-anchored split point on the line. When snap-to-grid is off,
   `snapped` equals `world` so behavior matches the previous rule.

   findNearestElement still uses the RAW cursor (so the user can
   "point at" a bar even when the grid would warp the cursor away
   perpendicular to the line), but the projection input is the
   grid-snapped position. The endpoint guard t ∈ [0.05, 0.95] still
   applies; existing-node and duplicate-node guards still apply.

Verification (browser probes, all passing, zero console errors):
- 6 examples render full element counts
- axialColor mode applies blue/red colors visibly in wireframe
- 4-case grid-snap-autosplit probe confirms node lands on grid (3,0)
  when clicking off-grid (3.3, 0.05) on a horizontal bar
- prior snap-precedence + duplicate-node + autosplit probes still pass

Vitest: 2148 passing (+2 new lod tests), same 5 pre-existing failures.

Solver itself untouched.
…ger drags

User-reported bug: in Basic mode, while in element/select mode, click+
drag would silently move nodes. The user's mental model is that node
repositioning belongs to the node tool; the select tool should only
select; the element tool should only create elements.

Root cause: handleMouseDown's `currentTool === 'select'` branch (with
the default `selectMode === 'elements'`) ran:

    historyStore.pushState();
    draggedNodeId = nearNode.id;
    dragMoved = false;
    dragStartWorld = { x: snapped.x, y: snapped.y };

i.e. any time the user pressed mouse-down while a node was within 0.3 m
of the cursor, the next mousemove handler entered the drag branch and
moved the node. Users hit this constantly when just inspecting the model
because select+elements is the default UI state.

Fix is a tight tool-mode reassignment in Viewport.svelte:

  1. Select tool / elements selectMode: drop the historyStore.pushState
     + draggedNodeId set. The branch now only does selection.
     `uiStore.selectNode(nearNode.id, e.shiftKey)` — no more drag setup.
     Box-select still works (separate code path), shift-select still
     works.

  2. Node tool create mode: when the click effectively lands on an
     existing node (within nodeThreshold of the raw cursor), promote
     the previous "just select" behavior to "select + start drag" by
     setting draggedNodeId. This is now the ONLY entry point for node
     repositioning.

The existing mousemove drag block (gated on `draggedNodeId !== null`)
and the mouseup cleanup are unchanged — they keep working since they
depend only on the state, not on which tool set it. Touch handling
synthesizes a MouseEvent and routes through handleMouseDown so it
inherits the new gating automatically.

What still works (regression-checked via /tmp/probe-drag.mjs):
  A. Select tool drag attempt → node stays put (the fix).
  B. Select tool single click on a node → node selected, no dup.
  C. Element tool drag attempt → node stays put (was already correct).
  D. Node tool drag → node moves (legitimate repositioning).
  E. Node tool click on empty space → new node created.
  F. Node tool click on existing node → no duplicate, no movement.

3D sanity re-check:
  - axial colors still light up the arch in compression-blue post-solve
    in wireframe.
  - truss/frame distinction holds (space-truss yellow=527, space-frame
    cyan=2910 + truss-color pixels).
  - All 6 named examples render with full element counts.
  - LOD result-coloring exception still works (unit tests in lod.test.ts).

Vitest: 2148 passing, same 5 pre-existing failures, no new regressions.

Solver itself untouched.
…design-tab UI)

Bounded, non-domain fixes from the review. Web suite: 2125 passing
(5 skipped); vite build clean.

- pro-report: the "Design Summary" section and its TOC entry rendered
  whenever verifications existed, ignoring showSection('verification').
  Now gated on showSection('verification') like every other section, so
  opting out of Verification in the report config also drops the summary
  (and its TOC renumbering).
- ProLoadsTab: LRFD/service combo numbering used the *count* of matching
  combos, so after deleting an earlier combo the next generation reused an
  index and produced duplicate names ("U3" twice). Number from the highest
  existing index instead.
- ProDesignTab: the utilization bar was colored from the strength ratio
  (which excludes anchorage/fit checks) while the status badge used the
  overall status, so a member failing only on a detailing check showed a
  red FAIL badge above a green bar. New statusBarColor() floors the bar
  color to the member status; the numeric ratio (strength utilization) is
  unchanged.

Deliberately NOT fixed — these are structural-design correctness issues
that need a PE/domain decision or a data-model change, not a mechanical
patch (documented in the review):
- auto-verify feeds independently-enveloped Mz/Nu/Vy from different combos
  into one verifyElement call (non-concurrent P-M pairing), and collapses
  axial to an unsigned magnitude so net-tension members are checked with
  the compression Vc term (unconservative). cirsoc201 wants Nu +=compression
  while station extraction tags n<0 as compression. A correct fix needs
  concurrent per-combo force tuples + a confirmed solver sign convention;
  a scalar-Nu patch can't be correct for both shear and P-M at once.
- fc = material.fy (Material has no fc field); concrete with fy>80 is
  silently skipped. Needs a material category / fc on the model.
- M1/M2 slenderness still endpoint-based on the station path; biaxial
  Nu-scaling and Vy-only (no Vz) tie shear need PE validation.
…ign, batch accept)

Bounded, non-domain fixes from the review. Web suite: 2125 passing
(5 skipped); vite build clean.

- ProPanel: the report only re-verified CIRSOC when verificationStore was
  empty, so after editing the model (store still populated from a prior run)
  the report showed stale verification results next to current model data.
  Always re-verify against the current model on report open.
- verification-service (runSteelVerification): the station-demand branch
  collapsed demands with the SIGNED `.value` via Math.max, so a compression-
  only axial (n<0) or hogging-only moment (Mz-<0) became 0 — designing the
  steel member as unloaded. Use `.absValue` (the magnitude used for ranking),
  matching the RC path in auto-verify.ts. NOTE: this path is currently only
  reachable from the dormant ProVerificationTab, so the bug is latent today;
  fixing it removes the landmine before that tab is reactivated.
- ProDesignTab (acceptAutoDesignAll): accepting auto-design for N elements
  cloned the whole elements Map and bumped the reinforcement version once per
  element, and each bump re-ran the WASM station extraction + re-verified
  every element (O(M·N)). Thread a commit flag through setProvided/
  acceptAutoDesign so the batch mutates all elements then triggers reactivity
  once. Default commit=true keeps all other call sites unchanged.

Deliberately NOT fixed (documented in review — need domain work / are
intentional):
- WASM station extraction samples 11 uniform stations and drops the critical
  interior stations (point-load/zero-shear peaks) the JS path inserted, so
  governing moment/shear can be under-sampled (unconservative) in the live
  Design tab + report. Needs the Rust beam-station extractor to accept/insert
  critical stations (or reinstating JS critical sampling) — PE-relevant.
- Two CIRSOC capacity engines (cirsoc201.ts vs station-design-forces.ts) still
  coexist; the dormant ProVerificationTab and the unused VerificationReport
  interface were kept intentionally by the authors as a reactivation reference.
…tmap, LOD double-draw)

Web suite: 2155 passing (5 skipped); vite build clean.

- solver-service (2D): validateAndSolve2D's preflight credits connectivity from
  connectors + constraints (and constraint-connectivity.test.ts pins that), but
  the 2D SolverInput omitted both fields — so a node coupled only via a
  constraint/connector passed the disconnected-node check and was then handed to
  the solver with zero stiffness → singular system, masking the diagnostic.
  Carry `constraints`/`connectors` into both 2D input objects (inline
  validateAndSolve2D and buildSolverInput2D), mirroring buildSolverInput3D, so
  the preflight is honest and the 2D constrained solver actually receives them.
- results-sync (colorMap wireframe): the batched wireframe mesh was always
  colored by mean axial force regardless of the selected heatmap variable, so
  wireframe mode showed axial colors while the legend said shear/moment.
  applyFrameHeatmap now mirrors the SELECTED variable's color onto the batched
  mesh using the same heatmapColor()/globalMax mapping as the cylinders.
- lod (double-draw): during orbit with result coloring active in solid/sections
  mode, both elementsParent (kept visible for colored solids) and the batched
  wireframe were forced on → redundant wireframe overlay + doubled element draw.
  Suppress the batched mesh when elementsParent is kept for results; wireframe
  render mode (where the batched mesh IS the rendering) is unchanged.

Deliberately NOT fixed (need domain validation — documented in review):
- Partial-DOF couplings (single-DOF equalDOF / single-term linearMPC) are
  credited as full connectivity, so a partial mechanism passes preflight.
- Constraint discriminators renamed (equalDof→equalDOF, linearMpc→linearMPC,
  rhs removed, dofs string→int) with no load migration → old saved/shared models
  with the old names render as unknown and the solver rejects the variant.
- EccentricConnection 2D `releases` length (UI always emits the 6-element 3D
  array; 2D expects [ux,uz,ry]). These are solver/DOF-semantics decisions for a
  structural engineer — and gate fully trusting the new 2D constraint wire above.
fix(pro): safe correctness fixes from PR #47 review (report, combos, …
fix(pro): fixes from PR #51 review (2D constraint wire, wireframe hea…
…report-and-pro-polish

Conflicts in ProDesignTab.runDesignCheck: this branch refactored the
CIRSOC path into runCirsocDesign (verification-service), while the base
added an empty-run guard and stale-result invalidation. Resolution keeps
the service call and moves the publish-side semantics into it: an empty
CIRSOC run publishes nothing (caller surfaces the error), a successful
run clears previous legacy/unified results before publishing; non-CIRSOC
codes keep the guard + clear + publish in the component.
@diegokingston diegokingston changed the base branch from pr/4-verification-unification to main June 10, 2026 18:21
Review findings (PR #51): the feature plumbing existed in solver-service
but the store call sites never passed it, so connectors were inert in
every real solve and the diagnostics false positive survived:

- solve3D / solveCombinations3D / solveCombinations3DParallel now pass
  connectors (PRO-gated, same as constraints); buildSolverInput3D passes
  constraints + connectors (PRO advanced analyses route through it).
- remapModelForPlane carries constraints into the 2D ModelData, so the
  2D wire forwarding added by this branch is reachable in production.
- ProDiagnosticsTab/ProPanel pass connectors + constraints to
  checkModel, so the Diagnostics tab stops flagging connector-only
  nodes as disconnected.
- New store-level regression tests that solve THROUGH modelStore
  (the engine-level wire tests construct ModelData by hand and missed
  all of this).
…d.connector in clear()

A node deletion left connectors (and constraints) with dangling node
references — persisted in snapshots/files/share URLs — that the engine
silently skips (assemble_connectors: None => continue) while the
connectivity preflight kept crediting them as edges, yielding mechanism
errors with no diagnostic. removeNode now deletes connectors touching
the node, drops rigidLink/equalDOF/eccentricConnection constraints whose
master or slave is gone, prunes diaphragm slave lists (dropping emptied
ones), and drops linearMPC constraints whole (removing one term would
change the equation's meaning). clear() also resets nextId.connector,
the only counter it missed. Store-level tests added.
This branch renamed constraint discriminators ('equalDof'→'equalDOF',
'linearMpc'→'linearMPC') and switched DOFs from name strings to integer
indices, but only on the write path. Persisted data (autosaves, saved
projects, share URLs — all rehydrated through restore()) kept the old
shapes: the new constraint-connectivity switch silently skipped them
(false 'disconnected node') and Rust serde rejected them at solve time
('unknown variant equalDof').

restore() now normalizes constraints at the single chokepoint, the same
pattern as the hinge→release and iy/iz read-migrations: discriminators
renamed, DOF name strings mapped to indices, unknown kinds dropped
rather than shipped to the solver. Tests cover each legacy shape.
…vention

ProConstraintsTab always emits 3D semantics (dofs 0..5, 6-bool eccentric
releases) while the Rust 2D solver speaks [0=ux, 1=uz, 2=ry]: a
rotational dof index hard-failed 2D validation ('references DOF 4 but
max is 2') and a 6-bool releases array was silently misread (uy slot
interpreted as uz). New constraint-2d-remap.ts is the single
translation layer: in-plane dofs 0/2/4 map to 0/1/2, out-of-plane
dofs/equations are dropped (the 2D solve restrains them implicitly),
eccentric offsets move vertical Z into the 2D dy slot and releases
collapse to [ux, uz, ry]. Diaphragms pass through verbatim — the Rust
transform is already dimension-aware for them.

Both 2D wire builders consume the remapped list, and the connectivity
preflight credits exactly what reaches the solver (a dropped
out-of-plane-only constraint no longer masks a real orphan).
…ctors

The flat-2D→3D embedding (project2DToXZ) remaps node coordinates,
supports and loads, but has no translation for constraint DOF indices,
eccentric offsets or connector local axes — an offsetY authored in 3D
landed on the restrained out-of-plane axis, silently zeroing the lever
arm. shouldEmbedFlat2DModelIn3D now refuses models with constraints or
connectors (PRO-authored, true 3D semantics): a flat grillage with rigid
links solves in direct 3D coordinates instead.
Two windows let the node tool place a coincident-unconnected node ON a
bar — the exact trap auto-split exists to prevent:

- The cursor-based split search used 0.3m while snapWithMidpoint's
  midpoint snap reaches 0.4m: clicks in the 0.3-0.4 annulus near a
  bar's midpoint skipped the split but still placed the node exactly at
  the midpoint. A second split attempt now runs at the resolved
  placement point `ms` with a tight 0.01 tolerance, covering both the
  midpoint snap and grid intersections that lie on a bar.
- The duplicate-coincident-node guard only checked the raw cursor
  (0.5m), while placement happens at `ms`: with grid snap on, `ms` can
  resolve onto an existing grid-aligned node up to ~0.7m from the
  cursor, creating an exact duplicate. The guard now also checks the
  placement point.

The split-projection logic is extracted into one attemptSplit helper
instead of a second inline copy.
… parsing

- lod.ts: shellsParent now honors resultsColoringActive like
  elementsParent — the shell Von Mises heatmap is painted on the shell
  groups, so hiding them during orbit made the one visualization shell
  models care about vanish exactly while the user inspects it. Test
  added.
- Viewport3D restoreColor: hover-out while a color mode is active is now
  a no-op instead of a full syncColorMap3D() — applyHoverColor never
  paints in color mode, so there was nothing to restore, and the full
  resync recolored every element per element-leave (multi-ms stalls on
  large models). Mode changes mid-hover are covered by the colorMap
  $effect.
- elements-batched flush(): color-only changes skip the position
  re-upload and computeLineDistances (both depend solely on positions).
- ProConstraintsTab: numeric inputs use the comma-tolerant parseNum rule
  from ProLoadsTab — '0,5' as an offset/stiffness silently parsed as 0
  via bare parseFloat on these type="text" inputs.
- constraint-connectivity.ts: fix the header claim that 'the Rust solver
  doesn't care' — its pre-solve isolated-node gate counts elements and
  connectors but not constraints, so it still emits a warning-level
  diagnostic for constraint-only nodes (engine-side follow-up).
…m path

The web/src/lib/wasm gitignore pattern has a trailing slash, which does
not match a symlink to the wasm pkg (used in git-worktree dev setups) —
git add -A picked it up. Remove it from the index and widen the ignore.
…ts/connectors"

Deeper review of the reverted gate showed it fixed an unreachable case
while breaking the reachable one:

- The embed only triggers for models with 2D-style support/load types,
  i.e. 2D-editor-authored models (fixtures or legacy files opened in
  PRO). For those, the embed maps editor-y (vertical) onto solver-z —
  so constraints authored in the PRO z-up UI ('uz' = vertical,
  offsetZ = vertical) land on the correct axis UNDER the embed. The
  'mis-axed offset' scenario required a flat PRO grillage with 2D-typed
  supports, which the PRO tools cannot author.
- Refusing the embed created a real cliff for the reachable models:
  the blanket out-of-plane restraints stop being injected (instant
  mechanisms for hinged/truss models) and 2D nodal loads split gravity
  axes against self-weight; and only the solver-side gate changed, so a
  flat model rendered in the upright2dIn3d presentation would draw
  projected while solving unprojected.

The embed's axis convention is self-consistent for every model that can
reach it; keep it.
Findings from the adversarial review of the fix series itself:

- ProAdvancedTab.buildInput now passes connectors — the 8th hand-built
  ModelData literal; without it every PRO advanced analysis (modal,
  buckling, P-Delta, time-history...) silently dropped connector
  stiffness while the fixed linear solve included it.
- migrateConstraint: an unmappable legacy linearMPC term DOF now drops
  the whole constraint instead of silently rewriting it onto ux
  (rewriting changes the equation's meaning; consistent with how
  rigidLink/equalDOF filter and unknown kinds drop).
- removeNode cascade is now bulkMutate-safe: the commit phase of
  bulkMutate overwrites model.constraints/model.loads with its
  pre-callback buffers, which would resurrect the pruned entries —
  the cascade prunes the buffers too (latent today: no current caller
  deletes nodes inside bulkMutate).
- Node tool: the cursor node scan is computed once and shared by the
  auto-split guard and the duplicate-coincident guard (two identical
  scans that could silently diverge if one threshold is tuned).
- Hinge-mode split gets the zero-length-element guard the extracted
  attemptSplit already had (tParam = NaN otherwise).
- linearMPC input: ';' is now the documented term separator (',' still
  accepted), and a malformed fragment ABORTS the add instead of being
  dropped — with comma separators, a decimal comma in a coefficient
  ('-0,5') split as a term boundary and committed a silently-zeroed
  equation (pre-existing; the comma-tolerant parseNum cannot fix it at
  this call site because the separator is also a comma).
@diegokingston diegokingston merged commit 7c1e67f into main Jun 11, 2026
3 checks passed
diegokingston added a commit that referenced this pull request Jun 11, 2026
web/node_modules entered main via 753c862 (PR #51): the node_modules/
gitignore pattern has a trailing slash, which does not match a symlink
to a node_modules dir (git-worktree dev setups) — same trap as the wasm
symlink removed in 39948bb. Remove it from the index and add the
non-slash pattern.
diegokingston added a commit that referenced this pull request Jun 11, 2026
…main)

main absorbed PRs #51 and #57 (including their review-fix commits).
Conflicts in lod.ts/lod.test.ts: main carried the pr/5 review fix
(shellsParent honors resultsColoringActive in the old always-strip LOD)
while this branch rewrote the LOD around the heavy-model policy.
Resolved keeping the heavy-model structure and porting the shell
exception into it: in the heavy fallback, shellsParent now follows the
same result-coloring exception as elementsParent (the shell Von Mises
heatmap lives on the shell groups). Tests merged accordingly.
diegokingston added a commit that referenced this pull request Jun 12, 2026
…to main)

main absorbed PRs #51/#57/#58 with their review fixes. Semantic
resolutions:
- scene-sync element signature: union of both extensions (tl + leftHand
  from main, offset from this branch).
- syncLocalAxes: main's always-mode cap / no-selection-read / shells
  guard combined with this branch's manual-selection semantics ("When
  selected" = manually selected only).
- ProResultsTab query-highlight effect: main's untrack design (fires on
  query changes only, empty query never wipes) combined with this
  branch's elementSelectionManual guard (a manual click survives query
  re-evaluations too).
- Triad $effect: selection/manual deps stay implicit (nested reads),
  preserving the always-mode rebuild fix.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants