Skip to content

Add a headless app gallery that pictures each panel - #117

Merged
matt-edmondson merged 4 commits into
mainfrom
claude/project-thread-4d94d3
Oct 11, 2026
Merged

matt-edmondson merged 4 commits into
mainfrom
claude/project-thread-4d94d3

Conversation

@matt-edmondson

@matt-edmondson matt-edmondson commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Matt · project thread

Before: there were no pictures of what Osculator's panels look like. The only way to see them was to build and run the app.

After: Osculator.Tests/Gallery/ stages the real application headlessly and photographs it. docs/gallery/README.md will caption six pictures:

  1. The application: the storage comparison above a screened conjunction panel, with the element inspector down the right, at 1600x1000.
  2. Storage comparison, the ISS a day out: float, double, decimal and PreciseNumber against the 50-digit reference.
  3. Storage comparison, a Molniya orbit a week out.
  4. Element inspector.
  5. Conjunction screening: a screen over constructed objects fitted at a fixed instant.
  6. A close approach in every storage type: one row of that screen re-found in all four types.

How: this follows the pattern ImageGui uses.

  • Each picture is an ordinary MSTest test.
  • Each test starts the configuration Program starts, AppShell.BuildConfig(), through ImGuiAppHarness on the CPU rasterizer, and loads ImGuiApp's font.
  • The mouse is parked off-screen, the panels are staged, and the frame is saved as a PNG.
  • A WriteTheIndex test writes the captions.
  • Output goes to a temp directory unless OSCULATOR_GALLERY_OUT names one. Because the pictures are tests, every PR proves each one can still be staged.

Determinism: the pictures are byte-identical from run to run.

  • Nothing reaches the network or the wall clock. The element sets come from the application itself, and the screen uses constructed objects.
  • Stopwatch columns read "hidden" while the gallery runs. They are switched off by a new MeasuredDurations.Hidden, which stays false in the application.

Refactors to reach the app from a test, with no change in runtime behaviour:

  • MeasuredDurations is a new class in Osculator.App/Shell/.
  • StorageComparisonPanel's last result is now a read-only Latest property.
  • StorageComparisonPanel gained Select(objectName, minutes), IsMeasuring and ResetState().
  • The test project sets AllowUnsafeBlocks, because the font loader calls ImGui through pointers.

Workflow: .github/workflows/app-gallery.yml follows ImGuiApp's widget-gallery workflow. It regenerates docs/gallery and commits the PNGs back as LFS objects with [bot][skip ci] Regenerate the app gallery. dotnet.yml calls it as a job with needs: build on pushes to main. That is a different hook from the reference repos, which have it wait on a shared ci job. Osculator has its own dotnet.yml and no release job, so it waits on build, and the workflow header says so.

No PNGs are committed here. They, and docs/gallery/README.md, arrive with the first run on main after merge, or sooner by dispatching the App Gallery workflow. The README now has a Gallery section previewing a-close-approach-in-every-storage-type.png, linked to docs/gallery/README.md. The image renders once the gallery job has run on main.

A finding worth a look: the panels are placed as floating windows, not docked. ImGuiApp's full-viewport ##mainWindow (WindowBg alpha 0.94) is drawn above the dockspace host. The dockspace host is pushed to the back by NoBringToFrontOnFocus, so anything docked shows through dim. This is likely the same in the real application. Panels docked as tabs of one node also churn between frames, because Hexa closes on a Begin that returned false and the panel guard then re-shows it.

Second round of fixes, after a review of the rendered pictures:

  • The overview runs the conjunction screen, so that panel is no longer empty, and stacks it under the storage comparison so neither column is half blank.
  • The storage comparison pictures are 440 px tall, as tall as what they draw. The Molniya caption now says the resolution table is fixed at a 7,000 km radius.
  • App fix: the approaches table shows whole rows only and reserves room for the storage-type comparison, instead of capping itself at half the panel. The last row is no longer cut, and the table no longer resizes when an approach is selected. This is layout only, so it has no regression test; the gallery pictures cover it.
  • App fix: the element inspector's label column is wide enough for "Identity (not quantized orbit fields)" in the half-width overview.

Feature correctness: I checked each picture against the measured figures in CLAUDE.md and the tests.

Verified locally:

  • The Release solution build is clean.
  • Two gallery runs produced byte-identical PNGs and README. I checked the pictures by eye: text renders, nothing is blank or cut off, and nothing is highlighted under the mouse.
  • The full suite ran 583 tests: 582 passed. The one failure, DecompositionTests.TheMonteCarloAgreesWithTheFieldByFieldTerm (0.009866 against an expected 0.009459), fails the same way on current main.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WWRBkw7bXdWGzuLpqnCvrB


Generated by Claude Code


Generated by Claude Code

Six pictures of the application as it draws today, staged and taken by the
test suite through ImGuiAppHarness and the configuration Program starts: the
whole shell side by side, the storage comparison for the ISS a day out and a
Molniya orbit a week out, the element inspector, a conjunction screen over
constructed objects, and one approach measured in every storage type. A
WriteTheIndex test writes the captions to docs/gallery/README.md.

The pictures are byte-identical run to run. Nothing reaches the network or
the wall clock: the storage comparison and inspector draw the element sets
compiled into the application, and the screen runs over objects fitted at a
fixed instant. The stopwatch columns read "hidden" while the gallery runs,
through a new MeasuredDurations switch that is off in the application.

The panels are placed as floating windows rather than docked. ImGuiApp's
full-viewport ##mainWindow sits above the dockspace, so a docked panel is
painted over by its almost opaque background and shows through dim.

To reach the storage comparison from a test, its latest measurement became a
read-only property, and it gained Select, IsMeasuring and ResetState.

app-gallery.yml regenerates the pictures after the build job passes on main
and commits them back as LFS objects. No pictures are committed here; the
first run on main writes them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWRBkw7bXdWGzuLpqnCvrB
@matt-edmondson matt-edmondson self-assigned this Oct 9, 2026
Comment thread Osculator.Tests/Gallery/GalleryCatalog.cs Fixed
Comment thread Osculator.Tests/Gallery/AppGallery.cs Fixed
Comment thread Osculator.Tests/Gallery/AppGallery.cs Fixed
Comment thread Osculator.Tests/Gallery/AppGallery.cs Fixed
Path.Join does not discard the directory if a later segment happens to
be rooted, and the storage comparison wait now compares minutes with a
tolerance rather than exact equality.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWRBkw7bXdWGzuLpqnCvrB

Copy link
Copy Markdown
Contributor Author

"Build and test" is red, but the failure is not caused by this PR. The failing test is DecompositionTests.TheMonteCarloAgreesWithTheFieldByFieldTerm, and it fails the same way on main at f14dee6 (the .NET run there is red). It has failed since #108 (7afc032). This PR does not touch DataTerm, Decomposition or that test.

Cause. #108 changed DataTerm.Measure in two ways:

  • it now perturbs the epoch;
  • it now takes its steps from ElementFieldQuantization.For(elements).

The ISS history is OMM, so the field-by-field term now uses OMM's finer eccentricity and B* steps. The Monte Carlo in Decomposition.Perturb still draws with the TLE steps. The test also adds the epoch term a second time, although DataTerm now includes it. As a result the prediction reads 0.009459 km against a measured 0.009866 km, which is outside the 1% tolerance.

No fix exists yet. The proposed patch is to make both sides use the same steps and drop the duplicate epoch term. Two ways to do that:

  • In the test, measure with history[1] with { Format = ElementSetFormat.Tle }, and predict Math.Sqrt(fieldByField * fieldByField / 3.0).
  • Or have Decomposition.Perturb draw from ElementFieldQuantization.For(e) too, so an OMM ensemble uses OMM steps, and remove the epoch term from the test's prediction.

The second option also corrects the CLAUDE.md note that the Monte Carlo figure is an upper bound for OMM sets. I've left this out of the gallery PR so it doesn't widen.


Generated by Claude Code

The overview now stacks the storage comparison above a screened
conjunction panel, with the element inspector down the right, so no
panel is empty and no column is half blank. The storage comparison
pictures are as tall as what they draw, and the Molniya caption says the
resolution table is fixed at a 7,000 km radius.

In the app, the approaches table now shows whole rows and leaves room
for the storage-type comparison under it instead of capping itself at
half the panel, so the last row is never cut and the table no longer
resizes when an approach is selected. The element inspector's label
column is wide enough for its "Identity" section caption.

The README previews the gallery with the close-approach picture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WWRBkw7bXdWGzuLpqnCvrB
#112 made DataTerm format-aware and gave it the epoch, while #108's
Monte Carlo still drew every field from the two-line text's steps and
its test added the epoch to DataTerm a second time. The committed ISS
snapshots are OMM, so the two measured different bands and
TheMonteCarloAgreesWithTheFieldByFieldTerm failed on main (9.866e-3
against 9.459e-3). Perturb now takes ElementFieldQuantization.For(e),
and the test compares against DataTerm alone: 9.274e-3 against
9.264e-3, 0.1% apart.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jxv8yPrvvmTax9Bhh9phBA
@matt-edmondson
matt-edmondson merged commit 77eb20b into main Oct 11, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants