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:180–184) → status bar viaShell.StatusText(:684) + stderr/stdout mirror viaStatusSink.Emit(:685) → return. - Facade load:
LoadResult load = _session.LoadModel(benchy)(MW.axaml.cs:186) →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:187–191), message read back viaGetLastErrorMessage(EH/EngineSession.cs:214–216→EC:2855→EC:971); return. - Success:
_modelLoaded = true(MW.axaml.cs:193); status line (:198);MeshInfoLabel.Text = "Plates: … | Pal: … | V=16562 T=33097 | millimeter"(:199–203); thenRunGenerate()(:205) — 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:198-203)"
H->>F: "GenerateDemoToolpath(options) (MW.axaml.cs:278)"
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:293-303)"
V->>A: "ViewportSetGcodeMoves (ViewportController.cs:245)"
A->>E: "viewport copies moves (EC:5274 -> EC:5714)"
H->>F: "Analyze(PLA 210/60) (MW.axaml.cs:306)"
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:313-314)"
H->>V: "force thermal overlay + BuildBuffers (MW.axaml.cs:322-329)"
V->>A: "ViewportApplyOverlay (ViewportController.cs:547)"
A->>E: "EC:5363"
H->>H: "RenderViewport + final status (MW.axaml.cs:330-334)"
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: the FixtureLocator walk permutations (no walk-order unit
test); the exact MeshInfoLabel text (only the "Plates:" prefix is
asserted).
COVERED (nps-oro.3): AppHeadlessSmokeTests.cs:367 (missing-fixture →
"ERROR: primary 4-colors+Benchy+AMS+test-v2.3mf required...", engine never
called), :420 (invalid-file → "Load failed: <code> <msg>", _modelLoaded
stays false, engine session stays valid), :479 (Benchy success →
"Slicer parity loaded ...", _modelLoaded==true, viewport.MoveCount>0,
MeshInfoLabel populated). LoadAndAnalyzeFromPath is the production seam
for the test surface; the on-click handler still resolves the fixture.
3. Load real G-code (.gcode / .gco / .nc / .bgcode)¶
Goal: load a user-picked G-code file directly (text or binary) — bypasses
the slice-from-mesh path so a real production slice can be opened without
needing the source 3MF. Parse failures surface as a status-bar error.
Preconditions: workflow 1 done; _session valid.
- Click "Load G-code" (
MW.axaml:61–64) →OnLoadGcodeClick(MW.axaml.cs:272). - Session guard — silent return (
MW.axaml.cs:274–277). - Picker resolve:
ResolveGcodePicker()(MW.axaml.cs:88–98) — first call constructsAvaloniaGcodeFilePicker(this)(SH/Controls/GcodeFilePicker.cs:191–265) wrapping the window'sStorageProvider; subsequent calls reuse the cached instance. - Picker call:
await picker.PickGcodeAsync()(MW.axaml.cs:283) →AvaloniaGcodeFilePicker.PickGcodeAsync(GcodeFilePicker.cs:206–264): buildsFilePickerOpenOptions { AllowMultiple=false, FileTypeFilter= [G-code: *.gcode/*.gco/*.nc, Binary G-code: *.bgcode] }, callsprovider.OpenFilePickerAsync(options); cancellation returnsnull; I/O failure returnsGcodePickResult.Failure(message). - Dispatch by extension:
LoadGcodeFromBytes(displayName, extension, bytes)(MW.axaml.cs:316)..gcode / .gco / .nc / .txt→Encoding.UTF8.GetString(bytes)→EngineSession.ParseGcode(text)(EH/EngineSession.cs:124–147);.bgcode→EngineSession.ParseBgcode(bytes)(:158–176); unknown extension →Status("Load G-code: unsupported extension '...'")+ return. - UI mirror invalidation BEFORE parse:
InvalidateToolpathUiState()(MW.axaml.cs:562def; called atMW.axaml.cs:330) clears_findings / _findingMoveIndices / _warpGridDims, pushesSetMoves(IntPtr.Zero, UIntPtr.Zero)to the viewport, clears the canvas, and clears the findings panel — the engine drops the move buffer on every parse call (success or fail), so the stale pointer must be evicted pre-parse. - Parse facade (
EH/EngineSession.cs:124–197): clears the move mirror, routes bytes/text toNpsAbi.ParseGcode/NpsAbi.ParseBgcode(EH/NpsAbi.cs:379, 382→EC:3621/EC:3407), then snapshots the engine-owned move buffer via the sharedSnapshotMoveshelper (EngineSession.cs:183–197) — pointer + count +HasMovesall captured identically toGenerateDemoToolpath. - Failure branch:
!pr.Succeeded→Status("Load G-code failed: code=... <engine message>")(MW.axaml.cs:348–351); engine diagnostic read back viaGetLastErrorMessage(EH/EngineSession.cs:201–207). Return without presenting. - No-moves branch:
pr.Succeeded && !pr.HasMoves→Status("Load G-code: '<name>' parsed but produced no moves (empty or non-G-code text).")(MW.axaml.cs:354–357). The tolerant text parser returns success for empty / commandless input; the UI distinguishes "parse error" from "parsed but no moves". - Success:
_modelLoaded = false(MW.axaml.cs:363) — direct G-code replaces any prior model; Apply must not regenerate a stale Benchy toolpath from a model that is no longer in the engine. - Present:
PresentCurrentToolpath(movesPtr, movesCount, isDemo:false, demoLabel)(MW.axaml.cs:465def; called atMW.axaml.cs:366): same chain as the demo-gen path —_viewport.SetMoves / SetCamera / SetSimulationState / BuildBuffers, derive_maxLayerfrom actual moves (ResolveMaxLayerFromViewportMW.axaml.cs:538) and growLayerSlider.Maximumpast the legacy hard-coded 10,Analyze,LoadFindings,LoadWarpGrids, default to thermal overlay, repaint. TheisDemo:falseflag suppresses the demo status line; the picker-supplieddemoLabelis the user-visible "G-code loaded (text 'name'): N moves" string instead. - Final status:
Status("G-code loaded: '<name>' N moves (text|binary). Overlay + findings ready.")(MW.axaml.cs:371).
Tested by: AppHeadlessSmokeTests.SlicerApp_LoadGcodeButton_*
(AppHeadlessSmokeTests.cs): …_ScriptedPicker_Parses_Analyzes_Presents
(text payload → status "G-code loaded", viewport + HasMoves > 0,
_modelLoaded==false), …_CancelledPicker_StatusesCancellation
(picker returns null → status "cancelled", HasMoves==false).
EngineSessionTests.ParseGcode_AsciiText_ReturnsSuccessWithMoves,
…_EmptyText_…, …_CommandlessText_…, …_AfterDispose_… cover the
session-shape facade. ParseBgcode_InvalidBytes_ReturnsFailureWithParseError
and …_AfterDispose_… cover the binary path. Text + bgcode extension
dispatch is exercised in the headless app tests.
NOT COVERED: the real Avalonia StorageProvider.OpenFilePickerAsync
UI under Avalonia.Headless (the platform is not deterministic there);
manual smoke only.
5. 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:268) — reached from the Load button (:205) or Apply (workflow 6). - Options:
_sliceParams.ToGcodeGenOptions()(MW.axaml.cs:275;SliceParams.cs:88–96). - Facade:
_session.GenerateDemoToolpath(options)(MW.axaml.cs:278) →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, singlereturn NPS_RESULT_SUCCESS;). 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. Gen-failed branch unreachable today:Engine::GenerateGcodereturnsNPS_RESULT_SUCCESSunconditionally (EC:1433), soif (!gr.Succeeded)(MW.axaml.cs:279-283) is dead code; covered engine-side byEngineSessionTests.GenerateDemoToolpath_AfterBenchyLoad_ReturnsMoves(EH/EngineSession.Tests/EngineSessionTests.cs:166). Zero-moves branch reachable: statusMW.axaml.cs:287. - Viewport feed:
_viewport.SetMoves(gr.MovesPointer, gr.MovesCount)(MW.axaml.cs:293) →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:294),SetCamera(new CameraState(0.6f, 0.4f, 0f, 0f, 1.0f))(:295— field order RotationXRadians, RotationZRadians, PanX, PanY, Zoom,SH/ViewportController.cs:72–77),SetSimulationState(false, movesCount-1)(:298–301),BuildBuffers()(:303→NpsAbi.ViewportBuildBuffers→EC:5293→GcodeViewport::BuildRenderBuffersEC:5861).- Paint (return path):
RenderViewport()(MW.axaml.cs:614–655): clears the canvas (:623), reads toolbar toggles (:636–637), calls_toolpathPainter.RenderTo(canvas, vp, w, h, _maxLayer, showTravels, showExtrudes, maxMoveIndexExclusive: vp.MoveCount, _findingMoveIndices)(:639–648) →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:650–654viaTryGetLastMovePosition,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: painter happy-path drawing (lines/markers on a real canvas).
COVERED (nps-oro.3): the Apply click → RunGenerate chain via the
Benchy-success path AppHeadlessSmokeTests.cs:572
(ApplySliceButton.RaiseEvent(...) after load → "Slicer parity loaded...")
and the Apply-before-load gate :531 ("Load a model first...",
_modelLoaded==false).
6. 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:306) →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,void) →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). Analyze-failed branch unreachable today:EngineSession.Analyzeunconditionally returnsAnalyzeResult.Success()(EH/EngineSession.cs:368) because the C entry point is void;if (!ar.Succeeded)(MW.axaml.cs:307-311) is dead code on the current build. Covered engine-side byEngineSessionTests.Findings_AreEmptyBeforeAnalyze_AndPopulatedAfter(EngineSessionTests.cs:185). - Load findings:
LoadFindings()(MW.axaml.cs:313→:383–402) rewirespanel.FindingSelected(unsubscribe-then-subscribe,:390–391) →UpdateFindingsFromSession()(:405–427) →FindingFormatter.ProjectFromSession(_session, gridLookup, moveIndices)(:417–422): 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:426→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:438–498): prefetch-cache hit viapanel.TryGetCachedItem(:448–451→FindingsPanel.axaml.cs:123–133, no engine call), else keep stamped row (:452–457), else one-shotFindingFormatter.RefreshDetail(:458–463); genuine miss → status + keep row visible (:465–472). - Navigate + highlight:
_maxLayer = Math.Max(_maxLayer, stamped.Layer + 1)(MW.axaml.cs:478–481);SetLayerVisibility(:485);_viewport.SetSimulationState(true, stamped.MoveIndex)(:486–491→NpsAbi.ViewportSetSimulationState→EC:5334) — highlight only, no playback mirror (comment:436);BuildBuffers(:493);UpdateLayerLabel()(:496);RenderViewport()(:497) — 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: painter marker rendering.
COVERED (nps-oro.3): app-level OnFindingSelected hop-2 (cache tiers,
layer bump, viewport.SetSimulationState) via AppHeadlessSmokeTests.cs:727
— drives FindingsPanel.FindingSelected for a pinned move (layer=3,
moveIndex=99) and asserts _maxLayer>=4 + viewport.SimMoveIndex==99. The
OnFindingSelected RenderViewport call overwrites StatusText after the
"Finding ..." line, so the sim-index mirror is the observable contract.
7. 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:535).
- 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(:526–530):_currentOverlayMode = modeId(:528);ApplyCurrentOverlay()(:529→:533–575). - Facade:
_viewport.ApplyOverlay(_session, mode, false)(MW.axaml.cs:541) →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:542); legendLegendLow/High,StatsText,IsOverlayActive(:547–563);RenderViewport()(:566) recolors viaNpsAbi.ViewportGetHeatmapColor(GcodeToolpathPainter.cs:171–184→EC:5381→ blue→red rampEC:6186); status"Overlay '{mode}' applied (Slicer parity)."/"Overlay cleared…"(:567–574). - "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:322–327).
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.
COVERED (nps-oro.3): the radio → ModeChanged → app chain incl. "None" → motion-type at UI level via AppHeadlessSmokeTests.cs:657 — after Benchy success, reflects OverlayPanel.ModeChanged with modeId="motion-type", asserts _currentOverlayMode=="motion-type", overlayPanel.IsOverlayActive==false, and that the status reflects "Overlay cleared" / "Overlay 'motion-type'". Slicer twin of AppHeadlessSmokeTests.cs:998 (Simulator).
NOT COVERED: legend/stats text; warp-risk SetWarpParams path.
8. 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:217–221).
- 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:209–228): session guard → status "Engine session not ready." (:211–215);_modelLoadedguard → status "Load a model first (Load + Gen + Analyze)." (:217–221). HarvestSliceParamsFromUi()(MW.axaml.cs:226→:232–263): re-reads eachNumericUpDown/CheckBoxviaFindControlinto_sliceParams(belt-and-braces against decimal/float binding lag).RunGenerate()(MW.axaml.cs:227) — 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=..,:332–334).- Warp grid:
LoadWarpGrids()(MW.axaml.cs:314→:338–380): loops layers 0..5 calling_session.GetWarpGrid(l)(:353→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(:358) for the findingsgrid[x,y]lookup; risk floats deliberately discarded (:367). - 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:377–379).
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).
9. 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:618).
- 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(:577–581):_isDragging=true, cache pointer. - Drag →
ViewportPointerMoved(MainWindowShell.axaml.cs:36) →OnViewportPointerMove(MW.axaml.cs:583–608): left button → orbit (RotationZRadians += dx*0.01,RotationXRadiansclamp ±1.5,:595–600); other button → pan scaled0.5 / max(0.1, Zoom)(:601–605);_viewport.SetCamera(cam)(:606→SH/ViewportController.cs:262–273→NpsAbi.ViewportSetCamera(EH/NpsAbi.cs:401) →EC:5286→GcodeViewport::SetCameraEC:5761). - Return:
RenderViewport()(MW.axaml.cs:607) — 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:610). - 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:657–679→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.
10. 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 — bead nps-ai2.4 simplified the overlay tab tooltip to "Overlays" (MainWindowShell.axaml:86).
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.
11. 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 twin removed by bead nps-ai2.4: the previousOnActivityTabClick(object?, RoutedEventArgs)(MW.axaml.cs:504–507) had no callers and no XAML hookup; onlyOnActivityTabClicked(the liveShell.Activity.TabClickedsubscriber) remains. - 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:684) andStatusSink.Emit(:685→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.