Skip to content

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)" LayerHeightSliceParams.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)" FeedrateSliceParams.cs:57–61 1 / 30000 / 100 1800m (SliceParams.cs:41) UI-only until Apply
SliceFlowScale MainWindow.axaml:44–47 NumericUpDown "Volumetric Flow Scale" VolumetricFlowScaleSliceParams.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" EnableNonPlanarSliceParams.cs:71–75 false (SliceParams.cs:43) UI-only until Apply
SliceNumLayers MainWindow.axaml:53–56 NumericUpDown "Number of Layers" NumLayersSliceParams.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 ValueChangedOnLayerSliderChanged (MainWindow.axaml.cs:483–493) _viewport.SetLayerVisibilityNpsAbi.ViewportSetLayerVisibility (ViewportController.cs:252–257) + BuildBuffersNpsAbi.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)" ClickOnLoadAnalyzeClick (MainWindow.axaml.cs:155–189) EngineSession.LoadModelNpsAbi.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" ClickOnApplySliceClick (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 runs RunGenerate(). It does not harvest the sidebar values; first click uses the SliceParams defaults.
  • Apply / Regenerate additionally requires _modelLoaded (MainWindow.axaml.cs:200–204) and calls HarvestSliceParamsFromUi() first (215–246), copying each NumericUpDown / CheckBox value into _sliceParams before the same RunGenerate().

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: SliceParamsGcodeGenOptionsNpsGcodeGenParams

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) :85ZoomFitCamera() (: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.SetCameraNpsAbi.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:99OnOverlayModeChanged (: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:38OnChipAllClick FindingsPanel.axaml.cs:268–274), severity chips ChipCritical, ChipWarning, ChipInfo (:40–44OnChipSeverityClick :276–303), and seven category chips ChipCatThermal, ChipCatStructural, ChipCatMotion, ChipCatHole, ChipCatRetraction, ChipCatFlow, ChipCatUnknown (:52–64OnChipCategoryClick :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) LoadFindingsFindingFormatter.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.RefreshDetailGetAnalysisFindingsDetail; 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=falseUpdateRightLayout(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.HandleClickTabClicked("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–64 vs SliceParams.cs:44).

See also