Slicer — interaction map¶
Every control, key, and gesture in SlicerApp, 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. Both event
hops are shown: the shell control raises an event (hop 1), the app subscribes
(hop 2). All wiring is code-behind — no MVVM, no ICommand anywhere.
The window's root is a MainWindowShell with IsPlaybackTabVisible="False"
(src/SlicerApp/MainWindow.axaml:20); the shell supplies the chrome, the app
fills the four regions (src/SlicerApp/MainWindow.axaml:23–99):
flowchart TB
subgraph W["SlicerApp.MainWindow"]
L["LeftRegion<br/>layer slider · slice params · Load / Apply"]
V["ViewportRegion<br/>Canvas 600×600 + ViewportToolbar"]
I["InspectorRegion<br/>OverlayPanel + FindingsPanel (no Playback)"]
S["StatusRegion<br/>StatusBar ← Shell.StatusText"]
end
L --> ES["EngineSession"]
V --> VC["ViewportController"]
I --> VC
ES --> ABI["NpsAbi → native engine"]
VC --> ABI
Left bar¶
Hosted in shell:MainWindowShell.LeftRegion as the StackPanel
LeftSidebarPanel (MainWindow.axaml:23–69), bound to the SliceParams
view-model via left.DataContext = _sliceParams
(MainWindow.axaml.cs:68–72).
Slice parameter inputs¶
The AXAML sets no Value= on the NumericUpDowns — the displayed defaults come
from the two-way bindings to SliceParams, whose defaults deliberately match
the pre-nps-3a3 hard-coded generation call (SliceParams.cs:16–20, 34–36).
Editing any of these is UI-only until Apply; values are also re-harvested
from the controls in HarvestSliceParamsFromUi (MainWindow.axaml.cs:215–246)
as a belt-and-braces against decimal/float binding lag.
| ID | Declared | Type | Label | Binding (TwoWay) | Min / Max / Inc | Default | Source |
|---|---|---|---|---|---|---|---|
SliceLayerHeight |
MainWindow.axaml:34–37 |
NumericUpDown | "Layer Height (mm)" | LayerHeight → SliceParams.cs:50–54 |
0.05 / 2 / 0.05 | 0.2m (SliceParams.cs:40) |
UI-only until Apply |
SliceFeedrate |
MainWindow.axaml:39–42 |
NumericUpDown | "Feedrate (mm/min)" | Feedrate → SliceParams.cs:57–61 |
1 / 30000 / 100 | 1800m (SliceParams.cs:41) |
UI-only until Apply |
SliceFlowScale |
MainWindow.axaml:44–47 |
NumericUpDown | "Volumetric Flow Scale" | VolumetricFlowScale → SliceParams.cs:64–68 |
0.1 / 5 / 0.05 | 1.0m (SliceParams.cs:42) |
UI-only until Apply |
SliceNonPlanar |
MainWindow.axaml:48–51 |
CheckBox | "Non-Planar" | EnableNonPlanar → SliceParams.cs:71–75 |
— | false (SliceParams.cs:43) |
UI-only until Apply |
SliceNumLayers |
MainWindow.axaml:53–56 |
NumericUpDown | "Number of Layers" | NumLayers → SliceParams.cs:78–82 |
1 / 500 / 1 | 5 (SliceParams.cs:44) |
UI-only until Apply |
Range enforcement lives only on the control min/max — SliceParams has no
validation (SliceParams.cs:107–116 is a plain equality-guarded setter).
Other left-bar elements¶
| ID | Declared | Type / Label | Trigger → handler | Engine call | UI updates | Notes |
|---|---|---|---|---|---|---|
LayerSlider |
MainWindow.axaml:28 |
Slider 0–10, Value=10 | ValueChanged → OnLayerSliderChanged (MainWindow.axaml.cs:483–493) |
_viewport.SetLayerVisibility → NpsAbi.ViewportSetLayerVisibility (ViewportController.cs:252–257) + BuildBuffers → NpsAbi.ViewportBuildBuffers (ViewportController.cs:657–661) |
LayerLabel.Text (:495–502), canvas redraw (:492) |
No-op visually until moves exist (RenderViewport early-out :601) |
LayerLabel |
MainWindow.axaml:29 |
TextBlock "Layer: all (10)" | — (display) | — | Written at MainWindow.axaml.cs:500 |
Readout only |
Load button (no x:Name) |
MainWindow.axaml:58–60 |
Button "Load + Gen + Analyze (Benchy primary)" | Click → OnLoadAnalyzeClick (MainWindow.axaml.cs:155–189) |
EngineSession.LoadModel → NpsAbi.LoadModelFromFile (EngineSession.cs:202–245), then RunGenerate() (:251–318) |
Status + MeshInfoLabel.Text (:181–186) |
Silent return if session invalid (:157–160) or fixture missing after a status (:163–167) |
ApplySliceButton |
MainWindow.axaml:61–64 |
Button "Apply / Regenerate" | Click → OnApplySliceClick (MainWindow.axaml.cs:192–211) |
Same RunGenerate() chain |
Status only until gen succeeds | Gated by _modelLoaded (:200–204); harvests UI values first (:209) |
MeshInfoLabel |
MainWindow.axaml:66 |
TextBlock "Mesh: (load to see)" | — (display) | — | Written on load (:182–186) |
Plates / palette / V / T / unit |
WarpGridLabel |
MainWindow.axaml:67 |
TextBlock "WarpGrid: (analyze for full grid)" | — (display) | — | Written in LoadWarpGrids (:321–363) via _session.GetWarpGrid(l) → NpsAbi.GetWarpGridInfo + GetWarpGridRisk (EngineSession.cs:421–443) |
Slicer-only element (absent in Simulator) |
Load vs Apply¶
The two buttons differ in one important way — HOW IT IS:
- Load + Gen + Analyze guards the session, locates the primary Benchy
fixture (
FixtureLocator.FindPrimaryBenchy3Mf(),FixtureLocator.cs:60–90), errors out if the file is missing, calls_session.LoadModel, sets_modelLoaded = true(MainWindow.axaml.cs:176), then runsRunGenerate(). It does not harvest the sidebar values; first click uses theSliceParamsdefaults. - Apply / Regenerate additionally requires
_modelLoaded(MainWindow.axaml.cs:200–204) and callsHarvestSliceParamsFromUi()first (215–246), copying each NumericUpDown / CheckBox value into_sliceParamsbefore the sameRunGenerate().
RunGenerate() then, in order: builds options via
_sliceParams.ToGcodeGenOptions() (MainWindow.axaml.cs:258,
SliceParams.cs:88–96), calls _session.GenerateDemoToolpath(options)
(261), pushes moves, layer visibility, camera and simulation state into the
ViewportController and builds buffers (274–287), runs _session.Analyze(new
MaterialProfileOptions("PLA", 210, 60)) (289), loads findings and warp
grids (296–297), forces the overlay mode to "thermal" (305–310),
applies the overlay, and repaints (312–314). The full click-level chain is
in Slicer UI workflows.
Note
The generated path is a demo toolpath, not a production slice —
stated in the code comment (MainWindow.axaml.cs:260) and in the final
status string (316). The material profile is hard-coded to PLA 210/60
with no UI (289). slice/ is planned, not implemented.
Parameter mapping: SliceParams → GcodeGenOptions → NpsGcodeGenParams¶
The full chain from sidebar value to ABI struct — HOW IT IS
(SliceParams.cs:88–96, EngineSession.cs:258–269, NpsAbi.cs:132–141):
SliceParams property |
GcodeGenOptions field |
NpsGcodeGenParams field |
Cast / notes |
|---|---|---|---|
EnableNonPlanar |
EnableNonPlanar |
enableNonPlanar |
bool → 1/0 |
LayerHeight |
LayerHeight |
layerHeight |
(float) |
VolumetricFlowScale |
VolumetricFlowScale |
volumetricFlowScale |
(float) |
Feedrate |
Feedrate |
feedrate |
(float) |
NumLayers |
NumLayers |
numLayers |
int; 0 = auto from mesh (NpsAbi.cs:138) |
| (none) | — | reserved |
always new int[4] zeros |
The app never names the ABI struct itself — packing happens inside
EngineSession.GenerateDemoToolpath, per the thin-shell constraint noted in
MainWindow.axaml.cs:21–22 and SlicerApp.csproj:20–24. The sole C#
[DllImport] home is src/Nps.EngineHost/NpsAbi.cs. See
UI → engine calls for the wider call table.
Viewport toolbar¶
ViewportToolbar is code-built (src/Nps.Shell/Controls/ViewportToolbar.cs
— there is no .axaml). Ten 32×32 icon buttons, tooltips set via
ToolTip.SetTip (ViewportToolbar.cs:120, 135); events raised by lambdas at
ViewportToolbar.cs:70–81 and subscribed in InitializeShell
(MainWindow.axaml.cs:80–89) — this wiring is character-identical to the
Simulator's (see duplication audit).
| ID | Built | Tooltip | Hop 1 event (file:line) | Hop 2 app subscriber | Engine call / effect |
|---|---|---|---|---|---|
Toolbar.ResetButton |
ViewportToolbar.cs:53 |
"Reset Camera" | ResetClicked (:18, raised :70) |
MainWindow.axaml.cs:80 |
SetCameraPreset(Reset) (ViewportController.cs:294–328) → NpsAbi.ViewportSetCamera + RenderViewport |
Toolbar.IsometricButton |
:54 |
"Isometric View" | IsometricClicked (:19, :71) |
MainWindow.axaml.cs:81 |
same chain, preset Isometric |
Toolbar.FrontButton |
:55 |
"Front View" | FrontClicked (:20, :72) |
:82 |
same chain, preset Front |
Toolbar.TopButton |
:56 |
"Top View" | TopClicked (:21, :73) |
:83 |
same chain, preset Top |
Toolbar.RightButton |
:57 |
"Right View" | RightClicked (:22, :74) |
:84 |
same chain, preset Right |
Toolbar.ZoomFitButton |
:58 |
"Zoom to Fit" | ZoomFitClicked (:23, :75) |
:85 → ZoomFitCamera() (:640–662) |
Host-side projection via NpsAbi.ProjectOrbitXY (ViewportController.cs:432–482), then SetCamera + redraw |
Toolbar.TravelsToggle |
:60–61 (default checked) |
"Show Travels" | ShowTravelsToggled(bool) (:24, :77) |
:86 |
UI-only draw filter — RenderViewport reads TravelsToggle.IsChecked (:619), painter skips travels (GcodeToolpathPainter.cs:155) |
Toolbar.ExtrudesToggle |
:63–64 (default checked) |
"Show Extrudes" | ShowExtrudesToggled(bool) (:25, :78) |
:87 |
UI-only — read at :620, painter skips extrudes (GcodeToolpathPainter.cs:150) |
Toolbar.LayerUpButton |
:66 |
"Layer Up" | LayerUpClicked (:26, :80) |
:88 |
Clamps LayerSlider.Value + 1 → re-fires OnLayerSliderChanged (:483) → ViewportSetLayerVisibility |
Toolbar.LayerDownButton |
:67 |
"Layer Down" | LayerDownClicked (:27, :81) |
:89 |
Mirror of Layer Up |
Canvas gestures¶
The viewport is a fixed 600×600 Canvas (ViewportCanvas,
MainWindow.axaml:74). Pointer
input is attached by the shell (MainWindowShell.AttachViewportHandlers,
MainWindowShell.axaml.cs:256–276) and re-raised verbatim as
ViewportPointerPressed/Moved/Released (MainWindowShell.axaml.cs:35–37,
handlers :278–291); the app subscribes at MainWindow.axaml.cs:76–78.
| Gesture | Hop 1 (shell) | Hop 2 (app) | Effect | Engine call |
|---|---|---|---|---|
| Press on canvas | ViewportPointerPressed (MainWindowShell.axaml.cs:35) |
OnViewportPointer (MainWindow.axaml.cs:560–564) |
_isDragging = true, cache pointer |
— |
| Left-drag | ViewportPointerMoved (:36) |
OnViewportPointerMove (:566–591) |
Orbit: RotationZRadians += dx*0.01, RotationXRadians clamp ±1.5 (:578–583) |
_viewport.SetCamera → NpsAbi.ViewportSetCamera (ViewportController.cs:262–273), then RenderViewport |
| Other-button drag | same | same | Pan scaled by 0.5 / max(0.1, Zoom) (:584–588) |
same |
| Release | ViewportPointerReleased (:37) |
OnViewportPointerRelease (:593) |
_isDragging = false |
— |
| Mouse wheel | — | — | Nothing. There is no wheel-zoom handler anywhere in the app, shell, or controller. Zoom comes only from presets + Zoom to Fit. | — |
Painting is delegated to the shared GcodeToolpathPainter.RenderTo(...)
(MainWindow.axaml.cs:622–631) — the Slicer passes the full path
(maxMoveIndexExclusive: vp.MoveCount, :630); the painter re-projects every
move through NpsAbi.ProjectOrbitXY (GcodeToolpathPainter.cs:160–163) and
never clears the canvas — the app clears at :606.
Inspector panels¶
Overlay panel (PanelOverlays, MainWindow.axaml:83, Title "Overlays (Slicer)")¶
Five radios in OverlayPanel.axaml:10–14 (GroupName="OverlayMode"). Each
fires OnRadioButtonChecked (OverlayPanel.axaml.cs:98–108) → hop 1
ModeChanged(string) (OverlayPanel.axaml.cs:66) → hop 2 app subscription
MainWindow.axaml.cs:99 → OnOverlayModeChanged (:509–513) →
ApplyCurrentOverlay() (:516–558).
| Radio | Tag (mode id) | Default | Engine call on select |
|---|---|---|---|
| "None" | motion-type |
checked (OverlayPanel.axaml:10) |
Not a no-op — it is an active engine mode; controller marks IsActive=false (ViewportController.cs:576) so the painter bypasses the heatmap and draws flat type colors |
| "Thermal / Heat" | thermal |
— | per-move values via GetOverlayValue → one-shot NpsAbi.ViewportApplyOverlay (ViewportController.cs:547–554) |
| "Warp Risk" | warp-risk |
— | same, plus auto SetWarpParams(true, 5.0f) → NpsAbi.ViewportSetWarpParams (ViewportController.cs:627–634) |
| "Stress / Bond" | bond-strength |
— | same as thermal |
| "Cooling" | cooling-eff |
— | same as thermal |
Return path: legend min/max, stats text, IsOverlayActive written to the panel
(MainWindow.axaml.cs:530–546), RenderViewport() recolors extrudes via
NpsAbi.ViewportGetHeatmapColor (GcodeToolpathPainter.cs:171–184), status
"Overlay '{mode}' applied (Slicer parity)." / "Overlay cleared…"
(:550–557). After every Load/Apply the mode is forced to "thermal"
(MainWindow.axaml.cs:305–310) — set unconditionally because Avalonia
suppresses equal-value property changes, so ModeChanged would not fire on a
second load.
Findings panel (PanelFindings, MainWindow.axaml:87, initially IsVisible="False")¶
Filter chips are shell-internal (no app subscription, UI-only): ChipAll
(FindingsPanel.axaml:38 → OnChipAllClick FindingsPanel.axaml.cs:268–274),
severity chips ChipCritical, ChipWarning, ChipInfo (:40–44 →
OnChipSeverityClick :276–303), and seven category chips ChipCatThermal,
ChipCatStructural, ChipCatMotion, ChipCatHole, ChipCatRetraction,
ChipCatFlow, ChipCatUnknown (:52–64 → OnChipCategoryClick :305–322).
Each updates an immutable FindingsFilterState (FindingsFilterState.cs:95–135)
and reassigns FindingsList.ItemsSource once (FindingsPanel.axaml.cs:342–351).
There is no search TextBox.
| Interaction | Hop 1 | Hop 2 | Engine call | UI updates |
|---|---|---|---|---|
| Panel shown (activity tab) | ActivityBarHost.TabClicked (ActivityBarHost.cs:58) |
OnActivityTabClicked (MainWindow.axaml.cs:106–115) → Panels.Show("findings", onShown: LoadFindings) |
LoadFindings → FindingFormatter.ProjectFromSession (:400–405) → 16-byte summaries via GetAnalysisFindingsSummary + per-row 300-byte prefetch via GetAnalysisFindingsDetail |
_findings / _findingMoveIndices filled, panel.SetFindings (:409) |
Row selected (FindingsList, FindingsPanel.axaml:70) |
OnFindingSelected (FindingsPanel.axaml.cs:328–332) raises FindingSelected (:165) |
Subscribed in LoadFindings (MainWindow.axaml.cs:373–374) → OnFindingSelected (:421–481) |
Cache hit = no call; miss → FindingFormatter.RefreshDetail → GetAnalysisFindingsDetail; highlight via SetSimulationState(true, MoveIndex) → NpsAbi.ViewportSetSimulationState (:469–474) + BuildBuffers |
panel.UpdateDetail (:453/:457), status (:459), _maxLayer bump (:461–464), UpdateLayerLabel + RenderViewport (:479–480) |
There is no playback scrubber mirror in the Slicer (comment
MainWindow.axaml.cs:419) — selection is highlight-only.
Window keys¶
KeyDown on the window (MainWindow.axaml:8) → OnKeyDown
(MainWindow.axaml.cs:136–151) → ShellKeybindings.Handle
(ShellKeybindings.cs:82–113), which handles exactly four keys:
| Key | ShellKeybindings line |
Effect in Slicer |
|---|---|---|
P |
ShellKeybindings.cs:84–89 |
Status "Playback unavailable in Slicer." (MainWindow.axaml.cs:142) — deliberate stub |
Left |
:105–110 |
Same playback-unavailable status (:144) |
Right |
:98–103 |
Same playback-unavailable status (:143) |
Ctrl+L |
:91–96 |
Toggles Shell.IsRightExpanded + status (:145–149) |
Playback is absent by design: no PlaybackPanel (PanelPlayback) is mounted, the activity tab
is hidden via IsPlaybackTabVisible="False" (MainWindow.axaml:20,
MainWindowShell.axaml.cs:180), and the comment at MainWindow.axaml:89–90
states the panel is intentionally omitted. No scrubber gestures exist in this
app — the scrubber lives in the Simulator only.
Chrome: collapse, splitters, activity tabs, status bar¶
All shell-internal; the app's only subscription is the activity-tab click.
| Element | Declared | Trigger → handler | Effect |
|---|---|---|---|
| Collapse-left button "◀" | MainWindowShell.axaml:44 |
OnCollapseLeftClick (MainWindowShell.axaml.cs:294–297) |
IsLeftExpanded=false → caches width, hides splitter (UpdateLeftLayout, :324–380) |
LeftCollapsedContent "▶" |
MainWindowShell.axaml:52 |
OnExpandLeftClick (:299–302) |
Restores _lastLeftWidth (:354) |
| Collapse-right button "▶" | MainWindowShell.axaml:82 |
OnCollapseRightClick (:304–307) |
IsRightExpanded=false → UpdateRightLayout(false) (:382–438) |
RightCollapsedContent "◀" |
MainWindowShell.axaml:105 |
OnExpandRightClick (:309–312) |
Restores _lastRightWidth (:412) |
LeftSplitter / RightSplitter |
MainWindowShell.axaml:61, :70 |
GridSplitter drag (Avalonia built-in) | Resizes columns 0/2 and 2/4; dragged width is what the next collapse caches |
TabOverlays |
MainWindowShell.axaml:86 |
Activity.HandleClick → TabClicked("overlays") |
Panels.Show("overlays") — tooltip "Overlays (T/S/C/W)" is misleading: T/S/C/W hotkeys do not exist |
TabFindings |
MainWindowShell.axaml:89 |
TabClicked("findings") → OnActivityTabClicked (MainWindow.axaml.cs:106–115) |
Panels.Show("findings", onShown: LoadFindings()) |
TabPlayback |
MainWindowShell.axaml:92 |
— | Hidden and never registered in Slicer (MainWindowShell.axaml.cs:172–188) |
StatusBar |
MainWindow.axaml:95–98 |
— | Bound to Shell.StatusText; every Status() call also mirrors to stdout+stderr via StatusSink.Emit (MainWindow.axaml.cs:665–669, StatusSink.cs:57–62) — two independent paths |
Dead code: OnActivityTabClick(object?, RoutedEventArgs)
(MainWindow.axaml.cs:504–507) forwards to Shell.Activity.HandleClick but
has no callers and no XAML hookup.
Known limits visible from this map
- The layer slider is hard-capped at 10 and not driven by the generated
layer count (
MainWindow.axaml:28,MainWindow.axaml.cs:44). - There is no file picker; only the primary Benchy fixture at a known
path can be loaded (
MainWindow.axaml.cs:162–167,FixtureLocator.cs:41). - The CLI preflight generates with
NumLayers: 0(auto) while the UI defaults to 5 (Program.cs:59–64vsSliceParams.cs:44).
See also¶
- Slicer — how it is for the pipeline narrative behind these handlers.
- Slicer UI workflows for the click-level chain of every
workflow, each hop with
file:lineand its test coverage. - Simulator — interaction map for the same map with playback; the toolbar, slider, overlay, and gesture blocks are near-verbatim clones of this one.
- Duplication audit for which of these blocks the analysis says to move into the shell.