Shared today¶
Both apps — SlicerApp and SimulatorApp — are already thin shells over two shared
assemblies. Nps.Shell owns window chrome, panels, viewport plumbing, keybindings,
and the paint helpers. Nps.EngineHost owns the managed API of the native engine:
the ABI surface plus two session-shaped facades. Everything on this page is HOW IT
IS (verified code truth); each claim carries a file:line citation.
The framing that holds across all of it: these are operational concerns — host lifecycle, layout, input plumbing, diagnostics, engine interop — and they are correctly owned by one deep module each. Apps call the shared interfaces; they do not re-implement windowing glue or P/Invoke.
Shell inventory¶
Every control, helper, and facade in Nps.Shell, with the public surface each app
actually touches. All paths are under /home/christoph/github/nps/src/Nps.Shell/.
| Type | Path | Public surface |
|---|---|---|
MainWindowShell |
Controls/MainWindowShell.axaml.cs |
Region properties LeftRegion / ViewportRegion / InspectorRegion / StatusRegion (lines 44–78); TitleText / StatusText (81–97); IsLeftExpanded / IsRightExpanded with width memory (100–116, 324–438); IsPlaybackTabVisible (123–131); owns Activity + Panels (40–41); ViewportPointer* events (35–37); AttachViewportHandlers() (256–276); auto-discovers inspector panels by name at load (218–247) and shows "overlays" by default (250). |
ActivityBarHost |
Controls/ActivityBarHost.cs |
Register(key, button) (66), HandleClick (80), TabClicked event (58). |
PanelHost |
Controls/PanelHost.cs |
Register(key, panel) (58), Show(key, onShown?) (73). |
ViewportToolbar |
Controls/ViewportToolbar.cs |
Camera preset / ZoomFit / travels / extrudes / layer up-down events (18–27); button and toggle accessors for paint-flag state (29–38). |
OverlayPanel |
Controls/OverlayPanel.axaml.cs |
Title, LegendLow, LegendHigh, StatsText, IsOverlayActive, SelectedMode styled properties (12–64); ModeChanged event (66). |
FindingsPanel |
Controls/FindingsPanel.axaml.cs |
SetFindings (94), TryGetCachedItem (123), UpdateDetail (142), FindingSelected event (165); detail styled properties for severity/category/location/message/suggestion/warp (183–262). |
PlaybackPanel |
Controls/PlaybackPanel.axaml.cs |
Play/step/speed/auto-pause/scrubber events (91–121); MoveCount (198), SimIndex (231), IsPlaying (256), Speed (272), AutoPause (283), SetMarkers (303). A pure UI surface — no timer, no engine calls. |
FindingItem |
Controls/FindingItem.cs |
View-model row: FindingId, Layer, MoveIndex accessors (104–116) projected from the ABI summary. |
FindingFormatter |
Controls/FindingFormatter.cs |
ProjectFromSession (175), RefreshDetail (206), FromSummaryAndDetail (222); severity/category label and brush helpers (74–144). |
FindingsFilterState |
Controls/FindingsFilterState.cs |
Immutable filter-chips model; Matches(item) (95), severity/category toggles (113–122). |
GcodeToolpathPainter |
Controls/GcodeToolpathPainter.cs |
RenderTo(canvas, viewport, w, h, maxLayer, showTravels, showExtrudes, maxMoveIndexExclusive, findingMoveIndices?) (85–94). The maxMoveIndexExclusive parameter exists so Simulator can cap the path at the playhead. |
MeshCanvasPainter |
Controls/MeshCanvasPainter.cs |
RenderTo(canvas, mesh, …) (112); MaxTrianglesPerPlate cap (87). Simulator's mesh background path. |
ShellKeybindings |
Controls/ShellKeybindings.cs |
Handle(e, actions) (82) with an Actions struct of app-supplied delegates: TogglePlay, StepForward, StepBackward, ToggleInspector (58–70). |
TimelineMarkerHelper |
Controls/TimelineMarkerHelper.cs |
Fraction(moveIndex, moveCount) (58), BuildMarkers (87) — scrubber marker geometry. |
ViewportController |
ViewportController.cs |
Camera/sim/overlay/build/moves facade over the engine viewport (117): SetMoves (240), SetLayerVisibility (252), SetCameraPreset (294), ComputeZoomFit (380), SetSimulationState (489), ApplyOverlay → OverlayStats (564), BuildBuffers (657), TryGetLastMovePosition (694). Value types CameraState (72), CameraPreset (82), OverlayStats (99). |
FixtureLocator |
FixtureLocator.cs |
FindPrimaryBenchy3Mf() (60), PrimaryBenchyFileName const (41). |
StatusSink |
StatusSink.cs |
Emit (57), Format (44), Prefix = "[SlicerUI]" (36). |
ShellTrace |
ShellTrace.cs |
InstallStderrSink() (45) — stderr/process plumbing. |
| Themes | Themes/ |
Colors.axaml, Brushes.axaml, Spacing.axaml, Icons.axaml, SeverityPalette.cs, merged by Theme.axaml. |
A known branding leak inside the shared surface
StatusSink.Prefix is the constant "[SlicerUI]" (StatusSink.cs:36) and both
apps emit through it — Simulator's status lines carry the other app's name. The
constant itself is the right kind of sharing (one place to fix); the value is
wrong for one consumer. See the duplication analysis for the
full list of mis-shares.
EngineHost facades¶
Nps.EngineHost is the host border: the single managed assembly that knows the
native library exists.
NpsAbi(/home/christoph/github/nps/src/Nps.EngineHost/NpsAbi.cs:41) is the sole P/Invoke surface. Every one of the 62[DllImport]attributes insrc/lives in this file, along with everyNps*POD mirror, named ABI enum, and wire-id constant (file header, lines 1–28). Mirrors are kept in lockstep with the C headers and verified byMarshal.SizeOftests.EngineSession(EngineSession.cs:67) is the session-shaped facade over one engine instance: lifecycle,LoadModel(202),GenerateDemoToolpath(253),Analyze(332), findings summary/detail (376, 407), warp grid (421), overlay value lookup (486). Its header states the scope rule explicitly: no viewport creation, no UI types (lines 33–39).MeshController(MeshController.cs) wraps the engine's mesh query surface so the render loop iterates plate geometry from owned managed buffers instead of touching the raw handle (file header, lines 1–50). It holds a reference to the session without owning its lifetime.SafeEngineHandle/SafeGcodeViewportHandlerelease native handles deterministically on dispose.
Both apps reference the same three pieces — there is one EngineSession facade
and two app shells. Apps never see the wire format; the engine's design decisions
(struct layouts, step order) stay hidden behind the facades.
flowchart LR
Slicer[SlicerApp] --> Shell[Nps.Shell]
Sim[SimulatorApp] --> Shell
Shell --> Host[Nps.EngineHost]
Slicer --> Host
Sim --> Host
Host --> ABI["NpsAbi: sole DllImport home"]
ABI --> Engine[NPSEngine native]
Six parameterization points¶
The shell is shared; the apps differ through exactly six knobs. These are the complete set of per-app adjustments — everything else an app does is event wiring and region content, which is the app's own product code.
| # | Knob | Slicer | Simulator | Citation |
|---|---|---|---|---|
| 1 | IsPlaybackTabVisible |
False — hides the playback activity tab and skips PanelHost registration for "playback" |
default True |
SlicerApp/MainWindow.axaml:20; MainWindowShell.axaml.cs:123–131, 172–188, 241–246 |
| 2 | TitleText |
"NPS Slicer | G-code Preview + Analysis | Mod+L:inspector" |
"NPS Simulator | Viewport + Overlays + Findings + Playback (P:play Mod+L:inspector)" |
SlicerApp/MainWindow.axaml:21; SimulatorApp/MainWindow.axaml:21 |
| 3 | Region fills | Thick left sidebar (slice params, warp grids); viewport; two inspector panels | Thin left sidebar; viewport; three inspector panels | SlicerApp/MainWindow.axaml:23–92; SimulatorApp/MainWindow.axaml:23–60 |
| 4 | Panel naming contract | PanelOverlays (line 83), PanelFindings (line 87); no playback panel |
PanelOverlays (48), PanelFindings (52), PanelPlayback (58) |
auto-discovery probes those exact names at MainWindowShell.axaml.cs:222–229 |
| 5 | Keybinding actions | status-message stubs for play/step (honest product difference) | real play/step handlers | both call ShellKeybindings.Handle with their own Actions (ShellKeybindings.cs:58–82) |
| 6 | Event wiring | apps wire toolbar, overlay, findings, selection handlers themselves | same | the shell exposes events; it deliberately does not auto-wire app behavior |
One known gap in the naming contract
FindingsPanel is probed as "PanelFindings" with a fallback to the legacy
"FindingsList" name (MainWindowShell.axaml.cs:227–228) for apps that have not
migrated. Both current apps use the new name.
Themes and tokens¶
Both apps merge the identical resource dictionary:
avares://Nps.Shell/Themes/Theme.axaml in SlicerApp/App.axaml:10 and
SimulatorApp/App.axaml:10. Theme.axaml merges Colors.axaml, Brushes.axaml,
Spacing.axaml, and Icons.axaml; SeverityPalette.cs resolves the
Severity.Brush.* resources that FindingFormatter consumes via
SeverityPalette.BrushFor (Controls/FindingFormatter.cs:230), while the
category brushes come from the Category.Brush.* keys in Brushes.axaml via
FindingFormatter.CategoryBrush (109). One token set, two apps, zero per-app theme
files.
Read next¶
- Duplication and share verdicts — the ~45–50% near-clone
orchestration still living in both
MainWindow.axaml.csfiles, and which blocks are candidates to move into the shell versus copy deliberately. - Slicer overview and Simulator roadmap — the per-app product surfaces built on top of this shared base.