[5] PRO result-query layer — member queries, combo filters, and export#57
Merged
Conversation
…Phase B) First implementation slice of pr/5-basic-mode-overhaul. Schema + store + solver wiring + persistence for the two existing solver primitives that already model translational-release / sliding-bearing behavior. Frame: joint/connection behavior. Connectors and eccentric connections are exposed as solver primitives, NOT as a new top-level "Connectors" object family. Translational releases stay off the typed Release interface — they map to either an EccentricConnectionConstraint with releases[] flags or a ConnectorElement with selective zero stiffness. Schema (web/src/lib/engine/types.ts, types-3d.ts): - ConnectorElement type: nodeI/nodeJ + kAxial/kShear/kMoment + 3D-only kShearZ/kBendY/kBendZ. Mirrors Rust ConnectorElement. - EccentricConnectionConstraint: 5th variant of Constraint3D union. Carries masterNode, slaveNode, offsetX/Y/Z, releases[]. - SolverInput3D.connectors?: Map<number, ConnectorElement>. - SolverInput (2D) gets parallel constraints?/connectors? for schema symmetry; runtime 2D wiring lands when Basic 2D needs it. Store (web/src/lib/store/model.svelte.ts): - StructureModel.connectors: Map<number, ConnectorElement>. - nextId.connector counter. - addConnector / updateConnector / removeConnector / clearConnectors. Map-reassignment pattern per CLAUDE.md "Reactivity with Maps". - snapshot() / restore() round-trip connectors. - clear() resets connectors. Persistence: - ModelSnapshot.connectors? added (history.svelte.ts). - file.ts already round-trips constraints + connectors via the snapshot/restore chain — no direct edit needed there. Phase A audit initially flagged file.ts as dropping constraints; deeper inspection showed snapshot/restore covers them. Only connectors were missing, now wired. - url-sharing.ts: connectors round-trip as `c.cx` (cn already taken by constraints, co by combinations). Solver wiring: - solver-service.ts buildSolverInput3D includes model.connectors. - wasm-solver.ts serializeInput2D + serializeInput3D emit connectors (and constraints for 2D, which were also missing on the wire). Verification (web/src/lib/engine/__tests__/connectors-eccentric.test.ts): - ConnectorElement: kAxial/kShear/kMoment + 3D fields between two nodes, solve3D accepts payload, returns displacements, no error diagnostics. - EccentricConnectionConstraint with translational release on ux, solve3D accepts payload, no error diagnostics. Out of scope for this slice (per revised plan): - UI surfaces (deferred to Phase C, after this slice is reviewed). - 2D runtime wiring of constraints/connectors (deferred to Basic-mode UI slice). - "Sliding bearing" preset / Option B sugar.
…PRO UI
Smallest UI slice that exposes a solver-backed translational-release
primitive end-to-end through the existing constraints workflow. Frame:
joint/connection behavior — extends the existing ProConstraintsTab
pattern, no new top-level surface, no Basic-mode panel overhaul yet.
Per the workstream rule, every translational-release-like behavior
remains an explicit visible solver primitive. No "fake hinge" magic, no
implicit translation between Release and EccentricConnection.
ProConstraintsTab.svelte:
- ConstraintKind union extended with 'eccentricConnection' (5th option,
placed between equalDof and linearMpc).
- Form fields: master/slave node IDs, offsetX/Y/Z (m), and 6 release
checkboxes labelled [ux, uy, uz, rx, ry, rz] in solver order.
- Inline hint that explicitly names what the released DOFs mean
(sliding bearings = translational release; partial hinges =
rotational release) and re-states the DOF order.
- constraintLabel(c) renders `Eccentric: M → S, offset (x,y,z),
released [dofs]` with comma-separated released-DOF names so the
table row stays scannable.
- DOF ordering protected with a reminder comment: the 3D 6-bool array
MUST mirror engine/src/types/input.rs EccentricConnectionConstraint
releases ordering. 2D ordering [ux, uz, ry] is documented but not
used here (PRO is 3D-only).
i18n (en + es):
- pro.eccentricConnection, pro.constraintEcc, pro.eccentricNoRelease,
pro.eccentricHint, pro.releases, pro.offsetX/Y/Z.
Verification:
- web/src/lib/engine/__tests__/connectors-eccentric-persistence.test.ts:
3 tests covering snapshot/restore round-trip for the new constraint
AND for ConnectorElement, plus the share-URL compress/decompress path.
- Existing connectors-eccentric.test.ts (Phase B) still green: solve3D
accepts the new payloads.
- Headless Playwright (not committed): created an EccentricConnection
via the form on RC Design Frame (master=1, slave=2, offsetY=0.5,
release ux only), confirmed:
* type select shows the new option in correct position
* checkbox order is [ux, uy, uz, rx, ry, rz] (matches solver)
* row appears in the constraints table after Add
* row description reads "Eccentric: 1 → 2, offset (0, 0.5, 0),
released [ux]"
* subsequent Solve completes with 0 page errors and no tab error
- vite build clean. Full vitest sweep: +3 new pass, no new failures
(same 5 pre-existing failures as the base, unrelated to this slice).
… wording
linearMPC was a real, end-to-end-broken path. Confirmed by driving the
existing UI through the live solver: every linearMpc constraint created
through PRO Constraints would fail solver parse with
`unknown variant 'linearMpc', expected ... 'linearMPC'`.
Three concrete shape mismatches against the Rust contract (no solver
edit — the Rust side is the contract):
1. Discriminator: TS `'linearMpc'` vs Rust serde rename `linearMPC`.
2. Term shape: TS `{nodeId, dof: string, coeff: number}` vs Rust
`MPCTerm { node_id, dof: usize, coefficient: f64 }`. Camel-case
serde rule covers node_id↔nodeId; the rest were JS-side renames /
wrong types: `dof` was a string (e.g. "ux") and needed to be the
integer DOF index 0..5; `coeff` needed to be `coefficient`.
3. `rhs` field exposed in the UI does not exist on
`LinearMPCConstraint` — the constraint sums to 0 by definition.
It was being silently dropped by serde (no `deny_unknown_fields`),
which means users who typed an RHS got the wrong equation.
Fixes (web/src/lib/engine/types-3d.ts,
web/src/components/pro/ProConstraintsTab.svelte,
web/src/lib/i18n/locales/{en,es}.ts):
- ConstraintType / LinearMpcConstraint discriminator: 'linearMPC'.
- LinearMpcConstraint.terms: `{ nodeId, dof: number, coefficient: number }`.
Comment in the interface marks the contract.
- addLinearMpc(): parse `dof` string → index via dofLabels.indexOf,
emit `coefficient` (was `coeff`), emit `linearMPC` (was `linearMpc`),
no `rhs` in payload.
- Removed `rhs` UI input + state + i18n in the row label.
- selectedKind comparison + addConstraint() dispatch updated.
- constraintLabel for linearMPC: `MPC: {n} terms` (no RHS).
Verification (Playwright against live preview):
- Linear MPC selected → only 1 input visible (terms), no RHS field.
- Add: rows 0→1, row description "MPC: 2 terms".
- Solve completes: tabErr=false, 0 page errors, 0 console errors.
(Before the fix: solver returned `Parse error: unknown variant
'linearMpc'` and the constraint was non-functional.)
Tightened EccentricConnection offset wording (same files):
- Labels: "Offset X / Y / Z (m)" → "Offset X / Y / Z from master (m)".
- Hint: prepended "Offset is measured from the master node to the
connection point." so the form is unambiguous about who the offset
is relative to.
Out of scope for this slice (reported separately for follow-up):
- rigidLink and equalDof constraints are broken with the SAME class of
bug (rigidLink: `dofs: ["ux", ...]` vs Rust Vec<usize>;
equalDof: discriminator 'equalDof' vs Rust 'equalDOF'). Discovered
during the verification probe. Disciplined per task scope to
"linearMpc only" — flagging for next slice rather than expanding here.
Both pre-existing constraint kinds were end-to-end broken in the same
class as linearMPC was: discriminator and/or DOF-array shape diverged
from the Rust contract in engine/src/types/input.rs. Verified via the
live solver before the fix:
- rigidLink → `Parse error: invalid type: string "ux", expected usize`
(UI emitted dofs as name strings; Rust RigidLinkConstraint.dofs is
Vec<usize>).
- equalDof → `Parse error: unknown variant 'equalDof', expected ...
'equalDOF'` (Rust serde rename keeps the all-caps acronym), plus the
same string-DOFs vs Vec<usize> issue.
Fix scope is intentionally tight: discriminator rename + DOF-index
encoding only. Same pattern as the prior linearMPC fix.
Files changed:
- web/src/lib/engine/types-3d.ts: ConstraintType union: 'equalDof' →
'equalDOF'. EqualDofConstraint.type discriminator likewise.
RigidLinkConstraint.dofs typedoc updated to spell out the integer
index mapping (3D: 0=ux, 1=uy, 2=uz, 3=rx, 4=ry, 5=rz).
- web/src/components/pro/ProConstraintsTab.svelte:
* ConstraintKind union and constraintKinds dropdown value: 'equalDOF'.
* addRigidLink(): emit dofs as integer indices via
dofLabels.map((_, i) => i).filter(i => rlDofs[i]).
* addEqualDof(): same encoding + emit type 'equalDOF'.
* selectedKind comparison and addConstraint() dispatch updated.
* constraintLabel: handle 'equalDOF' (was 'equalDof'); shared
dofIndicesToNames() helper renders integer indices back to readable
names (ux,uy,...) for the table row, with a string-tolerant fallback
to absorb any stale data persisted before this fix.
Verification (Playwright against live preview, all on RC Design Frame):
- rigidLink (master=1, slave=2, all 6 dofs): rows 0→1, desc
"Rigid: 1 → 2 [ux,uy,uz,rx,ry,rz]", solve clean (0 errors).
- equalDOF (master=1, slave=2, ux+uy+uz): rows 0→1, desc
"DOF =: 1 → 2 [ux,uy,uz]", solve clean.
- eccentricConnection regression: still solves clean.
- linearMPC regression: still solves clean.
Production build clean (12.67s). Full vitest sweep: same 5 pre-existing
failures as the base, no new regressions.
Surface ConnectorElement (joint/spring/bearing primitive between two nodes) as a section under the existing PRO Constraints workflow. Per the user's revised UI direction, NOT a separate top-level Connectors tab. Sits below the constraints table with a visual separator so it reads as its own surface inside the same right-side workflow. Mental-model framing: - Intro line above the form: "Joint / spring / bearing between two nodes. Not a structural member — does not appear in M/V/N diagrams or design checks." This sets the expectation explicitly that connectors are NOT iterated by the diagrams/verification paths. - Hint line below the form: "Stiffness in each named direction (kN/m or kN·m/rad). A value of 0 means sliding / flexibility in that direction (e.g. kAxial = 0 with stiff kShear/kShearZ = sliding bearing along the connector axis)." - No presets, no "fake hinge" sugar — the user types stiffness values for each of the six solver-shaped directions and sees exactly what the solver will receive. Form fields (all 8 inputs, all explicit): - Node i / Node j (validated against modelStore.nodes). - Stiffness (in-plane): kAxial, kShear, kMoment. - Stiffness (3D only): kShearZ, kBendY, kBendZ. - Add-connector validator rejects all-zero connectors (those are guaranteed mechanisms and almost certainly user error). Table column shape mirrors the constraints table: - # / Nodes (i → j) / Stiffness summary / × delete. - fmtStiff() folds large magnitudes to exponential notation so rows stay readable. i18n (en + es): pro.nConnectors, pro.addConnector, pro.connectorIntro, pro.connectorHint, pro.nodeI / nodeJ, pro.kInPlane, pro.k3D, pro.thNodes, pro.thStiffness. Solver-alignment audit (read-only sweep): - Verified `model.connectors` is consumed ONLY by url-sharing.ts, solver-service.ts, wasm-solver.ts, and modelStore. NOT by viewport rendering, NOT by diagrams (compute_diagrams_3d), NOT by verification (cirsoc201.ts, station-design-forces). Connector UI introduction does not require touching any of those layers. Verification (Playwright against live preview, on RC Design Frame): - Connectors header reads "Connectors: 0" before any add. - Intro text rendered verbatim. - Form has 8 stiffness inputs in declared order. - Sliding-bearing case: nodeI=1, nodeJ=2, kAxial=0, kShear=1e6, kMoment=0, kShearZ=1e6, kBendY=0, kBendZ=0 — added cleanly, rows 0→1, row description renders the full stiffness summary with kAxial=0 / kShear=1.0e+6 / etc. - Subsequent Solve completes: tabErr=false, 0 page errors, 0 console errors. Solver accepts the new payload without complaint. Persistence/share path was already wired end-to-end in Phase B (commit 0c9200e). The persistence test in connectors-eccentric-persistence.test.ts exercises modelStore. addConnector + snapshot/restore + compressSnapshot/decompressSnapshot round-trip for the connector. Still green. Production build clean. No regressions on existing constraints.
…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.
Add a result-query / extraction surface to the PRO Results tab so users can interrogate solved 3D results instead of only scrolling tables. - New pure module lib/engine/result-query.ts: per-end query rows, max/min/abs-max extremes, abs-threshold filter, governing-combo lookup (preserves source-combo label), and CSV serialization. No store/DOM/WASM. - ProResultsTab: scope (all/selection/typed IDs), source (active view or governing combo), component (N/Vy/Vz/T/My/Mz), governing-value card, threshold filter, click-to-select, and "export current query rows" CSV. - activeSourceLabel reads resultsStore.activeView (source of truth) so the label never claims "Envolvente" while single/all-loads data is shown. - i18n: pro.query* keys in en + es (other locales fall back to en). - .gitignore: anchor the Nix result/result-* rules to repo root so they stop silently ignoring source files like result-query.ts. No solver/engine/verification changes. 17 unit tests; full suite green.
- "Link with diagram" / "Vincular con diagrama" checkbox (on by default). - When linked: query component drives resultsStore.diagramType (N→axial, Vy→shearY, Vz→shearZ, T→torsion, My→momentY, Mz→momentZ), and the current scope/threshold element set drives viewport highlight via uiStore.setSelection. Skips scope='selected' (selection IS the scope) and equal sets to avoid reactive loops. When unlinked, neither diagram nor selection is auto-changed. - CSV export is now fully flat with self-contained metadata on every row: sourceKind, sourceId, sourceName, component, scopeMode, scopeIds, threshold, extremeMode, element, end, value, unit. Combo exports carry the real combo id+name; governing exports use sourceKind=governing with each element's own governing combo and end=governing. Export caption states exactly what is exported. "Export all active results" deferred. No solver/engine/verification changes; results.svelte.ts untouched. 20 result-query unit tests; full web suite green; build passes.
The result query is now always linked to the active view:
- Remove the "Link with diagram" checkbox (linkage is permanent).
- Remove the Component selector — component derives from
resultsStore.diagramType (axial→N, shearY→Vy, shearZ→Vz, torsion→T,
momentY→My, momentZ→Mz) via new diagramTypeToComponent(). Non-force
diagrams (deformed/colorMap/verification/…) show an explicit empty
state ("Select a force diagram to query results") — no silent fallback.
- Remove the Source selector and the governing query mode from the panel.
Source now follows the existing Case/Combo/Envelope view controls
(resultsStore.activeView). Governing helpers stay in result-query.ts for
a future explicit feature.
- Keep scope / extreme / |value|≥ filter / CSV export / click-to-select.
- Highlight stays linked (scope+filter drive selection); skips
scope='selected' and non-force diagrams to avoid loops / wiping selection.
- CSV: flat per-row metadata incl. exact case/combo id+name; envelope uses
sourceKind=envelope, sourceId="".
- Rename reactions toggle to "Show Reactions" / "Mostrar reacciones".
No solver/engine/verification changes; results.svelte.ts untouched.
23 result-query unit tests; full web suite green; build passes.
f38fd3e to
d793ab7
Compare
…ics, provenance
Correctness/UX (from the PR review):
- Selection link: the query→selection $effect tracked selectedElements,
so any manual click (viewport, table row, or the governing card's own
selectQueryElement) was instantly reverted to the full query set —
click-to-select was self-defeating. The selection is now read inside
untrack(): the effect pushes when the QUERY changes, manual selection
sticks. An empty query (blank By-ID input) no longer wipes the
selection.
- Envelope semantics: envelope view holds maxAbsResults3D (signed
abs-max winners per field) — 'max'/'min' over it are not the true
envelope extremes and could report +100 where the design minimum is
-60. The mode is coerced to 'absmax' in envelope view and the other
options are disabled.
- Provenance: handleSolve set only the local viewMode='envelope' while
the store stayed activeView='single' — the Envelope toggle rendered
active while the query card/CSV honestly said 'Case'. It now calls
switchView('envelope') (syncs both). The label and the CSV metadata
now read ONE derived activeSource (the two parallel branch chains
could drift), and the all-loads single solve is labeled 'All loads'
instead of a blank-id 'Case'.
- scope='selected' returns no rows in shells select-mode: selection ids
are plate/quad ids there (colliding counters) and resolved as frame
element forces the user never selected.
- Removed the $effect that rewrote uiStore.showLoads3D (and reset
hideLoadsWithDiagram) on every mount/diagram change — it clobbered
preferences owned by ToolbarConfig/ProLoadsTab/KinematicPanel and
URL-restored state, and behaved differently depending on which tab
was mounted. The hideLoadsWithDiagram pref already covers that UX.
- CSV: csvCell neutralizes spreadsheet formula prefixes (=+-@) on
string cells — combo/case names are user-editable and shared via .ded
files; numeric cells (negative values) are unaffected. Export uses
the shared downloadText helper (in-DOM anchor; the inline copy was a
detached-anchor no-op on some browsers). Test added.
- Render: keyed {#each} + 500-row render cap (export still includes all
rows; note shown when capped) — scope='all' mounted 2 rows/element
unbounded (20k <tr> on a 10k-element model).
Purge of the surface left behind by the simplify iteration: the
governing-query exports (governingForComponent, topGoverning,
governingToCsv, componentToDiagramType, FORCE_COMPONENTS) had zero
production callers — the PR body's 'governing combo source' does not
exist in the final UI; their tests, six orphaned i18n keys x 2 locales,
and the dead .col-src CSS are removed with them.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds a result-query / extraction surface to the PRO Results tab so engineers can interrogate solved 3D results instead of only scrolling tables.
What
lib/engine/result-query.ts: per-end query rows, max/min/abs-max extremes, abs-threshold filter, governing-combo lookup (preserves source-combo label), and CSV serialization. No store/DOM/WASM deps.activeSourceLabelreadsresultsStore.activeView(source of truth) so the label never claims "Envolvente" while single/all-loads data is shown.pro.query*keys in en + es (other locales fall back to en)..gitignore: anchor the Nixresult/result-*rules to repo root so they stop silently ignoring source files likeresult-query.ts.Not touched
No solver / engine / verification / design changes; no
results.svelte.tsmutation. Purely additive read-only layer over existing result stores.Verification
result-queryunit tests: 17/17 pass (max/min/absmax, governing label preservation, filter predicate, empty/no-results, CSV).npm run buildsucceeds.pro-edificio-7p): governing value matches the internal-forces table (47.50 kN·m, Elem 34 j = both-ends table max); scopes/threshold/governing mode/click-select/CSV export all verified; 0 console errors. Post-solve source label confirmed correct after the label fix.🤖 Generated with Claude Code