Skip to content

SPEC-13 — Sunburst view: polar projection of the world (GPU, morph, same scan as the treemap)#32

Merged
rducom merged 5 commits into
mainfrom
feat/sunburst-view
Jul 13, 2026
Merged

SPEC-13 — Sunburst view: polar projection of the world (GPU, morph, same scan as the treemap)#32
rducom merged 5 commits into
mainfrom
feat/sunburst-view

Conversation

@rducom

@rducom rducom commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

What

A second map mode, dotMemory-style: depth becomes concentric rings, size becomes angular extent, and the current zoom root becomes the hole in the middle — its relative path + on-disk size as the wheel's readout (the selection takes over the centre when one is active).

image

Both projections read the same ScanController observables (tree, version, zoomRoot, selection, search, type highlight): switching modes never re-scans and carries the whole navigation state over. The toggle lives in the map pane's breadcrumb bar, persisted via @AppStorage("mapMode").

Full spec with design rationale and post-mortems: docs/specs/SPEC-13-sunburst.md.

How

  • SunburstWorld — the polar sibling of TreemapWorld (SPEC-10): per-node children spans stored parent-relative as fractions, ε-stable local revalidation (sibling order holds while share drift ≤ 2 %), LOD on projected arc length with hysteresis. Rings decay geometrically (k = 0.8): an arbitrarily deep tree fits the finite disc, zooming in is what reveals outer generations. Sub-pixel sibling tails close their ring with an aggregate remainder arc (the polar underlay); the visibility walk tests the subtree sector through to the rim, since descendants extend radially outward.
  • SunburstMetalRenderer — same discipline as SPEC-09/10 (one instanced draw, triple buffering, prev-buffer + morph uniform, camera-only frames write nothing): each arc is its bounding quad, the annular sector is carved per-pixel by a polar SDF in the fragment shader — which also anti-aliases the curved edges (no MSAA), draws the border, the radial cushion and the highlight/search dim. Blending on, no depth. During a morph the quad covers the union of both sectors, params lerp in the shader — re-roots sweep, live ticks breathe.
  • SunburstNSView — isotropic letterboxed camera (circles stay circles; resize never re-bakes), explicit re-root on dive/pull-back (no camera-derived focus), free pan/zoom inspection on top. Interaction parity with the treemap: hover pill, click reveals (outside click resets, like the centre), double-click dives, right-click menu, sector spotlight, search/type dimming, live-scan teleport + post-scan morphs.
  • MapChrome — hover pill, GPU-unavailable state, context menus and the VoiceOver summary extracted and shared by both views; identical colour semantics (hue = dominant type, brightness = relative weight) so the wheel matches the File-types legend.

Fixes found while shipping it

  • Mode switch drained the Metal drawable pool (map black ~10 s / sunburst colours gone for good, only the CG overlay drawing): a recreated NSView presented frames while detached. Both views now never present while window == nil, re-present on attach, and retry once on a dropped frame (draw() reports it). Post-mortem in SPEC-13 §5.
  • A view recreated while zoomed (mode switch) fits its camera on zoomRoot instead of the whole world — both projections always agree on where you are.
  • Centre label blinked on live scans: it was gated on the morph clock; every FSEvents reconciliation tick hid it for 0.32 s. The hole is static world geometry — drawn unconditionally now.

Tests & verification

  • 116 tests green, including 9 new SunburstWorld property tests: ring partition (contiguous, largest-first, share-exact), camera independence, ε order-keeping vs re-decide, re-root + worldSpan composition, ring monotonicity/boundedness, LOD hysteresis, sub-pixel tail closure, seam-safe polar containment and bbox tightness.
  • Driven live on real scans (small repo + full disk): rendering, mode toggle both ways (< 0.4 s, full colours), selection readout, outside-click reset, background/theme integration — all verified by screenshot.

rducom added 4 commits July 13, 2026 17:08
…ame scan as the treemap)

A second map mode, dotMemory-style: depth = concentric rings (geometric
decay — infinite depth fits the disc, zoom reveals it), size = angular
extent, the zoom root = the hole (name + total). Same ScanController,
same tree, same navigation state: switching modes never re-scans.

- SunburstWorld: parent-relative angular fractions, ε-stable local
  revalidation, LOD on projected arc length with hysteresis, subtree
  reach test (descendants extend outward), sub-pixel tail closed by an
  aggregate remainder arc (the polar underlay), polar file LOD.
- SunburstMetalRenderer: instanced bounding quads carved by a polar SDF
  in the fragment shader (AA on curved edges, cushion, border, dim),
  triple buffering + prev-buffer morph, blending on, no depth.
- SunburstNSView: isotropic letterboxed camera (circles stay circles,
  no aspect re-bake), explicit re-root morph on dive/pull-back, free
  pan/zoom inspection, sector spotlight + hole label in the CG overlay,
  full interaction parity (hover pill, reveal, context menu, gestures).
- MapChrome: hover/menu/unavailable/a11y chrome shared by both views;
  MapModePicker toggle persisted via @AppStorage("mapMode").
- Tests: 8 SunburstWorld property tests (partition, camera independence,
  ε order-keeping, re-root composition, rings bounded, LOD hysteresis,
  tail closure, seam-safe polar helpers) — 116 green.
…), views land on zoomRoot

Switching treemap ⇄ sunburst recreated the NSView, which presented frames
while detached: drawables went to an uncomposited CAMetalLayer, the 3-deep
pool drained, and nextDrawable() timed out (~1 s each) — the treemap came
back black for ~10 s (until a data tick re-presented), the sunburst stayed
black for good (only the CG overlay drew: hole, hover outlines).

- Both NSViews: never present while window == nil; re-present on attach
  (viewDidMoveToWindow → setScale); renderers' draw() now returns whether a
  frame was actually presented, and a dropped frame schedules a one-shot
  retry so a map can never stay blank waiting for the next tick.
- TreemapNSView: a view (re)created while zoomed (mode switch) fits the
  camera on zoomRoot instead of the whole world — both projections agree
  on where you are; SPEC-13 §5 updated with the lifecycle post-mortem.

Verified live: toggle both ways now paints in < 0.4 s with full colours.
…el background

- The hole becomes the wheel's readout (dotMemory's retained-size centre):
  when a subtree is selected it shows its relative path + on-disk size,
  and falls back to the display root otherwise.
- Clicking the emptiness around the disc resets the selection, exactly
  like clicking the centre.
- The wheel now floats on the app's panel colour (the treemap keeps its
  dedicated darker canvas — its tiles cover it entirely); the hole digs
  below it on the window colour, in both themes.
The hole (disc + name + size) was gated on morphT >= 1, so every data
tick of a live scan — FSEvents reconciliation morphs at up to a few per
second on a busy disk — hid the centre for the 0.32 s of the morph and
brought it back: a permanent blink. The hole is static world geometry
(arcs never enter it, its radius never animates): draw it whatever the
morph clock says.
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Jul 13, 2026
@rducom
rducom merged commit 50536eb into main Jul 13, 2026
6 checks passed
@rducom
rducom deleted the feat/sunburst-view branch July 13, 2026 15:48
@rducom rducom added enhancement New feature or request and removed documentation Improvements or additions to documentation labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant