Simulator — interaction map¶
Every control, key, and gesture in SimulatorApp, traced to its handler and
the engine call it makes. Status is HOW IT IS — verified against the code
with file:line citations — unless a row is explicitly marked otherwise.
Tables follow the repo element schema: each row shows hop 1 (shell control
handler → event it raises) and hop 2 (app subscription → app handler),
plus whether anything leaves the process (Engine call?).
The window is a MainWindowShell with the default IsPlaybackTabVisible=true
(src/SimulatorApp/MainWindow.axaml:20–21); the shell supplies the chrome, the
app fills the four regions (MainWindow.axaml:23–67). All playback state
(DispatcherTimer, _simMoveIndex, _isPlaying, _playSpeed, _autoPause,
_wasPlayingBeforeScrub) is owned by the app's MainWindow
(MainWindow.axaml.cs:54–65); the shell panels are pure UI surfaces that raise
events. There is no MVVM, no ICommand — all wiring is code-behind events.
flowchart TB
subgraph W["SimulatorApp.MainWindow"]
L["LeftRegion<br/>LayerSlider · Load button"]
V["ViewportRegion<br/>Canvas 600x600 + ViewportToolbar"]
I["InspectorRegion<br/>OverlayPanel + FindingsPanel + PlaybackPanel"]
S["StatusRegion<br/>StatusBar ← Shell.StatusText"]
end
L --> ES["EngineSession"]
V --> VC["ViewportController"]
I --> VC
I --> PT["DispatcherTimer (app-owned)"]
ES --> ABI["NpsAbi → native engine"]
VC --> ABI
Left bar¶
Declared in MainWindow.axaml:23–34.
| Element | Declared | Type | Label / tooltip | Trigger | Handler chain | Engine call? |
|---|---|---|---|---|---|---|
LayerSlider |
MainWindow.axaml:26 |
Slider | none; range 0–10, default 10 | ValueChanged |
OnLayerSliderChanged (MainWindow.axaml.cs:433) → _viewport.SetLayerVisibility (ViewportController.cs:252) → NpsAbi.ViewportSetLayerVisibility (ViewportController.cs:256), then BuildBuffers (:439) + UpdateLayerLabel (:441) + RenderViewport (:442) |
Yes |
LayerLabel |
MainWindow.axaml:27 |
TextBlock | starts "Layer: all (10)"; becomes Layer max: N (vis) |
— | written by UpdateLayerLabel (MainWindow.axaml.cs:445–452) |
UI-only (output) |
| Load button | MainWindow.axaml:28 |
Button | "Load + Gen + Analyze (Benchy)" | Click |
OnLoadAnalyzeClick (MainWindow.axaml.cs:205) → _session.LoadModel → GenerateDemoToolpath (5 layers, :245–250) → _viewport.SetMoves/SetCamera/BuildBuffers (:263–269) → _session.Analyze (:272) → LoadFindings (:316) → ApplyCurrentOverlay (:465) |
Yes (full chain) |
| Fixture note | MainWindow.axaml:29 |
TextBlock | "Primary fixture enforced (nps-l7k.16)" | — | static text | UI-only |
MeshInfoLabel |
MainWindow.axaml:32 |
TextBlock | plates/palette/V/T/unit summary | — | written at MainWindow.axaml.cs:238–242 after load |
UI-only (output) |
The slider maximum 10 is hard-coded; the generated demo toolpath has 5 layers
(MainWindow.axaml.cs:245–250), and the XAML header comment calls this region
a "layer nav stub" (MainWindow.axaml:12).
Viewport toolbar¶
Code-built in ViewportToolbar.cs:40–107 (no .axaml); events declared at
:18–27, raised by lambdas at :70–81; icons assigned from theme resources
Icon.Viewport.* in SetIconGeometries (:145–157). App subscriptions at
MainWindow.axaml.cs:91–100. Both toggles default IsChecked = true
(ViewportToolbar.cs:61, 64).
| Element | Declared | Type | Label / tooltip | Trigger | Handler chain | Engine call? |
|---|---|---|---|---|---|---|
ResetButton |
ViewportToolbar.cs:53 |
Button (Icon.Viewport.Reset :147) |
"Reset Camera" | Click → ResetClicked (:18, raised :70) |
lambda MainWindow.axaml.cs:91 → SetCameraPreset(Reset) (ViewportController.cs:294) → SetCamera → NpsAbi.ViewportSetCamera (ViewportController.cs:266) + RenderViewport |
Yes |
IsometricButton / FrontButton / TopButton / RightButton |
ViewportToolbar.cs:54–57 |
Button (icons :148–151) |
"Isometric View" etc. | Click → *Clicked (:19–22, raised :71–74) |
lambdas MainWindow.axaml.cs:92–95 → same preset path |
Yes |
ZoomFitButton |
ViewportToolbar.cs:58 |
Button (Icon.Viewport.ZoomFit :152) |
"Zoom to Fit" | Click → ZoomFitClicked (:23, raised :75) |
lambda MainWindow.axaml.cs:96 → ZoomFitCamera (MainWindow.axaml.cs:827) → ComputeZoomFit (ViewportController.cs:432; projects via NpsAbi.ProjectOrbitXY at :456) → SetCamera + RenderViewport |
Yes |
TravelsToggle |
ViewportToolbar.cs:60–61 |
ToggleButton, checked (:61) |
"Show Travels" | Click → ShowTravelsToggled(bool) (:24, raised :77) |
lambda MainWindow.axaml.cs:97 → RenderViewport only; the flag is read in the paint loop (MainWindow.axaml.cs:745) |
UI-only |
ExtrudesToggle |
ViewportToolbar.cs:63–64 |
ToggleButton, checked (:64) |
"Show Extrudes" | Click → ShowExtrudesToggled(bool) (:25, raised :78) |
lambda MainWindow.axaml.cs:98 → RenderViewport; read at MainWindow.axaml.cs:746 |
UI-only |
LayerUpButton / LayerDownButton |
ViewportToolbar.cs:66–67 |
Button (icons :155–156) |
"Layer Up" / "Layer Down" | Click → LayerUpClicked / LayerDownClicked (:26–27, raised :80–81) |
lambdas MainWindow.axaml.cs:99–100 clamp LayerSlider.Value ± 1 → routes through OnLayerSliderChanged above |
Yes (via slider) |
Overlay panel¶
Declared in OverlayPanel.axaml:10–14; hop 1 is OnRadioButtonChecked
(OverlayPanel.axaml.cs:98–108) raising ModeChanged (:66); the app wires
hop 2 at MainWindow.axaml.cs:115.
| Element | Declared | Type | Label | Trigger | Handler chain | Engine call? |
|---|---|---|---|---|---|---|
| Radio "None" | OverlayPanel.axaml:10 |
RadioButton, tag motion-type, default checked |
"None" | IsCheckedChanged |
ModeChanged("motion-type") → OnOverlayModeChanged (MainWindow.axaml.cs:459) → ApplyCurrentOverlay (:465) → _viewport.ApplyOverlay (ViewportController.cs:564) → per-move GetOverlayValue (:588) → NpsAbi.ViewportApplyOverlay (:547) |
Yes |
| Radio "Thermal / Heat" | OverlayPanel.axaml:11 |
RadioButton, tag thermal |
"Thermal / Heat" | same | same chain | Yes |
| Radio "Warp Risk" | OverlayPanel.axaml:12 |
RadioButton, tag warp-risk |
"Warp Risk" | same | same chain; additionally SetWarpParams(true, 5.0f) (ViewportController.cs:629) |
Yes |
| Radio "Stress / Bond" | OverlayPanel.axaml:13 |
RadioButton, tag bond-strength |
"Stress / Bond" | same | same chain | Yes |
| Radio "Cooling" | OverlayPanel.axaml:14 |
RadioButton, tag cooling-eff |
"Cooling" | same | same chain | Yes |
Notes on the apply path: after a load, OnLoadAnalyzeClick forces the mode to
Thermal and calls ApplyCurrentOverlay unconditionally
(MainWindow.axaml.cs:282–295), because Avalonia suppresses equal-value
property changes and ModeChanged would not fire on a second load. "None" is
motion-type — type-colored extrudes, not a true off state. Legend low/high
and stats text are written back to the panel at MainWindow.axaml.cs:477–497.
Findings panel¶
Declared in FindingsPanel.axaml; chips at :38–64, list at :70, detail
pane at :97–118. Initially IsVisible="False" (MainWindow.axaml:52); the
app subscribes FindingSelected inside LoadFindings
(MainWindow.axaml.cs:322–323, unsubscribe-first to avoid double-wiring).
| Element | Declared | Type | Label | Trigger | Handler chain | Engine call? |
|---|---|---|---|---|---|---|
Severity chips ChipAll / ChipCritical / ChipWarning / ChipInfo |
FindingsPanel.axaml:38–44 |
ToggleButton | "All", "C", "W", "I" | Click |
OnChipAllClick / OnChipSeverityClick (FindingsPanel.axaml.cs:268, 276) → immutable FindingsFilterState + RebuildList (:342) |
UI-only |
Category chips ChipCatThermal, ChipCatStructural, ChipCatMotion, ChipCatHole, ChipCatRetraction, ChipCatFlow, ChipCatUnknown |
FindingsPanel.axaml:52–64 |
ToggleButton ×7 | category names | Click |
OnChipCategoryClick (FindingsPanel.axaml.cs:305) → filter state + list rebuild |
UI-only |
| Findings list row | FindingsPanel.axaml:70 |
ListBox | severity badge + summary text | SelectionChanged |
hop 1: OnFindingSelected (FindingsPanel.axaml.cs:328) → FindingSelected?.Invoke (:332; event :165). hop 2: app OnFindingSelected (MainWindow.axaml.cs:359) → SetLayerVisibility + SetSimulationState + BuildBuffers (:410–419), then PlaybackPanel.SimIndex sync (:423–427) and RenderViewport (:430) |
Yes |
Selecting a finding with layer >= 0 widens _maxLayer to at least
layer + 1 (MainWindow.axaml.cs:405–408); a finding with moveIndex >= 0
jumps the playhead (:413–417). Unpinned findings (moveIndex = -1) update
the detail pane but do not move the playhead. There is no search TextBox —
filtering is chips-only (verified: zero TextBox in FindingsPanel.axaml).
Playback panel¶
Declared in PlaybackPanel.axaml (root x:Name="PanelPlayback"); initially IsVisible="False"
(MainWindow.axaml:58); events wired at MainWindow.axaml.cs:124–133. The
panel raises intent events only; the app owns the timer.
| Element | Declared | Type | Label / tooltip | Trigger | Handler chain | Engine call? |
|---|---|---|---|---|---|---|
PlayPauseButton |
PlaybackPanel.axaml:34–40 |
Button + PathIcon PlayPauseIcon (Icon.Play ↔ Icon.Pause) |
"Play / Pause (P)" | Click |
hop 1: OnPlayPauseClick (PlaybackPanel.axaml.cs:428) → PlayPauseClicked (:91). hop 2: sub :124 → TogglePlay (MainWindow.axaml.cs:510) → starts/stops _playTimer; interval 120 / max(0.1, _playSpeed) ms (:520); icon swap via panel.IsPlaying (:531–535 → PlaybackPanel.axaml.cs:256–269) |
UI-only (timer) |
| Step Back / Step Forward | PlaybackPanel.axaml:41–52 |
Button (Icon.StepBack / Icon.StepForward) |
"Step Back" / "Step Forward" | Click |
hop 1: OnStepBackClick / OnStepForwardClick (PlaybackPanel.axaml.cs:433, 438) → StepClicked(-1/+1) (:94). hop 2: sub :125 → StepPlayback (MainWindow.axaml.cs:616) → SetSimulationState + BuildBuffers (:624–625) + RenderViewport |
Yes |
SpeedSlider |
PlaybackPanel.axaml:62–63 |
Slider, range 0.1–10, default 1 | — | ValueChanged |
hop 1: OnSpeedValueChanged (PlaybackPanel.axaml.cs:443) → SpeedChanged (:97). hop 2: sub :126 → OnPanelSpeedChanged (MainWindow.axaml.cs:540) → recomputes timer interval if playing (:543–546) |
UI-only |
| Speed label | PlaybackPanel.axaml:60 |
TextBlock | Speed: N.Nx |
— | written by UpdateSpeedLabel (PlaybackPanel.axaml.cs:509–512, styled prop :127–134) |
UI-only (output) |
AutoPauseCheckBox |
PlaybackPanel.axaml:68–70 |
CheckBox, default checked | "Auto-pause on findings" | IsCheckedChanged |
hop 1: OnAutoPauseChanged (PlaybackPanel.axaml.cs:449) → AutoPauseChanged (:100). hop 2: sub :127 → _autoPause field (MainWindow.axaml.cs:64, default true) |
UI-only |
SimIndexLabel |
PlaybackPanel.axaml:56 |
TextBlock | Sim move: i / N |
— | written by UpdateSimIndexLabel (PlaybackPanel.axaml.cs:499–507) |
UI-only (output) |
Scrubber slider |
PlaybackPanel.axaml:78–81 |
Slider; range re-bound to [0, MoveCount-1] by MoveCount setter (PlaybackPanel.axaml.cs:198–224) |
— | see gesture table below | commit → ScrubberChanged (:104) → sub :128 → OnScrubberChanged (MainWindow.axaml.cs:554) → SetSimulationState + BuildBuffers + RenderViewport (:561–564) |
Yes |
MarkerCanvas |
PlaybackPanel.axaml:82–84 |
Canvas, IsHitTestVisible="False" so the slider keeps input |
— | — | rebuilt by SetMarkers (PlaybackPanel.axaml.cs:303) via TimelineMarkerHelper.BuildMarkers; markers with moveIndex < 0 are dropped |
UI-only (display) |
Auto-pause rule (HOW IT IS): on a forward step only, if auto-pause is
enabled and any finding has moveIndex >= 0 less than 2 moves from the
playhead (Math.Abs(...) < 2, i.e. distance 0 or 1), the timer stops and the
status reads "Auto-paused at finding." (MainWindow.axaml.cs:645–656).
Scrubbing deliberately does not auto-pause — the user is driving
(MainWindow.axaml.cs:549–553).
Defined but unused: ScrubberDragStarted / ScrubberDragCompleted
The panel defines and raises both events (PlaybackPanel.axaml.cs:118,
121; raised at :402, 421), but they are documented as diagnostic-only
(:116–121) and no app subscribes to them. The Simulator's
pause/resume flow runs entirely through ScrubberPointerPressed /
ScrubberPointerReleased (MainWindow.axaml.cs:132–133), which cover both
thumb drags and bare track clicks.
Window keys¶
KeyDown="OnKeyDown" on the window (MainWindow.axaml:8) → OnKeyDown
(MainWindow.axaml.cs:188) → shared dispatcher ShellKeybindings.Handle
(ShellKeybindings.cs:82) with app-supplied actions in the readonly struct
ShellKeybindings.Actions (ShellKeybindings.cs:58–71; supplied at
MainWindow.axaml.cs:190–200).
| Key | Dispatch line | Action | Handler chain | Engine call? |
|---|---|---|---|---|
| P | ShellKeybindings.cs:84 |
TogglePlay |
TogglePlay (MainWindow.axaml.cs:510) — same as the panel button |
UI-only (timer) |
| Right | ShellKeybindings.cs:98 |
StepForward |
StepPlayback(+1) (MainWindow.axaml.cs:616) |
Yes |
| Left | ShellKeybindings.cs:105 |
StepBackward |
StepPlayback(-1) |
Yes |
| Ctrl+L | ShellKeybindings.cs:91 |
ToggleInspector |
toggles Shell.IsRightExpanded + status line (MainWindow.axaml.cs:195–199) |
UI-only |
Tooltip myth: the T/S/C/W keys do not exist
The Overlays activity tab carries the tooltip "Overlays (T/S/C/W)"
(MainWindowShell.axaml:86), implying hotkeys for the overlay modes. No
such bindings exist: ShellKeybindings.Handle (ShellKeybindings.cs:82–113)
matches exactly four keys — P, Ctrl+L, Left, Right — and no other key
handler is registered anywhere in the app. The tooltip is stale.
Canvas and scrubber gestures¶
Viewport pointer events reach the app through the shell: the app content is a
fixed 600×600 Canvas (ViewportCanvas, MainWindow.axaml:39);
AttachViewportHandlers
subscribes the ViewportHost (MainWindowShell.axaml.cs:269–274) and forwards
to ViewportPointer* (events MainWindowShell.axaml.cs:35–37, raised
:278–291); the app subscribes at MainWindow.axaml.cs:87–89.
| Gesture | Trigger | Handler chain | Engine call? |
|---|---|---|---|
| Left-drag on canvas | ViewportPointerMoved with left button |
OnViewportPointerMove (MainWindow.axaml.cs:666) → orbit: RotationZRadians += dx*0.01, RotationXRadians += dy*0.01 clamped to [-1.5, 1.5] (:678–683) → SetCamera (:689) → NpsAbi.ViewportSetCamera (ViewportController.cs:266) + RenderViewport |
Yes |
| Non-left-drag on canvas | ViewportPointerMoved other button |
same handler → pan: PanX/Y += d * 0.5 / max(0.1, zoom) (MainWindow.axaml.cs:684–688) → SetCamera + RenderViewport |
Yes |
| Press / release on canvas | ViewportPointerPressed / Released |
set / clear _isDragging (MainWindow.axaml.cs:661–665, 692) |
UI-only |
| Mouse wheel | — | No handler. No PointerWheelChanged subscription exists in SimulatorApp or Nps.Shell. Zoom changes only via Zoom Fit, the Reset preset, or the load-time camera (MainWindow.axaml.cs:267). |
— |
| Scrubber press (thumb or track) | PointerPressed / Thumb.DragStarted on slider |
AddHandler subscriptions (PlaybackPanel.axaml.cs:182–186) → BeginUserScrub (:365) → ScrubberPointerPressed once (idempotent, :109) → hop 2 sub MainWindow.axaml.cs:132 → OnScrubberPressPause (:567) → latches _wasPlayingBeforeScrub and stops the timer |
UI-only |
| Scrubber drag (intermediate) | slider Value changes during drag |
OnScrubberPropertyChanged (PlaybackPanel.axaml.cs:454) — suppressed while _isUserScrubbing (:478–481); no ScrubberChanged fires |
UI-only |
| Scrubber release / drag end | PointerReleased / Thumb.DragCompleted |
EndDrag (PlaybackPanel.axaml.cs:405) fires exactly one ScrubberChanged commit (:418–420) — but only when a thumb drag was active; its guard (:407) makes it a no-op after a bare track click, whose commit instead comes from OnScrubberPropertyChanged (:483–484). The commit → OnScrubberChanged (MainWindow.axaml.cs:554) → SetSimulationState + BuildBuffers; then ScrubberPointerReleased → OnScrubberReleaseMaybeResume (MainWindow.axaml.cs:592) restarts the timer only if it was playing before the press |
Yes |
| Scrubber capture lost | PointerCaptureLost (alt-tab, focus loss) |
OnScrubberPointerCaptureLost (PlaybackPanel.axaml.cs:357–363) → EndUserScrub (:376) → same commit + release path, so a cancelled drag still settles the index and clears the latch |
Yes (via commit) |
Two suppression flags keep the scrubber and timer from fighting
(PlaybackPanel.axaml.cs:143–162): _suppressScrubberEvent blocks the echo
when the app pushes SimIndex into the panel (:463–467), and
_isUserScrubbing parks intermediate value changes during a drag so the app
sees a single commit on release.
Sidebar collapse, activity tabs, status bar¶
Declared in MainWindowShell.axaml; all chrome is shell-owned and UI-only.
| Element | Declared | Type | Label / tooltip | Trigger | Handler chain |
|---|---|---|---|---|---|
| Collapse left | MainWindowShell.axaml:44 |
Button | "◀" / "Collapse Left Sidebar" | Click |
OnCollapseLeftClick (MainWindowShell.axaml.cs:294) → IsLeftExpanded = false → class handler (:135) → UpdateLeftLayout (:324); width cached in _lastLeftWidth (:26) |
| Expand left strip | MainWindowShell.axaml:54 |
Button | "▶" / "Expand Left Sidebar" | Click |
OnExpandLeftClick (MainWindowShell.axaml.cs:299) → restores cached width |
| Collapse inspector | MainWindowShell.axaml:82 |
Button | "▶" / "Collapse Inspector" | Click |
OnCollapseRightClick (MainWindowShell.axaml.cs:304) → UpdateRightLayout (:382) |
| Expand inspector strip | MainWindowShell.axaml:107 |
Button | "◀" / "Expand Inspector" | Click |
OnExpandRightClick (MainWindowShell.axaml.cs:309) |
| Tab "overlays" | MainWindowShell.axaml:86 |
Button (PathIcon) |
"Overlays (T/S/C/W)" — see tooltip myth above | Click |
wired in shell ctor (MainWindowShell.axaml.cs:154) → ActivityBarHost.HandleClick (ActivityBarHost.cs:80) → TabClicked (ActivityBarHost.cs:58, 84) → OnActivityTabClicked (MainWindow.axaml.cs:144) → Shell.Panels.Show (PanelHost.cs:73) |
| Tab "findings" | MainWindowShell.axaml:89 |
Button (PathIcon) |
"Findings" | Click |
same chain; the onShown callback re-runs LoadFindings (MainWindow.axaml.cs:148–151) |
| Tab "playback" | MainWindowShell.axaml:92 |
Button (PathIcon) |
"Playback Sim" | Click |
same chain; registered only when IsPlaybackTabVisible (MainWindowShell.axaml.cs:182–187) |
Left / right GridSplitter |
MainWindowShell.axaml:61, 70 |
GridSplitter | — | drag | Avalonia-managed resize of column pairs 0/2 and 2/4; collapsed state hides the splitter (MainWindowShell.axaml.cs:363–366, 421–424) |
StatusBar |
MainWindow.axaml:63–66 |
TextBlock bound to Shell.StatusText |
— | — | display only; visible path is Shell.StatusText (MainWindow.axaml.cs:853), the render loop also writes a coords line directly (:820–824). The stderr mirror is a separate path via StatusSink.Emit (:857, dual-write stdout+stderr, StatusSink.cs:57–62) |
Dead handler
OnActivityTabClick (MainWindow.axaml.cs:454–457) is a thin wrapper that
forwards to Shell.Activity.HandleClick — but nothing subscribes it. The
XAML wires tabs inside the shell itself (MainWindowShell.axaml.cs:154–155,
185), and the only reference to the method is an example in a doc comment
(ActivityBarHost.cs:5–7). It is dead code in both apps.
Related pages¶
- Simulator — how it is — load/gen/analyze pipeline behind the Load button.
- Simulator — UI workflows — click-level end-to-end chains (gesture → engine → UI) with test coverage per workflow.
- Slicer — interaction map — the sibling app's surface; toolbar, layer slider, pointer math, and key dispatch are verbatim-shared.
- Sharing — duplication map — which of these handler blocks are clones.
- Sharing — plan — the stated direction for moving shared glue into the shell.