Skip to content

06 — User and developer flows

Developer-level end-to-end sequences: every hop named, L5 app → L4 shell → L3 EngineHost → L2 C ABI → L1 engine. For the click-level, per-control view of the same gestures see ../../slicer/workflows.md and ../../simulator/workflows.md. Abbreviations: SA = src/SlicerApp, SI = src/SimulatorApp, SH = src/Nps.Shell, EH = src/Nps.EngineHost.

App boot

sequenceDiagram
    participant OS
    participant Program as Program.Main
    participant App as Avalonia App
    participant MW as MainWindow
    participant ES as EngineSession
    participant VP as ViewportController
    participant ABI as "NpsAbi / NPSEngine"

    OS->>Program: start process
    Program->>ABI: ABI gate (version assert)
    Program->>ABI: CLI preflight (session + fixture load)
    Program->>App: BuildAvaloniaApp().StartWithClassicDesktopLifetime
    App->>MW: new MainWindow
    MW->>MW: InitializeShell (wire chrome + events)
    MW->>ES: CreateAndInitialize()
    ES->>ABI: CreateEngine + InitializeEngine
    MW->>VP: CreateAndInitialize()
    VP->>ABI: CreateGcodeViewport
    MW->>MW: "Status(shell ready)"
# Step SlicerApp SimulatorApp
1 Program.Main SA/Program.cs:14 SI/Program.cs:15
2 ABI gate (NPS_ABI_VERSION = 1) facade EngineSession.EnsureAbiMatchesHost() SA/Program.cs:22EH/EngineSession.cs:104 raw NpsAbi.GetAbiVersion() SI/Program.cs:22, throws :23-28
3 CLI preflight session EngineSession.CreateAndInitialize() SA/Program.cs:27, disposed before GUI raw new SafeEngineHandle(NpsAbi.CreateEngine(), true) SI/Program.cs:31
4 Fixture probe session.LoadModel(benchy) SA/Program.cs:35 + demo-gen :59-64 (NumLayers: 0) raw NpsAbi.LoadModelFromFile SI/Program.cs:43; failure caught, GUI still starts :88-96
5 Avalonia boot SA/Program.cs:80-81 (ShellTrace.InstallStderrSink() :90) SI/Program.cs:98-99 (sink :108)
6 MainWindow ctor → InitializeShell SA/MainWindow.axaml.cs:59-65; playback tab hidden (IsPlaybackTabVisible="False" axaml:20) SI/MainWindow.axaml.cs:77-83; playback timer 80 ms :105-106, MeshController created :109
7 GUI engine + viewport _session :93, _viewport :94 _session :108, _mesh :109, _viewport :110

The GUI session in step 7 is a second engine instance; the CLI preflight session is disposed (→ DestroyEngine) before the window opens.

Load → demo gen → analyze → viewport

Primary button path (OnLoadAnalyzeClickSA/MainWindow.axaml.cs:155 / SI/MainWindow.axaml.cs:205).

sequenceDiagram
    participant U as User
    participant MW as MainWindow
    participant FL as FixtureLocator
    participant ES as EngineSession
    participant Eng as nps::Engine
    participant VP as ViewportController
    participant UI as Canvas/Status

    U->>MW: click Load + Gen + Analyze
    MW->>FL: FindPrimaryBenchy3Mf()
    FL-->>MW: path or missing
    alt fixture missing
        MW->>UI: "ERROR: primary 3mf required"
    else ok
        MW->>ES: LoadModel(path)
        ES->>Eng: "LoadModelFromFile (ZIP/OPC → ModelXML → MeshResolver)"
        Eng-->>ES: plates, verts, tris, palette, unit
        ES-->>MW: LoadResult
        MW->>UI: Status mesh summary
        MW->>ES: GenerateDemoToolpath(options)
        Note over ES,Eng: "DEMO TOOLPATH — not production slice"
        ES->>Eng: GenerateGcode
        Eng-->>ES: engine-owned move array
        MW->>VP: SetMoves(LastMovesPointer, count)
        MW->>VP: SetLayerVisibility + SetCamera + BuildBuffers
        MW->>ES: Analyze(profile)
        ES->>Eng: AnalyzeGcode (6 analyzers)
        MW->>MW: LoadFindings (16B summary rows)
        MW->>UI: RenderViewport + Status(DEMO TOOLPATH…)
    end

Ordered steps (code)

# Step App call site Facade / export
1 Resolve fixture SA:162 / SI:212 FixtureLocator.FindPrimaryBenchy3Mf SH/FixtureLocator.cs:60
2 Load 3MF _session.LoadModel SA:169 / SI:219 EngineSession.LoadModel EH/EngineSession.cs:202NpsAbi.LoadModelFromFile EH/NpsAbi.cs:252
3 Mesh snapshot (Sim only) _mesh?.Reload() SI:226 (unconditional — engine clears mesh on every load attempt) MeshController.Reload EH/MeshController.cs:131
4 Demo toolpath SA:261 (SliceParams-driven) / SI:245-250 (hardcoded) EngineSession.GenerateDemoToolpath EH/EngineSession.cs:253NpsAbi.GenerateGcode EH/NpsAbi.cs:333
5 Feed viewport _viewport.SetMoves SA:276 / SI:265 ViewportController.SetMoves SH/ViewportController.cs:240ViewportSetGcodeMoves EH/NpsAbi.cs:395 (exact export name)
6 Layer/camera/buffers SA:277-286 / SI:266-268 SetLayerVisibility :252, SetCamera :262, BuildBuffers :657
7 Analyze SA:289 / SI:272 — both pass MaterialProfileOptions("PLA", 210, 60) EngineSession.Analyze EH/EngineSession.cs:332NpsAbi.AnalyzeGcode EH/NpsAbi.cs:443
8 Findings summary LoadFindings() SA:366 / SI:316 GetFindingsSummary EH/EngineSession.cs:376 (16B rows; 300B detail only on selection)
9 Render RenderViewport() SA:597 / SI:695; Sim paints mesh underlay first SI:732 painters read MovesSpan SH/ViewportController.cs:672

Golden-fixture expectation after step 2: 1 plate / 16562 verts / 33097 tris / 4 palette colors.

Parse path (ParseGcode / ParseBgcode)

No app UI path exists today. Neither app has an open/paste G-code gesture and EngineSession exposes no Parse facade; every caller is a test harness (SH/ViewportController.cs:235 notes the "future ParseGcode path"). What matters architecturally:

Fact Where
ParseGcode(engine, text, len) EH/NpsAbi.cs:377
ParseBgcode(engine, bytes, len) EH/NpsAbi.cs:380
Split text buffers: generated ≠ parsed GetLastGeneratedGcode EH/NpsAbi.cs:336 vs GetLastParsedGcode :345
Move array is shared after either gen or parse GetLastGcodeMoveCount :366 / GetLastGcodeMoves :369

Finding click → detail

sequenceDiagram
    participant U as User
    participant FP as FindingsPanel
    participant MW as MainWindow
    participant ES as EngineSession
    participant Eng as Engine

    U->>FP: select finding row
    FP->>FP: UpdateDetail(item)
    FP->>MW: FindingSelected(item)
    MW->>FP: TryGetCachedItem(findingId)
    alt cache miss
        MW->>ES: GetFindingDetail(findingId)
        ES->>Eng: GetAnalysisFindingsDetail
        Eng-->>ES: 300B category/message/suggestion
    end
    MW->>MW: status + layer bump + highlight
Step SlicerApp SimulatorApp
Panel hop FindingsPanel.axaml.cs:328 → event :165 same
App handler SA:421 SI:359
Cache first TryGetCachedItem SH/Controls/FindingsPanel.axaml.cs:123-133 — no second host call same
Miss fetch FindingFormatter.RefreshDetailEngineSession.GetFindingDetail EH/EngineSession.cs:407 _session.GetFindingDetail SI:384-391
Highlight SetSimulationState(true, MoveIndex) SA:469-474 — highlight only, full path still drawn _simMoveIndex seek SI:413-417, scrubber mirror pp.SimIndex SI:423-427

Preferred ABI path honored: 16-byte NpsAnalysisFindingSummary rows first (EH/NpsAbi.cs:180), one 300-byte NpsAnalysisFinding detail per selection (:161, :461).

Overlay application

sequenceDiagram
    participant U as User
    participant MW as MainWindow
    participant ES as EngineSession
    participant VP as ViewportController

    U->>MW: choose overlay mode
    MW->>VP: ApplyOverlay(session, mode)
    loop each move index
        VP->>ES: GetOverlayValue(mode, layer, i)
    end
    VP->>VP: "one-shot ViewportApplyOverlay (NpsAbi.cs:432)"
    MW->>MW: RenderViewport (heatmap via ViewportGetHeatmapColor)
  • Radio "None" maps to mode id motion-type, not a no-op (SH/Controls/OverlayPanel.axaml:10 Tag); motion-type renders as the inactive/plain path (SH/ViewportController.cs:576).
  • Apps call the session overload ViewportController.ApplyOverlay(session, mode) SH/ViewportController.cs:564; it fetches per-move values via EngineSession.GetOverlayValue (:588EH/EngineSession.cs:486) and finishes with the one-shot export ViewportApplyOverlay EH/NpsAbi.cs:432.
  • warp-risk additionally auto-enables deformation SetWarpParams(true, 5.0f) SH/ViewportController.cs:627-634; any other mode sets (false, 0).
  • After every Load+Gen+Analyze both apps force the thermal overlay (SA:305-312 / SI:288-295).

Playback (Simulator only)

sequenceDiagram
    participant U as User
    participant MW as MainWindow
    participant T as DispatcherTimer
    participant VP as ViewportController

    U->>MW: press P (play)
    MW->>T: start (120 / speed ms)
    loop tick
        T->>MW: StepPlayback(+1)
        MW->>VP: SetSimulationState(true, ++SimMoveIndex)
        MW->>MW: RenderViewport progressive
        opt hit finding within 2 moves
            MW->>T: stop (auto-pause)
        end
    end
    U->>MW: P again (pause)
Step Where
Key dispatch (P / Left / Right only) ShellKeybindings.Handle SH/Controls/ShellKeybindings.cs:84-110
Toggle play, interval 120 / max(0.1, speed) ms TogglePlay SI:510-536 (:520)
Advance + clamp + ViewportSetSimulationState StepPlayback SI:616-658 (:623-625; export EH/NpsAbi.cs:420)
Auto-pause rule: \|moveIndex − simIndex\| < 2 AND _autoPause SI:645-656; moveIndex == -1 findings excluded
Scrub pause/resume apps subscribe ScrubberPointerPressed/Released (SI:132-133), NOT ScrubberDragStarted/Completed (defined PlaybackPanel.axaml.cs:118/121, unused)

In SlicerApp there is no playback tab (SA/MainWindow.axaml:20) and P/Left/Right only emit Status("Playback unavailable in Slicer.") (SA:142-144).

Gates

Gate Behavior Where
ABI gate NPS_ABI_VERSION mismatch → InvalidOperationException before any handle alloc EH/EngineSession.cs:104-113; SA/Program.cs:22, SI/Program.cs:22-28
Fixture gate missing 4-colors+Benchy+AMS+test-v2.3mf → status error, engine never called SA:163-167 / SI:213-217
Session guard null/invalid _session → silent return SA:157-160 / SI:207-210
Model-loaded gate Apply/Regenerate blocked until a load succeeded SA:200-204
No-moves guard gen success but 0 moves → status, stop SA:268-272 / SI:257-261
Load failure engine code + GetLastErrorMessage text → "Load failed: {code} {message}" EH/EngineSession.cs:214-216SA:170-173 / SI:228-232

Developer: build / test / run

cmake -B build -S src/Engine -DBUILD_TESTING=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j
ctest --test-dir build --output-on-failure

DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1 $HOME/.dotnet/dotnet test

# apps (native lib on LD_LIBRARY_PATH / build dir)
./start-gui.sh   # or project-specific launch

See also