Slicer — UI workflows¶
The complete click-level map of everything a user can do in SlicerApp. Each
workflow lists its goal, preconditions, and the full hop chain — gesture →
handler → panel event → app subscription → facade → NpsAbi export →
nps::Engine / L0 internals → return path → exact UI updates — with
file:line at every hop, then the tests that cover it. All wiring is
code-behind events; there is no MVVM/ICommand anywhere. Path shorthand:
MW = src/SlicerApp/MainWindow.axaml(.cs), SH = src/Nps.Shell,
EH = src/Nps.EngineHost, EC = src/Engine/engine.cpp.
1. Boot & first paint¶
Goal: dotnet run → composed window with chrome, sidebars, empty
viewport, ready status — before any user click.
Preconditions: libNPSEngine.so locatable; the repo-root fixture matters
only for the CLI preflight (the GUI boots even when preflight fails).
Program.Main(src/SlicerApp/Program.cs:14,[STAThread]:13).- ABI gate:
EngineSession.EnsureAbiMatchesHost()(Program.cs:22; bodyEH/EngineSession.cs:104–113) →GetRuntimeAbiVersion(EH/EngineSession.cs:93–96) →NpsAbi.GetAbiVersion(EH/NpsAbi.cs:241) → C exportGetAbiVersion(EC:2816) returnsNPS_ABI_VERSION= 1. Mismatch → throw, caught atProgram.cs:70–78and dual-written to stdout+stderr. - CLI preflight session:
EngineSession.CreateAndInitialize()(Program.cs:27) →NpsAbi.CreateEngine(EH/EngineSession.cs:184) →CreateEngine(EC:2789) →new nps::Engine()(EC:2790; ctorEC:469);NpsAbi.InitializeEngine(EH/EngineSession.cs:191) →EC:2800. - Preflight load + demo gen:
FixtureLocator.FindPrimaryBenchy3Mf()(Program.cs:34, resolverSH/FixtureLocator.cs:60–90) →session.LoadModel(benchy)(Program.cs:35) →GenerateDemoToolpath(new GcodeGenOptions(false, 0.2f, 1.0f, 1800f, 0))(Program.cs:59–64— noteNumLayers: 0here vs GUI default 5) → prints stats; session disposed (DestroyEngine,EC:2793). - Avalonia boot:
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args)(Program.cs:80–81);ShellTrace.InstallStderrSink()(Program.cs:90);App.Initialize()(src/SlicerApp/App.axaml.cs:9–12);desktop.MainWindow = new MainWindow()(App.axaml.cs:14–19). MainWindow()ctor:InitializeComponent()+InitializeShell()(MW.axaml.cs:61–62). The shell self-composes:MainGridrows/cols (SH/Controls/MainWindowShell.axaml:13–25), registers "overlays"/"findings" tabs (MainWindowShell.axaml.cs:148–155), hidesTabPlaybackbecauseIsPlaybackTabVisible="False"(:172–188, set atMW.axaml:20).InitializeShell(MW.axaml.cs:65–104): left sidebarDataContext = _sliceParams(:68–72); subscribes activity tab (:74), viewport pointer events (:76–78), 10 toolbar events (:80–89),OverlayPanel.ModeChanged(:96–100);Status("Slicer shell ready.")(:91);_session = EngineSession.CreateAndInitialize()(:93— a second engine instance);_viewport = ViewportController.CreateAndInitialize()(:94) →NpsAbi.CreateGcodeViewport(SH/ViewportController.cs:222) →new nps::GcodeViewport()(EC:5266–5268).- Shell
OnLoaded(MainWindowShell.axaml.cs:212–254): auto-discoversPanelOverlays/PanelFindings, shows "overlays" by default (:250), attaches viewport pointer forwarders (AttachViewportHandlers,:256–276). - First paint: chrome + empty
ViewportCanvas. Initial state:_maxLayer=10(MW.axaml.cs:44),_currentOverlayMode="motion-type"(:57),_modelLoaded=false(:51), camera rotX 0.6 / rotZ 0.4 / zoom 1.0 (SH/ViewportController.cs:139–144).
Tested by: AppHeadlessSmokeTests.SlicerApp_MainWindow_Loads_Layout_And_Bindings
(tests/NPSEngine.Tests/AppHeadlessSmokeTests.cs:58 — title, ready status,
controls, IsPlaybackTabVisible false, PanelPlayback null);
EngineBridgeTests.GetAbiVersion_MatchesExpectedAtStartup
(EngineBridgeTests.cs:194);
EngineSessionTests.CreateAndInitialize_ProducesValidSession
(EngineSessionTests.cs:48);
ViewportControllerTests.CreateAndInitialize_ProducesValidViewport
(ViewportControllerTests.cs:31).
NOT COVERED: the CLI preflight in Program.cs:27–67 (no headless test runs
Program.Main); StatusSink.Emit stderr mirroring (only the disabled
scripts/smoke-apps.sh exercised it).
2. Load model via "Load + Gen + Analyze (Benchy primary)"¶
Goal: resolve the fixture, load the 3MF through the engine, show mesh
counts/plate/palette/unit.
Preconditions: workflow 1 done; _session valid.
- Click "Load + Gen + Analyze (Benchy primary)" (
MW.axaml:58–60) →OnLoadAnalyzeClick(MW.axaml.cs:155). - Session guard — silent return (
MW.axaml.cs:157–160). - Fixture resolve:
FixtureLocator.FindPrimaryBenchy3Mf()(MW.axaml.cs:162) → walk up to 12 levels probing4-colors+Benchy+AMS+test-v2.3mf(SH/FixtureLocator.cs:60–90, filename const:41). - Existence gate:
!File.Exists→Status("ERROR: primary … required. Place at repo root.")(MW.axaml.cs:163–167) → status bar viaShell.StatusText(:667) + stderr/stdout mirror viaStatusSink.Emit(:668) → return. - Facade load:
LoadResult load = _session.LoadModel(benchy)(MW.axaml.cs:169) →EngineSession.LoadModel(EH/EngineSession.cs:202) clears the move snapshot (:207–208) →NpsAbi.LoadModelFromFile(:211; DllImportEH/NpsAbi.cs:252) → C export (EC:2820) →nps::Engine::LoadModelFromFile(EC:540→ options overloadEC:550): clears prior mesh + all three gcode buffers (EC:567–577), preflight (exists/regular/≤512 MiB,EC:597–618), magic sniffPK\3\4(EC:641–644),detail::ZipArchiveReader.open(EC:657), extract3D/3dmodel.model+3D/Objects/object_1.model(EC:666–684), metadata configs (EC:690–714),ParseModelXmlObjectsroot+geo (EC:718–726), base-extruder scan (EC:748–793), palette viaColorParser(EC:800–809),MeshResolver::resolve(EC:871), single plate"plate_1"(EC:883–885), AABB (EC:908–940), thumbnail (EC:953–958), plate pushed (EC:961). - Facade return-path queries (
EH/EngineSession.cs:219–244):GetPlateCount, per-plateGetTriangleCountForPlate/GetVertexPositions,GetPaletteColorCount/GetPaletteColor,GetModelUnit(empty → "mm":239) →LoadResult.Success. - Failure branch:
Status($"Load failed: {code} {message}")(MW.axaml.cs:170–174), message read back viaGetLastErrorMessage(EH/EngineSession.cs:214–216→EC:2855→EC:971); return. - Success:
_modelLoaded = true(MW.axaml.cs:176); status line (:181);MeshInfoLabel.Text = "Plates: … | Pal: … | V=16562 T=33097 | millimeter"(:182–186); thenRunGenerate()(:188) — workflow 3.
sequenceDiagram
autonumber
actor U as User
participant B as "Load button (MW.axaml:58)"
participant H as "OnLoadAnalyzeClick (MW.axaml.cs:155)"
participant F as "EngineSession (EH)"
participant A as "NpsAbi (EH)"
participant E as "nps::Engine (EC)"
participant V as "ViewportController (SH)"
U->>B: "click: Load + Gen + Analyze"
B->>H: Click event
H->>H: "guard session, FixtureLocator (MW.axaml.cs:157-167)"
H->>F: "LoadModel(benchy) (MW.axaml.cs:169)"
F->>A: "LoadModelFromFile (EngineSession.cs:211)"
A->>E: "C export LoadModelFromFile (EC:2820)"
E-->>F: "LoadResult: plates/palette/unit (EC:968)"
F-->>H: LoadResult
H->>H: "MeshInfoLabel + status (MW.axaml.cs:181-186)"
H->>F: "GenerateDemoToolpath(options) (MW.axaml.cs:261)"
F->>A: "GenerateGcode (EngineSession.cs:281)"
A->>E: "demo toolpath, shared moves (EC:2947 -> EC:1215)"
E-->>F: "moves ptr+count (EC:3031/3037)"
F-->>H: GenerateResult
H->>V: "SetMoves / SetLayerVisibility / SetCamera (MW.axaml.cs:276-286)"
V->>A: "ViewportSetGcodeMoves (ViewportController.cs:245)"
A->>E: "viewport copies moves (EC:5274 -> EC:5714)"
H->>F: "Analyze(PLA 210/60) (MW.axaml.cs:289)"
F->>A: "AnalyzeGcode (EngineSession.cs:361)"
A->>E: "6 analyzers (EC:5421 -> EC:1479 -> EC:4909)"
H->>F: "findings summary + warp grids (MW.axaml.cs:296-297)"
H->>V: "force thermal overlay + BuildBuffers (MW.axaml.cs:305-312)"
V->>A: "ViewportApplyOverlay (ViewportController.cs:547)"
A->>E: "EC:5363"
H->>H: "RenderViewport + final status (MW.axaml.cs:313-317)"
Tested by: EngineSessionTests.LoadModel_OnBenchy3Mf_ReturnsStructuredPlatesAndPalette
(EngineSessionTests.cs:95); LoadModel_ClearsPriorMoveBuffer (:142);
EngineBridgeTests.LoadModelFromFile_OnBenchy3Mf_SucceedsWithAssertion
(EngineBridgeTests.cs:292); error codes EngineSessionTests.LoadModel_MissingFile_ReturnsFailureWithMessage
(:127), EngineBridgeTests.cs:446/:465/:555; C++ Catch2
src/Engine/tests/engine_tests.cpp:287/:621/:1092 (golden 16562 v / 33097 t /
4 palette colors); lefthook fixture-present gate (lefthook.yml:139–140).
NOT COVERED: OnLoadAnalyzeClick itself (existence-only in
AppHeadlessSmokeTests.cs:58); the File.Exists gate message; the exact
status/MeshInfoLabel strings; the FixtureLocator walk permutations.
3. Generate demo toolpath¶
Goal: produce a demo toolpath (square perimeter + diagonal infill per
layer, sized from mesh bounds — not a production slice) and paint it.
Preconditions: _modelLoaded == true (workflow 2 step 8).
- Entry:
RunGenerate()(MW.axaml.cs:251) — reached from the Load button (:188) or Apply (workflow 6). - Options:
_sliceParams.ToGcodeGenOptions()(MW.axaml.cs:258;SliceParams.cs:88–96). - Facade:
_session.GenerateDemoToolpath(options)(MW.axaml.cs:261) →EngineSession.GenerateDemoToolpath(EH/EngineSession.cs:253): packsNpsGcodeGenParamsviaAllocHGlobal+StructureToPtr(:271–290) →NpsAbi.GenerateGcode(:281; DllImportEH/NpsAbi.cs:333) → C exportGenerateGcode(EC:2947) →nps::Engine::GenerateGcode(EC:1215): clearslastGeneratedGcode+ sharedlastGcodeMoves(EC:1216–1223), auto layer count clamp 2..12 whenNumLayers ≤ 0(EC:1236–1246), emits moves viaappendMove(EC:1151), publishes text + shared move array (EC:1432). Emitted header says "demo toolpath … NOT a production slice" (EC:1314–1317). - Return:
GetLastGcodeMoveCount(EH/EngineSession.cs:295→EC:3031→EC:1460) +GetLastGcodeMoves(:303→EC:3037→EC:1464) snapshot intoLastMovesPointer/Count. Failure → statusMW.axaml.cs:264; zero moves → status:270. - Viewport feed:
_viewport.SetMoves(gr.MovesPointer, gr.MovesCount)(MW.axaml.cs:276) →ViewportController.SetMoves(SH/ViewportController.cs:240–246) →NpsAbi.ViewportSetGcodeMoves(exact export name;EH/NpsAbi.cs:395) → C export (EC:5274) →GcodeViewport::SetGcodeMoves(EC:5714) which copies the moves intoownedMoves_(EC:5733–5736). SetLayerVisibility(_maxLayer)(MW.axaml.cs:277),SetCamera(new CameraState(0.6f, 0.4f, 0f, 0f, 1.0f))(:278— field order RotationXRadians, RotationZRadians, PanX, PanY, Zoom,SH/ViewportController.cs:72–77),SetSimulationState(false, movesCount-1)(:281–284),BuildBuffers()(:286→NpsAbi.ViewportBuildBuffers→EC:5293→GcodeViewport::BuildRenderBuffersEC:5861).- Paint (return path):
RenderViewport()(MW.axaml.cs:597–638): clears the canvas (:606), reads toolbar toggles (:619–620), calls_toolpathPainter.RenderTo(canvas, vp, w, h, _maxLayer, showTravels, showExtrudes, maxMoveIndexExclusive: vp.MoveCount, _findingMoveIndices)(:622–631) →GcodeToolpathPainter.RenderTo(SH/Controls/GcodeToolpathPainter.cs:85–227): per move pairNpsAbi.ProjectOrbitXY(:160–163→EC:5400), heatmap when overlay active (:171–184viaNpsAbi.ViewportGetHeatmapColor→EC:5381→EC:6186), green extrude / grey travel otherwise (:188–196), oneLineper segment (:199–210), yellow 8×8 ellipses at finding moves (:212–225). Status coords line (MW.axaml.cs:633–637viaTryGetLastMovePosition,SH/ViewportController.cs:694–710).
Tested by: engine gen Catch2 engine_tests.cpp:2538/:2637/:2700/:2780;
xUnit EngineBridgeTests.cs:744,
EngineSessionTests.GenerateDemoToolpath_AfterBenchyLoad_ReturnsMoves
(EngineSessionTests.cs:166); viewport copy Catch2
engine_tests.cpp:4164/:4275, xUnit ViewportControllerTests.cs:138;
painter no-op guards GcodeToolpathPainterTests.cs:19/:42.
NOT COVERED: the full click → RunGenerate chain; painter happy-path
drawing (lines/markers on a real canvas).
4. Analyze & findings browse/filter/select¶
Goal: run the 6-analyzer suite over the shared moves; browse, filter, and select findings with detail + viewport highlight. Preconditions: workflow 3 produced moves.
- Analyze:
_session.Analyze(new MaterialProfileOptions("PLA", 210, 60))(MW.axaml.cs:289) →EngineSession.Analyze(EH/EngineSession.cs:332): buildsNpsMaterialProfilewith only Type/MeltTemp/GlassTransition set (:337–347),AllocHGlobal+StructureToPtr(:353–356) →NpsAbi.AnalyzeGcode(:361; DllImportEH/NpsAbi.cs:443) → C export (EC:5421–5459): flat 108-byte POD → groupedMaterialProfile(EC:5427–5454) →nps::Engine::AnalyzeGcode(EC:1479) →AnalysisManager::analyze(EC:4909): clears prior findings (EC:4911), runs six analyzers — Thermal (EC:4320), Structural (EC:4650), Motion (EC:4803), Hole (EC:6606), Retraction (EC:6839), Flow (EC:6923). - Load findings:
LoadFindings()(MW.axaml.cs:296→:366–385) rewirespanel.FindingSelected(unsubscribe-then-subscribe,:373–374) →UpdateFindingsFromSession()(:388–410) →FindingFormatter.ProjectFromSession(_session, gridLookup, moveIndices)(:400–405): 16-byte summary rows first viasession.GetFindingsSummary()(EH/EngineSession.cs:376–401→GetAnalysisFindingsSummaryCount/GetAnalysisFindingsSummary,EC:5498/EC:5501→EC:1513/EC:1520,findingId= source indexEC:1531–1534), then a per-row 300-byte detail prefetch viaGetFindingDetail(EH/EngineSession.cs:407–413→EC:5530→EC:1538); stampsFindingItems, fills_findings+_findingMoveIndices,panel.SetFindings(viewItems)(MW.axaml.cs:409→FindingsPanel.axaml.cs:94–112— also keys the prefetch cache_byFindingId,:100–107). - Show the tab: click
TabFindings(MainWindowShell.axaml:89) →Activity.HandleClick→TabClicked("findings")(ActivityBarHost.cs:58) → app subscriptionMW.axaml.cs:74→OnActivityTabClicked(:106–115) →Shell.Panels.Show("findings", onShown: LoadFindings)→PanelHost.ShowflipsIsVisible(SH/Controls/PanelHost.cs:73–87). - Filter (UI-only, no engine): chips in
FindingsPanel.axaml:38–64→OnChipAllClick(FindingsPanel.axaml.cs:268–274) /OnChipSeverityClick(:276–303) /OnChipCategoryClick(:305–322) → immutableFindingsFilterState(FindingsFilterState.cs:95–135, set semantics) →RebuildListsingleItemsSourcereassign (FindingsPanel.axaml.cs:342–351). No search TextBox exists. - Select a row:
FindingsList(FindingsPanel.axaml:70) →OnFindingSelected(FindingsPanel.axaml.cs:328–332) → hop 1FindingSelected(item)(:165) + panelUpdateDetail(:142–159) → hop 2 appOnFindingSelected(MW.axaml.cs:421–481): prefetch-cache hit viapanel.TryGetCachedItem(:431–434→FindingsPanel.axaml.cs:123–133, no engine call), else keep stamped row (:435–440), else one-shotFindingFormatter.RefreshDetail(:441–446); genuine miss → status + keep row visible (:448–455). - Navigate + highlight:
_maxLayer = Math.Max(_maxLayer, stamped.Layer + 1)(MW.axaml.cs:461–464);SetLayerVisibility(:468);_viewport.SetSimulationState(true, stamped.MoveIndex)(:469–474→NpsAbi.ViewportSetSimulationState→EC:5334) — highlight only, no playback mirror (comment:419);BuildBuffers(:476);UpdateLayerLabel()(:479);RenderViewport()(:480) — painter draws the full path plus yellow markers at_findingMoveIndices.
Tested by: analyzers Catch2 engine_tests.cpp:1550/:1570/:2896/:2915/:2940;
facade EngineSessionTests.cs:185/:211/:233; summary↔detail contract
EngineBridgeTests.TestAnalysisFindingsSummary_RoundTripsThroughDetail
(EngineBridgeTests.cs:1468) + :1548; panel
FindingsPanelTests.cs:43/:73/:94/:130/:156/:189/:218/:248.
NOT COVERED: FindingFormatter.ProjectFromSession itself; the app-level
LoadFindings rewiring and tab-shown reload; app OnFindingSelected hop-2
(cache tiers, layer bump, highlight); painter marker rendering.
5. Inspect overlays (None=motion-type, Thermal/Heat, Warp Risk)¶
Goal: recolor extrudes by an analysis scalar; legend + stats update.
Preconditions: moves + analysis loaded (guard MW.axaml.cs:518).
- Click a radio (
OverlayPanel.axaml:10–14) →OnRadioButtonChecked(OverlayPanel.axaml.cs:98–108) → setsSelectedModestyled prop (:102–105) → hop 1ModeChanged(modeId)(:66). - Hop 2: app subscription
MW.axaml.cs:99→OnOverlayModeChanged(:509–513):_currentOverlayMode = modeId(:511);ApplyCurrentOverlay()(:512→:516–558). - Facade:
_viewport.ApplyOverlay(_session, mode, false)(MW.axaml.cs:524) →ViewportController.ApplyOverlay(session, mode, invert)(SH/ViewportController.cs:564–640): per movesession.GetOverlayValue(mode, m.layer, i)(:588→EH/EngineSession.cs:486–490→NpsAbi.GetOverlayValue→EC:5560→Engine::GetLastOverlayValueEC:1550→AnalysisManager::getOverlayValueEC:4992); stats exclude zeros except Thermal (:593);IsActive = mode != MotionType(:576). - One-shot push:
NpsAbi.ViewportApplyOverlay(SH/ViewportController.cs:547–554; DllImportEH/NpsAbi.cs:432–435) → C export (EC:5363–5376) →GcodeViewport::ApplyOverlay(EC:5829). - Warp Risk extra:
SetWarpParams(true, 5.0f)(SH/ViewportController.cs:627–629) →NpsAbi.ViewportSetWarpParams→EC:5344; other modesSetWarpParams(false, 0)(:633). - Return:
_viewport.BuildBuffers()(MW.axaml.cs:525); legendLegendLow/High,StatsText,IsOverlayActive(:530–546);RenderViewport()(:549) recolors viaNpsAbi.ViewportGetHeatmapColor(GcodeToolpathPainter.cs:171–184→EC:5381→ blue→red rampEC:6186); status"Overlay '{mode}' applied (Slicer parity)."/"Overlay cleared…"(:550–557). - "None" semantics: the None radio maps to mode id
"motion-type"(OverlayPanel.axaml:10Tag) — an active engine mode, not a no-op; the controller marks it inactive and the painter draws flat type colors. After every Load/Apply the app forces"thermal"(MW.axaml.cs:305–310).
Tested by: ViewportControllerTests.ApplyOverlay_FromSession_ComputesStatsAndAppliesOverlay
(ViewportControllerTests.cs:460), …_NonThermal_…SampleCountExcludesZeros
(:509); aggregate parity Catch2 engine_tests.cpp:4065/:4107; heatmap
EngineBridgeTests.cs:1642/:1661/:1675/:1686, Catch2
engine_tests.cpp:2047.
NOT COVERED: the radio → ModeChanged → app chain incl. the
None→motion-type mapping at UI level; legend/stats text; warp-risk auto
SetWarpParams(true, 5) path.
6. Slice params edit + ApplySliceButton + WarpGridLabel¶
Goal: change generation parameters, regenerate the demo toolpath, refresh
the warp-grid summary.
Preconditions: _modelLoaded == true for Apply (MW.axaml.cs:200–204).
- Edit a control (
MW.axaml:34–56) → TwoWay binding intoSliceParams(SliceParams.cs:50–82, INPCSetField:107–116). UI-only — nothing leaves the process; range enforcement is control min/max only. - Click "Apply / Regenerate" (
MW.axaml:61–64) →OnApplySliceClick(MW.axaml.cs:192–211): session guard → status "Engine session not ready." (:194–198);_modelLoadedguard → status "Load a model first (Load + Gen + Analyze)." (:200–204). HarvestSliceParamsFromUi()(MW.axaml.cs:209→:215–246): re-reads eachNumericUpDown/CheckBoxviaFindControlinto_sliceParams(belt-and-braces against decimal/float binding lag).RunGenerate()(MW.axaml.cs:210) — the full workflow-¾/5 chain with the new options (gen → viewport feed → analyze → findings → warp grids → forced thermal → repaint → status echo withLH=.. FR=.. FS=.. NP=.. N=..,:315–317).- Warp grid:
LoadWarpGrids()(MW.axaml.cs:297→:321–363): loops layers 0..5 calling_session.GetWarpGrid(l)(:336→EH/EngineSession.cs:421–443→NpsAbi.GetWarpGridInfo(:426→EC:5594→EC:1572→AnalysisManager::getWarpGridInfoEC:5048, NO_DATA when no grid) +NpsAbi.GetWarpGridRisk(:433→EC:5600→EC:1578→EC:5072, engine-owned floats copied viaMarshal.CopyEH/EngineSession.cs:437–440); caches dims into_warpGridDims(:341) for the findingsgrid[x,y]lookup; risk floats deliberately discarded (:350). - UI:
WarpGridLabel.Text = "WarpGrid: layers={N} e.g. L{l} {W}x{H} res={R:F1}"or"WarpGrid: none populated (check thermal)"(MW.axaml.cs:360–362).
Tested by: VM SliceParamsTests.cs:17/:30/:49/:64; warp ABI
Catch2 engine_tests.cpp:1674 (shape only);
EngineSessionTests.GetWarpGrid_BeforeAnalyze_ReturnsNull
(EngineSessionTests.cs:241).
NOT COVERED: the Apply click chain end-to-end; HarvestSliceParamsFromUi;
WarpGridLabel text; warp-grid risk values (never content-asserted).
7. Camera & navigation (orbit/pan, presets, Zoom to Fit — no wheel zoom)¶
Goal: orbit/pan/zoom the toolpath view.
Preconditions: _viewport valid; visible change only when moves exist
(RenderViewport early-out MW.axaml.cs:601).
- Press on
ViewportHost(MW.axaml:72) → shell-attachedOnViewportPointerPressed(MainWindowShell.axaml.cs:278–281, attached:256–276) → hop 1ViewportPointerPressed(:35) → app subMW.axaml.cs:76→OnViewportPointer(:560–564):_isDragging=true, cache pointer. - Drag →
ViewportPointerMoved(MainWindowShell.axaml.cs:36) →OnViewportPointerMove(MW.axaml.cs:566–591): left button → orbit (RotationZRadians += dx*0.01,RotationXRadiansclamp ±1.5,:578–583); other button → pan scaled0.5 / max(0.1, Zoom)(:584–588);_viewport.SetCamera(cam)(:589→SH/ViewportController.cs:262–273→NpsAbi.ViewportSetCamera(EH/NpsAbi.cs:401) →EC:5286→GcodeViewport::SetCameraEC:5761). - Return:
RenderViewport()(MW.axaml.cs:590) — the painter re-projects every move viaNpsAbi.ProjectOrbitXY(the engine-side camera feeds onlyComputeMVP, which nothing calls). - Release →
ViewportPointerReleased(MainWindowShell.axaml.cs:37) →OnViewportPointerRelease(MW.axaml.cs:593). - Presets (toolbar, code-built
ViewportToolbar.cs:53–58): hop 1 events:18–23(raised:70–75) → app subsMW.axaml.cs:80–85→SetCameraPreset(SH/ViewportController.cs:294–328— Reset 0.6/0.4/zoom 1.0; Isometric 0.615/0.785; Front 1.5/0; Top 0/0; Right 1.5/−1.571; pan zeroed) orZoomFitCamera()(MW.axaml.cs:640–662→ComputeZoomFit(w,h)SH/ViewportController.cs:432–482, ~90% fill viaNpsAbi.ProjectOrbitXY:456) →SetCamera+RenderViewport. - No wheel zoom — zero
PointerWheelChangedhandlers in app, shell, or controller. Zoom changes only via Reset preset and Zoom to Fit.
Tested by: projection Catch2 engine_tests.cpp:2078, xUnit
EngineBridgeTests.cs:1697/:1741/:1753; camera
ViewportControllerTests.cs:84/:253/:305; ZoomFit math
ViewportControllerTests.cs:376/:415/:439; engine MVP Catch2
engine_tests.cpp:1976.
NOT COVERED: the pointer-drag handlers and toolbar → preset/ZoomFit wiring
(ViewportToolbar has zero tests); engine-side ViewportSetCamera visible
effect.
8. Keyboard (Ctrl+L works; P/Left/Right report playback unavailable)¶
Goal: keyboard shortcuts.
Preconditions: window focused (Focusable = true; Focus(),
MW.axaml.cs:102–103).
- Key press → window
KeyDown="OnKeyDown"(MW.axaml:8) →OnKeyDown(MW.axaml.cs:136–151) →ShellKeybindings.Handle(e, actions)(SH/Controls/ShellKeybindings.cs:82–113;Actionsreadonly struct:58–71). P(ShellKeybindings.cs:84–89) →TogglePlay→Status("Playback unavailable in Slicer.")(MW.axaml.cs:142).Right(:98–103) /Left(:105–110) → same status (:143–144).Ctrl+L(:91–96) →ToggleInspector→Shell.IsRightExpanded = !Shell.IsRightExpanded+ status (:145–149).- Every handled key sets
e.Handled = true; others fall through (ShellKeybindings.cs:112). No other keys exist — the overlay tab tooltip "Overlays (T/S/C/W)" (MainWindowShell.axaml:86) is misleading.
Tested by: ShellChromeTests.ShellKeybindings_P_TogglesPlay_And_Handles
(ShellChromeTests.cs:50), …_Mod_L_TogglesInspector_Only_With_Ctrl (:69),
…_Left_Right_Step_And_Handle (:89), …_Unhandled_Key_FallsThrough (:114).
NOT COVERED: the app-level OnKeyDown → status/Ctrl+L wiring.
9. Chrome: collapse buttons, GridSplitters, activity tabs, status bar¶
Goal: layout chrome — collapse/expand sidebars, resize, switch inspector tabs, read status. Preconditions: workflow 1 done.
- Collapse left: "◀" (
MainWindowShell.axaml:44) →OnCollapseLeftClick(MainWindowShell.axaml.cs:294–297) →IsLeftExpanded=false→UpdateLeftLayout(false)(:324–380): caches pixel width_lastLeftWidth(:359–362), hides splitter + expanded content, shows collapsed strip. - Expand left:
LeftCollapsedContent"▶" (MainWindowShell.axaml:52) →OnExpandLeftClick(:299–302) → restores_lastLeftWidth(:354). - Collapse/expand right: "▶" (
MainWindowShell.axaml:82) →OnCollapseRightClick(:304–307) →UpdateRightLayout(false)(:382–438, caches_lastRightWidth:417–420);RightCollapsedContent"◀" (:105) →OnExpandRightClick(:309–312) → restore (:412). - GridSplitters (
MainWindowShell.axaml:61/:70, width 4): Avalonia built-in drag resizes column pairs 0/2 and 2/4; the dragged width is what the next collapse caches. UI-only, no app subscription. - Activity tabs:
TabOverlays/TabFindings(MainWindowShell.axaml:86/:89) wired in the shell ctor toActivity.HandleClick(MainWindowShell.axaml.cs:148–155) → hop 1TabClicked(tag)(ActivityBarHost.cs:58,:80–86) → hop 2 app subMW.axaml.cs:74→OnActivityTabClicked(:106–115) →Shell.Panels.Show(key, onShown)→PanelHost.ShowflipsIsVisible(SH/Controls/PanelHost.cs:73–87);"findings"triggersLoadFindings()(MW.axaml.cs:110–113).TabPlaybackis hidden and unregistered (MainWindowShell.axaml.cs:172–188). Dead code:OnActivityTabClick(object?, RoutedEventArgs)(MW.axaml.cs:504–507) has no callers. - Status bar:
StatusBarTextBlock (MW.axaml:95–98) bound{Binding StatusText, ElementName=Shell}→ styled property (MainWindowShell.axaml.cs:90–97). Every appStatus()call writes bothShell.StatusText(MW.axaml.cs:667) andStatusSink.Emit(:668→SH/StatusSink.cs:57–62, prefix"[SlicerUI]":36, to both stdout and stderr) — two independent paths.
Tested by: AppHeadlessSmokeTests.MainWindowShell_Collapse_Width_Memory_Smoke
(AppHeadlessSmokeTests.cs:152, incl. playback-tab hide);
ShellChromeTests.ActivityBarHost_FiresTabClicked_ForRegisteredKey (:146),
…_NoTabClicked_When_Button_Tag_Unregistered (:130),
PanelHost_Show_FlipsExactlyOneVisible_And_FiresOnShown (:164),
…_UnknownKey_NoOp (:188).
NOT COVERED: GridSplitter drag behavior; StatusSink.Emit dual-write
(disabled smoke script only); Ctrl+L → IsRightExpanded wiring in the app.
See also¶
- Slicer — interaction map — the element-by-element matrix behind these chains.
- Slicer — how it is — pipeline narrative.
- Simulator — UI workflows — the same map with the playback tab, scrubber, and auto-pause.