Skip to content

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.LoadModelGenerateDemoToolpath (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:91SetCameraPreset(Reset) (ViewportController.cs:294) → SetCameraNpsAbi.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:96ZoomFitCamera (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:97RenderViewport 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:98RenderViewport; 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.PlayIcon.Pause) "Play / Pause (P)" Click hop 1: OnPlayPauseClick (PlaybackPanel.axaml.cs:428) → PlayPauseClicked (:91). hop 2: sub :124TogglePlay (MainWindow.axaml.cs:510) → starts/stops _playTimer; interval 120 / max(0.1, _playSpeed) ms (:520); icon swap via panel.IsPlaying (:531–535PlaybackPanel.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 :125StepPlayback (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 :126OnPanelSpeedChanged (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 :128OnScrubberChanged (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:132OnScrubberPressPause (: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 ScrubberPointerReleasedOnScrubberReleaseMaybeResume (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.

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.