Simulator — how it is¶
The Simulator is one of the two desktop apps in this repo. Both apps are thin
composition layers over the same shell, the same viewport, and the same engine
(README.md:17). This page describes the Simulator as the code stands today —
what it actually does, not what the product statement says it will do.
Purpose¶
HOW IT SHOULD BE (STATED): the repo README defines the Simulator as the app
to "Run physics simulation on the same mesh and field data — stress,
temperature, deformation — visualized in the same viewport" (README.md:15).
HOW IT IS: no physics FEA is present in the app today. What the Simulator actually does is:
- Load the primary Benchy fixture into the engine.
- Generate a demo toolpath (5 layers, explicitly not a production slice —
MainWindow.axaml.cs:313,EngineSession.cs:248–250). - Run engine analysis on that toolpath and show the resulting findings.
- Play the toolpath back on a canvas with a scrubber, speed control, and automatic pauses near findings.
- Paint analysis results as overlays (motion-type, thermal, warp-risk,
bond-strength, cooling-eff —
OverlayPanel.axaml:10–14) on top of the toolpath and the mesh underlay.
So today the Simulator is a G-code analysis and playback viewer that happens to live in the Simulator shell. The physics simulation named in the README is the stated direction, not implemented code.
Startup self-check (Program.cs)¶
The Simulator is the only app that smoke-loads the engine with raw NpsAbi
P/Invoke calls before the UI starts (Program.cs). The sequence, inside a
try/catch in Main:
NpsAbi.GetAbiVersion()compared against the expected ABI version (const1,NpsAbi.cs:238) — mismatch throws (Program.cs:22–28).CreateEngine→InitializeEngine→GetEngineVersion(Program.cs:31–35).LoadModelFromFileon the primary Benchy viaFixtureLocator.FindPrimaryBenchy3Mf()(Program.cs:41–43).- On success, plate/triangle/vertex/palette/unit totals are printed to the
console only — not the UI (
Program.cs:50–71). On failure,GetLastErrorMessageis logged to stdout and stderr under thebenchy_load_via_engine_failedhook (Program.cs:75–82).
Two honest caveats:
- The self-check engine is thrown away. The UI creates a second engine via
EngineSession.CreateAndInitialize()(MainWindow.axaml.cs:108), so startup pays double engine-init cost and the self-check load does not seed the UI session. - The UI starts regardless of self-check success or failure
(
Program.cs:98–99). The check is a diagnostic, not a gate.
BuildAvaloniaApp installs ShellTrace.InstallStderrSink() before configuring
Avalonia (Program.cs:103–116).
Shell regions the Simulator fills¶
The Simulator hosts its content inside the shared MainWindowShell and fills
these regions:
| Region | Content | Source |
|---|---|---|
| Left bar | Layers header, layer slider (0…10), "Load + Gen + Analyze (Benchy)" button, fixture note, mesh info label | MainWindow.axaml:23–34 |
| Viewport | Canvas host + viewport toolbar (camera presets, zoom fit, layer up/down) | MainWindow.axaml.cs:87–100 |
| Inspector tabs | OverlayPanel, FindingsPanel, PlaybackPanel (the Simulator's signature tab) | shell panels |
| Status | StatusText strings; StatusSink mirrors to stderr |
MainWindow.axaml.cs:823, 857 |
The left bar is labeled a stub in the XAML itself: "Left: layer nav stub."
(MainWindow.axaml:12).
Load chain¶
The "Load + Gen + Analyze (Benchy)" button runs one orchestrated sequence in
OnLoadAnalyzeClick (MainWindow.axaml.cs:205–314):
sequenceDiagram
participant UI as MainWindow
participant ES as EngineSession
participant MC as MeshController
participant VP as ViewportController
UI->>ES: LoadModel(benchy)
UI->>MC: Reload() (always, even on load fail)
UI->>ES: GenerateDemoToolpath(5 layers, 0.2mm, feed 1800)
UI->>VP: SetMoves / SetLayerVisibility / SetCamera / BuildBuffers
UI->>ES: Analyze(PLA 210/60)
UI->>ES: GetFindingsSummary + GetFindingDetail
UI->>VP: ApplyOverlay(Thermal) → BuildBuffers
UI->>UI: PlaybackPanel.MoveCount / SimIndex=0 / SetMarkers
UI->>UI: RenderViewport() + "DEMO TOOLPATH" status
Notable details:
MeshController.Reload()runs always, success or fail, so a failed re-load cannot leave stale paint (MainWindow.axaml.cs:221–226; policy inMeshController.cs:127–129).- The thermal overlay is applied unconditionally after load — the mode field
is force-set so an equal-value Avalonia property change cannot skip the
re-apply (
MainWindow.axaml.cs:282–295). - Timeline markers are built from findings via
TimelineMarkerHelper.BuildMarkers, which drops findings withmoveIndex < 0(TimelineMarkerHelper.cs:87–109). - The final status line is deliberately honest: "DEMO TOOLPATH - not a
production slice" (
MainWindow.axaml.cs:312–313).
Playback engine¶
Ownership is split: the Simulator owns the clock and state (DispatcherTimer,
_simMoveIndex, _isPlaying, _playSpeed, _autoPause,
_wasPlayingBeforeScrub), while the shell's PlaybackPanel owns buttons,
speed slider, scrubber, markers, and icons and raises events only.
Timer and speed¶
- A
DispatcherTimerticksStepPlayback(1)(MainWindow.axaml.cs:104–106). - Interval is recomputed on play start, speed change, and scrub resume:
Interval = 120 / max(0.1, _playSpeed)ms (MainWindow.axaml.cs:520, 545, 601). - Speed range 0.1–10, default 1.0 (
PlaybackPanel.axaml:62–63). At 1.0x that is 120 ms per tick, +1 move per tick.
Stepping and auto-pause¶
StepPlayback(delta) (MainWindow.axaml.cs:616–658) clamps the index, pushes
SetSimulationState(true, index) + BuildBuffers() to the viewport, mirrors
the scrubber position, and re-renders. While playing forward with auto-pause
enabled, any finding within 2 moves of the current index stops the timer
with status "Auto-paused at finding." Findings stamped moveIndex = -1 are
excluded — the same filter the timeline markers use.
Scrubbing¶
| Panel event | App behavior |
|---|---|
ScrubberPointerPressed |
Latch _wasPlayingBeforeScrub; pause timer if playing (567–590) |
ScrubberChanged (drag-end commit) |
Clamp index → SetSimulationState → BuildBuffers → render; no auto-pause — the user is driving (554–565) |
ScrubberPointerReleased |
Resume with the speed interval only if it was playing before the press (592–614) |
The panel suppresses intermediate value changes during a drag and fires a single
commit on release (PlaybackPanel.axaml.cs:414–421, 471–480), so the engine is
not re-buffered on every pixel of thumb movement.
Finding timeline markers¶
Findings become 6×10 warning-brush marks on the scrubber track, positioned at
Offset = moveIndex / (moveCount - 1) (TimelineMarkerHelper.cs:87–109;
PlaybackPanel.axaml.cs:543–611). The marker canvas is hit-test invisible so
the slider keeps input (PlaybackPanel.axaml:82–84). Keyboard: P toggles
play, ←/→ step one move (MainWindow.axaml.cs:188–201).
Viewport rendering¶
RenderViewport (MainWindow.axaml.cs:695–825) paints, in order:
- Mesh underlay via
MeshCanvasPainter.RenderTo— subsampled plates (stride = max(1, triangleCount / 2000)), translucent palette fills (MeshCanvasPainter.cs:112–228). - Progressive toolpath: only moves up to
_simMoveIndex + 1are drawn (MainWindow.axaml.cs:739–742), so playback reveals the path move by move. - Per segment: projection through
NpsAbi.ProjectOrbitXY, layer and travel/extrude filtering, heatmap color viaNpsAbi.ViewportGetHeatmapColorwhen an overlay is active, green proxy otherwise, travels at alpha 0.3, and a yellow ellipse on any move that has a finding.
Camera: left-drag orbits (RotationZ += dx*0.01, RotationX clamped to
±1.5), any other drag pans scaled by 0.5/zoom (MainWindow.axaml.cs:678–688);
toolbar presets and zoom-fit go through ViewportController
(ViewportController.cs:294–328, 432–481).
Note
The shell already ships a shared GcodeToolpathPainter with the same
projection, heatmap, and marker logic, but the Simulator has not
migrated to it — its toolpath loop is inline in RenderViewport. The
painter's own comment says: "Simulator may pass _simMoveIndex + 1 until
it also migrates." (GcodeToolpathPainter.cs:78–79)
Status plumbing¶
Status is a plain string on StatusText, last line including move/sim/layer/
mode plus the last move's XYZ — flagged in code as "// status coords stub"
(MainWindow.axaml.cs:819). StatusSink mirrors status output to stderr, and
ShellTrace.InstallStderrSink() is installed at app build
(Program.cs:103–116), so status is observable from a terminal without the UI.
Limitations (each verified in code)¶
| Limitation | Evidence |
|---|---|
| No physics FEA; app is analysis-overlay + playback only | README promise README.md:15 vs. actual flow MainWindow.axaml.cs:205–314 |
| Demo toolpath, not a production slice; capped at 5 layers for "responsive UI" | MainWindow.axaml.cs:244–250, 313; EngineSession.cs:248–250 |
| Layer nav is a stub; slider max hard-coded 10 while gen produces 5 layers | MainWindow.axaml:12, 26; MainWindow.axaml.cs:433–452 |
Inline toolpath paint duplicates the shared painter (paint-time NpsAbi calls in MainWindow) |
GcodeToolpathPainter.cs:78–79 |
| Self-check engine discarded; UI creates a second engine (double init) | Program.cs; MainWindow.axaml.cs:108 |
| Startup self-check failure does not block the UI | Program.cs:98–99 |
| Mesh painter ignores camera pan — mesh and toolpath can desync under pan | MeshCanvasPainter.cs:112–228 |
| No mouse-wheel zoom; zoom only via toolbar fit/reset | MainWindow.axaml.cs:678–688 |
Overlay "None" is motion-type, not a true engine off state |
OverlayPanel.axaml:10–14 |
Warp grid ABI exists (GetWarpGrid) but the Simulator UI never calls it (only the Slicer does) |
EngineSession.cs:421 |
| Status coordinates are a stub (last move XYZ only) | MainWindow.axaml.cs:819 |
See also¶
- Slicer — how it is — the sibling app on the same shell
- Sharing plan — how shell and engine code is split between the two apps