Skip to content

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:

  1. Load the primary Benchy fixture into the engine.
  2. Generate a demo toolpath (5 layers, explicitly not a production slice — MainWindow.axaml.cs:313, EngineSession.cs:248–250).
  3. Run engine analysis on that toolpath and show the resulting findings.
  4. Play the toolpath back on a canvas with a scrubber, speed control, and automatic pauses near findings.
  5. 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:

  1. NpsAbi.GetAbiVersion() compared against the expected ABI version (const 1, NpsAbi.cs:238) — mismatch throws (Program.cs:22–28).
  2. CreateEngineInitializeEngineGetEngineVersion (Program.cs:31–35).
  3. LoadModelFromFile on the primary Benchy via FixtureLocator.FindPrimaryBenchy3Mf() (Program.cs:41–43).
  4. On success, plate/triangle/vertex/palette/unit totals are printed to the console only — not the UI (Program.cs:50–71). On failure, GetLastErrorMessage is logged to stdout and stderr under the benchy_load_via_engine_failed hook (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 in MeshController.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 with moveIndex < 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 DispatcherTimer ticks StepPlayback(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 → SetSimulationStateBuildBuffers → 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:

  1. Mesh underlay via MeshCanvasPainter.RenderTo — subsampled plates (stride = max(1, triangleCount / 2000)), translucent palette fills (MeshCanvasPainter.cs:112–228).
  2. Progressive toolpath: only moves up to _simMoveIndex + 1 are drawn (MainWindow.axaml.cs:739–742), so playback reveals the path move by move.
  3. Per segment: projection through NpsAbi.ProjectOrbitXY, layer and travel/extrude filtering, heatmap color via NpsAbi.ViewportGetHeatmapColor when 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