Skip to content

Duplication audit

This page is the as-is clone audit of the two Avalonia MainWindows: what is copied between SlicerApp and SimulatorApp today, how much, and which of it is wrong sharing versus acceptable replication. Everything under How it is was verified against the code; share/split recommendations live on the sibling page Sharing plan.

How it is — sources compared in full:

  • /home/christoph/github/nps/src/SlicerApp/MainWindow.axaml.cs — 671 lines
  • /home/christoph/github/nps/src/SimulatorApp/MainWindow.axaml.cs — 860 lines
  • /home/christoph/github/nps/src/SlicerApp/MainWindow.axaml — 102 lines
  • /home/christoph/github/nps/src/SimulatorApp/MainWindow.axaml — 70 lines

Method

The two code-behind files were compared block by block: each contiguous region of the Slicer MainWindow.axaml.cs (field skeleton, shell initialization, toolbar wiring, load chain, findings handling, render loop, and so on) was matched against its Simulator counterpart and given one of three verdicts:

  • Verbatim — identical or character-identical apart from whitespace.
  • Near-verbatim — same control flow and same calls; only strings, a guard, or one extra statement differ.
  • Partial / forked — a shared spine with app-specific forks, or a structural clone over a different API.

Fractions are estimated in lines, not characters: method-shaped clones with small string or type diffs count as duplicated.

Headline numbers

How it is (estimated from the block table below):

Metric Value
Slicer code-behind 671 lines
Simulator code-behind 860 lines
Slicer mirrored in Simulator ~45–50% of Slicer's file
Simulator mirrored in Slicer ~38–42% of Simulator's file (Sim is larger: playback + inlined paint + mesh)
Combined structural duplication ~42–48% of the union of both files
Near-clone orchestration glue still in both MainWindows ~300–350 lines

The chrome (window, regions, panels, themes) is not the duplication problem — it is already factored into MainWindowShell and Nps.Shell controls. What remains duplicated is orchestration glue: camera math, toolbar wiring, layer slider, overlay apply, findings selection skeleton, and the load spine.

Block-by-block table (A–R)

How it is — the heart of the audit. Line ranges cite src/SlicerApp/MainWindow.axaml.cs and src/SimulatorApp/MainWindow.axaml.cs.

# Block Slicer lines Sim lines Verdict
A Field skeleton (_session, _viewport, drag, overlay mode, _maxLayer) 25–57 26–75 Partial — same core fields; Sim adds mesh/playback/ABI buffers, Slicer adds slice/warp/painter
B InitializeShell (activity + pointer + toolbar + session/viewport + overlay) 65–104 83–142 Near-verbatim core; Sim adds timer + PlaybackPanel wiring (104–137)
C Toolbar wiring only 80–89 91–100 Verbatim (identical lambdas)
D OnActivityTabClicked 106–115 144–153 Verbatim
E OnClosed disposal 120–134 160–185 Near-verbatim shape (viewport→session); Sim also stops timer and nulls _mesh
F OnKeyDown via ShellKeybindings 136–151 188–201 Near-verbatim structure; actions differ (Slicer no-ops play/step, Sim binds real handlers)
G Load chain (fixture → LoadModel → status/mesh label → generate → analyze → findings → thermal → apply → render) 155–189 + 251–318 205–314 Partial / forked — shared spine; Slicer splits Apply/RunGenerate/warp, Sim hard-codes gen opts + mesh reload + playback markers
H Findings load + project 366–410 316–357 Structural clone over a different API — Slicer: FindingFormatter.ProjectFromSession; Sim: raw NpsAbi list + GetFindingDetail
I OnFindingSelected 421–481 359–431 Near-verbatim control flow (cache → stamp → UpdateDetail → layer/sim → BuildBuffers → render); Sim also writes PlaybackPanel.SimIndex
J Layer slider + label 483–502 433–452 Verbatim
K OnActivityTabClick (dead thin wrapper) 504–507 454–457 Verbatim
L Overlay mode + ApplyCurrentOverlay 509–558 459–508 Near-verbatim — legend/stats wiring identical, status strings differ
M Pointer press / move / release (orbit + pan) 560–593 661–692 Verbatim (same dx/dy, clamp, pan formula)
N RenderViewport 597–638 695–825 Diverged — Slicer delegates to GcodeToolpathPainter; Sim inlines the paint loop + mesh painter
O ZoomFitCamera 640–662 827–849 Verbatim
P Status helper 665–669 851–858 Near-verbatim (Shell.StatusText + StatusSink.Emit)
Q Playback (timer, scrubber, auto-pause, step) (absent) 510–658 Sim-only
R Slice params harvest / Apply / warp grids 192–246, 321–363 (absent) Slicer-only

Notes on the load chain (block G)

How it is — the shared spine, in order, with citations for both apps:

  1. Guard session; FixtureLocator.FindPrimaryBenchy3Mf() + missing-file error (Slicer 162–167 / Sim 212–217)
  2. _session.LoadModel (Slicer 169 / Sim 219)
  3. Plate/palette/unit status + MeshInfoLabel (Slicer 178–186 / Sim 234–242)
  4. GenerateDemoToolpath (Slicer 258–261 via SliceParams; Sim 245–250 hard-coded 5 layers)
  5. SetMoves / camera / BuildBuffers (Slicer 274–287 / Sim 263–269)
  6. Analyze(PLA…) (Slicer 289 / Sim 272)
  7. Findings + force thermal + ApplyCurrentOverlay (Slicer 296–314 / Sim 280–312)

The forks are product-shaped, not accidental: Slicer owns _modelLoaded, a separate Apply step, LoadWarpGrids, and SetSimulationState(false, last); Sim owns _mesh?.Reload() on every load, playback MoveCount/SetMarkers, and _simMoveIndex = 0. This is a diverging path, and forcing it into one shared LoadAnalyzePipeline would be the wrong share — the divergence forecast and verdicts are on the Sharing plan.

XAML duplication

How it issrc/SlicerApp/MainWindow.axaml (102 lines) vs src/SimulatorApp/MainWindow.axaml (70 lines):

Region Slicer Simulator Verdict
Window chrome (KeyDown, shell xmlns, design size) 1–10 1–10 Near-identical shell host
MainWindowShell 19–21, IsPlaybackTabVisible="False" + TitleText 20–21, TitleText only Shared control; one parameterization
LeftRegion 23–69 (layers + SliceParams + Load + Apply + Mesh + Warp) 23–34 (layers + Load + Mesh) Partial — slider/label/load/mesh pattern shared
ViewportRegion 71–78 (Border + Canvas + Toolbar) 36–43 Near-verbatim
InspectorRegion 80–92 (Overlay + Findings) 45–60 (+ PlaybackPanel) Shared two panels; Sim-only third
StatusRegion 94–99 (StatusBar bound to Shell.StatusText) 62–67 Verbatim pattern

Roughly ~70% of the Simulator XAML is the shared shell composition; ~55–60% of the Slicer XAML is the same skeleton with a much thicker left sidebar (slice params). The chrome itself is already factored into MainWindowShell; what remains duplicated are region fill patterns — viewport stack, status bar, dual inspector panels, layer slider block.

The seven wrong-sharing findings

How it is — each item cites code. Red-flag labels follow the design language (repetition, information leakage, special–general mixture).

  1. Sim inlines the paint loop despite GcodeToolpathPainter (missing share; repetition). The shell painter exposes RenderTo(..., maxMoveIndexExclusive, ...) designed for both apps — the sim playhead is the intended argument. Slicer consumes it (MainWindow.axaml.cs:622–631); Sim still owns _projBufA/B and _heatBuf (SimulatorApp/MainWindow.axaml.cs:43–45) plus a ~70-line inlined loop (748–817) that mirrors the painter. This is the single largest piece of current sharing debt.

  2. Sim findings path leaks ABI types into the window (information leakage). Slicer loads findings through FindingFormatter.ProjectFromSession (SlicerApp/MainWindow.axaml.cs:400–405); Sim still stores List<NpsAbi.NpsAnalysisFindingSummary> (SimulatorApp/MainWindow.axaml.cs:51) and calls _session.GetFindingsSummary / _session.GetFindingDetail from UI code (SimulatorApp/MainWindow.axaml.cs:332, 353, 386). The same "how findings become rows" knowledge lives in two different shapes.

  3. StatusSink.Prefix = "[SlicerUI]" is a forced brand on both apps (special–general mixture). The shared sink hard-codes the Slicer brand (Nps.Shell/StatusSink.cs:36), so Simulator status lines and smoke greps emit [SlicerUI]. A general mechanism (status emission) embeds one app's special case.

  4. FindingItem.Data exposes the ABI POD to apps (information leakage, half-fixed). Nps.Shell/Controls/FindingItem.cs:98 still surfaces NpsAbi.NpsAnalysisFindingSummary as a public property. Slicer's convenience accessors (FindingId, Layer, MoveIndex at lines 104–116) hide it, but Sim still reads item.Data.layer directly, so the ABI field layout remains UI-visible.

  5. Overlay mode keys are written in two styles (repetition without a single constant surface). Slicer uses string literals "motion-type" / "thermal" (SlicerApp/MainWindow.axaml.cs:57, 305); Sim uses NpsAbi.OverlayModeId.* (SimulatorApp/MainWindow.axaml.cs:72, 288). Same keyspace, two spellings — a sign the shell owns no UI-side constant for overlay modes.

  6. Dead OnActivityTabClick twins (repetition of dead weight; pass-through). Both apps carry an identical handler (SlicerApp/MainWindow.axaml.cs:504–507, SimulatorApp/MainWindow.axaml.cs:454–457) that only forwards to Shell.Activity.HandleClick. Activity tabs are shell-owned, so the wrapper adds interface surface and no function in both apps.

  7. Not wrong: IsPlaybackTabVisible. Slicer hides the playback tab (SlicerApp/MainWindow.axaml:20), Sim uses the default true. This is correct product parameterization of one shared control, not forced sharing — flagged here only because it can look like item-3-style branding at a glance.

Note

Blocks Q (playback) and R (slice params / warp grids) are deliberately absent from the wrong-sharing list. They are single-app code with no clone counterpart — the honest kind of divergence. The forecast for what should stay split versus converge is on the Sharing plan.

What the clones are made of

INFERENCE — grouping the verbatim and near-verbatim blocks by knowledge rather than by file order shows the duplication is concentrated in four kinds of glue:

  • Camera and input (blocks C, M, O): bit-identical math and event shapes.
  • Viewport adornment (blocks J, L): slider clamp + label, overlay legend + stats.
  • Findings skeleton (blocks D, I, and the control flow of H): cache-first selection, detail stamp, layer/sim navigation.
  • Load spine (block G): the shared seven-step sequence above, before the forks.

None of these are domain rules (slicing semantics, sim physics); they are operational UI glue. That is what makes the ~300–350 cloned lines a candidate for shell-owned helpers rather than acceptable replication — and what makes blocks G's forks, Q, and R the opposite. Verdicts per block are on the Sharing plan.