diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index cc01bb21e..d3a5ea131 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -60,13 +60,6 @@ From [ADR 0001 §"explicitly not deciding"](adrs/0001-fade-ownership.md): Diagnosed but unplanned. Captured here so they don't get lost; promote to a spec or plan when prioritised. Most have richer notes in agent memory (`~/.claude/projects/-Users-rulkens-Development-js-skymap/memory/`). -- **Make the codebase `poi`-free (`poi*` → `structure*`)** — **Priority: high.** The data layer is already `StructureRecord` and the InfoCard family was renamed to `StructureDetailCard` / `CompactStructureCard` (`structure` props, `Structure` eyebrow). The legacy `poi` vocabulary survives in the engine/identity/URL layers and reads as a second name for the same concept — the exact "two names, one thing" wart the simplicity convention flags. Holdouts found 2026-06-07: - - **`isPoi` predicate** (`services/engine/isPoi.ts`) — imported by ~8 non-InfoCard sites (`FocusableTarget.d.ts`, `EngineCallbacks.d.ts`, `EngineCameraHandle.d.ts`, `useUrlSync.ts`, `useStructureMemberCount.ts`, `engine.ts`, `commitFocus.ts` + tests). Rename to `isStructure`. - - **`POI_CATEGORY_INFO` / `data/poiCategoryInfo.ts` / `PoiCategory` type** (`@types/engine/data/PoiCategory.d.ts`) — the category-info table + its type alias; co-design with the `STRUCTURE_CATEGORY_META` consolidation in the DRY item below (don't merge the genuinely-different category *lists*). - - **`poiUrl` / `resolvePoiFromPick`** (`services/url/poiUrl.ts`, `services/engine/helpers/resolvePoiFromPick.ts`) and the engine `selectedPoi` params in `selectionRingPass.ts` / `EngineSubsystemHandles.d.ts` / `engine.ts`. - - **⚠️ The `#poi=` deep-link URL param** (`services/url/poiUrl.ts`, `utils/url/hasDeepLink.ts`, `App.tsx`) is a **persisted format** — existing shared links use `#poi=cluster-…`. Renaming the param string would break them, so this needs a back-compat read (accept both `#poi=` and a new `#structure=`) or must be deliberately left as-is. Decide explicitly; do **not** blind-rename. - - **Approach:** run `entanglement-radar` first to map the knots and the `#poi=` back-compat boundary, then apply via `/simplify` (delegate the edits per `feedback_simplify_edits_in_subagent`). Pairs with the `cluster* → structure*` naming migration and the structure-category-identity DRY item below — all three are the same "converge on `Structure*` vocabulary" cleanup. Mechanical apart from the URL-param decision; typecheck + tests are the safety net. - **Mobile layout reflow** — hover-on-touch is handled (`disable hover on touch input`, #226: hover-only affordances now route through tap). What remains is the general responsive layout pass: reflow the InfoCard / SettingsPanel / StatusBar for narrow viewports so the UI is usable on a phone, not just non-broken. **Update 2026-06-07:** the InfoCard half is specced — [2026-06-07 mobile bottom-sheet design](superpowers/specs/2026-06-07-mobile-info-card-bottom-sheet-design.md) (awaiting plan); the SettingsPanel launcher is a gated fast-follow pending an `entanglement-radar` pass over `SettingsPanel`. - **Lower-tier "close to home" weighting** — retune the small/medium tier subsampling so more galaxies survive near the camera's home position for maximum visual density on first load, while keeping the on-screen count fast. Distinct from the deliberate SDSS far-shell sample (memory `project_sdss_medium_intentionally_far`). - **Densely seed the Local Volume across all tiers (group explorability)** — surfaced 2026-06-04 with the `group` category. The 16 Local Volume groups are only interesting to fly into if their *member* galaxies are present, but `subsampleByAbsMag` (`tools/catalog/`) thins the nearby volume by absolute-magnitude cut, so faint dwarfs in the Local Group / M81 / Cen A / Sculptor etc. get culled — a group ring you focus into can be nearly empty at small/medium tier. Bias the subsampling to **keep galaxies inside (or near) the featured group spheres** regardless of `M_abs`, across small + medium and ideally large tiers, so each group has as many members as possible. Related to but distinct from the "close to home" weighting above: that's camera-home density; this is per-group membership density keyed off the structure seed. Implementation hooks: the group seed positions/radii (`data/structure_anchors.seed.json`) are available to the build, so the subsampler can spare points within `apparentRadiusMpc` of each group centre. Keep an eye on the on-screen count budget. Pairs with the cluster-focus member count (`PoiDetailCard` "Galaxies" row) — denser seeding makes that number meaningful at lower tiers. diff --git a/docs/superpowers/plans/completed/2026-06-08-poi-free-codebase.md b/docs/superpowers/plans/completed/2026-06-08-poi-free-codebase.md new file mode 100644 index 000000000..68cd56bf4 --- /dev/null +++ b/docs/superpowers/plans/completed/2026-06-08-poi-free-codebase.md @@ -0,0 +1,344 @@ +# Make the codebase `poi`-free — Implementation Plan + +> **Completed 2026-06-08** (PR #288). All three phases shipped; codebase is `poi`-free apart from the deliberate `#poi= is NOT a deep link` regression test. Follow-up shipped in the same PR: `CategoryLabelLayer` names the `'galaxyNames' | 'structure'` label-layer subset (was a raw literal union in five places). DoD audit: tests + typecheck + build green, no new TODOs, test parity improved. + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remove all `poi`/`Poi`/`POI` vocabulary from `src/` + mirrored `tests/`, dissolving the two distinct concepts it hid (the label/marker category superset, and "structure") into honest, registry-derived names. + +**Architecture:** Three phases. **A** dissolves `PoiCategory` into registry-derived capability flags (`bearsLabel`/`bearsMarker`/`labelLayer`) + a derived `LabelCategory` type, narrows the marker axis to `StructureCategory`, folds the display-metadata table into `SOURCE_REGISTRY`, and de-special-cases the visibility setters. **B** folds the `#poi=` deep-link into `#focus=` (deletes the `poiUrl` codec, adds a `structure` variant to `FocusTarget`). **C** is the mechanical `poi → structure` rename of every site where `poi` already means "structure". Keep build + typecheck green at every commit. + +**Tech Stack:** TypeScript, Vite, React, Vitest. Source of truth: `docs/superpowers/specs/2026-06-08-poi-free-codebase-design.md`. Plan style: `docs/superpowers/conventions/plan-style.md` (contract code only — signatures + test names; no implementation bodies; cite files, don't paste). + +**Execution notes:** Executing subagents cannot run `npm`/`npx`; the main thread runs `npm test -- ` / `npm run typecheck` and commits. Stage specific paths only (never `git add -A`/`.`). Prettier only touched files. Commit as the user's git identity with a `Co-Authored-By: Claude Opus 4.8 (1M context) ` trailer. + +--- + +## Phase A — Dissolve `PoiCategory` into registry-derived axes + +### Task A1: Add capability + display fields to the registry rows + +**Files:** + +- Modify: `src/@types/data/SourceEntryBase.d.ts` +- Modify: `src/data/sources.ts` (every `SOURCE_ENTRIES` row) +- Test: `tests/data/sources.test.ts` + +**Contract** — add to `SourceEntryBase`: + +```ts +/** True if this category carries toggleable on-screen text labels. */ +readonly bearsLabel: boolean; +/** True if this category carries a ring/halo marker (structures only today). */ +readonly bearsMarker: boolean; +/** Fade layer the labels live on. Present iff bearsLabel. */ +readonly labelLayer?: 'galaxyNames' | 'structure'; +/** Long form for detail surfaces ("Galaxy Cluster"). Present iff bearsLabel. */ +readonly detailLabel?: string; +/** Compact form for chips/previews ("Cluster"). Present iff bearsLabel. */ +readonly shortLabel?: string; +/** Plural for list/toggle headers ("Clusters"). Present iff bearsLabel. */ +readonly plural?: string; +``` + +These three display fields are **explicit** on each label-bearing row — deliberately NOT derived from the source `label`, because the famous-galaxy _source_ label (`'Famous'`) differs from the _category_ display copy. Decoupling them is what keeps the source name and the category name independent. + +Per-row values (see spec §A1/§A2 table): + +- `famousGalaxy`: `bearsLabel: true, bearsMarker: false, labelLayer: 'galaxyNames', detailLabel: 'Famous Galaxy', shortLabel: 'Galaxy', plural: 'Famous Galaxies'`. (Keep its existing source `label: 'Famous'` unchanged — only the category display fields are added.) +- `cluster/supercluster/void/group`: `bearsLabel: true, bearsMarker: true, labelLayer: 'structure'`, with `detailLabel`/`shortLabel`/`plural` copied verbatim from the old `POI_CATEGORY_INFO` rows (`src/data/poiCategoryInfo.ts:24-50`). +- `sdss/glade/2mrs/milliquas/synthetic`: `bearsLabel: false, bearsMarker: false` (omit `labelLayer`/`detailLabel`/`shortLabel`/`plural`). + +- [x] Add tests: `famousGalaxy row bears a label but no marker` (assert `bearsLabel` true, `bearsMarker` false, `labelLayer === 'galaxyNames'`); `famousGalaxy category copy is detail 'Famous Galaxy' / short 'Galaxy' / plural 'Famous Galaxies'`; `structure rows bear both a label and a marker` (loop cluster/supercluster/void/group); `bulk survey rows bear neither` (assert sdss/glade false/false). +- [x] Run: `npm test -- sources` → new tests FAIL (fields absent). +- [x] Add the five fields to `SourceEntryBase`; populate every row in `sources.ts`. +- [x] Run: `npm test -- sources` and `npm run typecheck` → PASS. +- [x] Commit (`git add src/@types/data/SourceEntryBase.d.ts src/data/sources.ts tests/data/sources.test.ts`). + +### Task A2: Derive `LABEL_CATEGORIES` + `LabelCategory` from the registry + +**Files:** + +- Create: `src/data/labelCategories.ts` +- Create: `src/@types/engine/data/LabelCategory.d.ts` +- Test: `tests/data/labelCategories.test.ts` + +**Contract** (mirror `src/data/structureCategories.ts` + `src/@types/engine/data/StructureCategory.d.ts`): + +```ts +// src/data/labelCategories.ts +export const LABEL_CATEGORIES = /* SOURCE_ENTRIES filtered by bearsLabel, mapped to .id, registry order */; +// src/@types/engine/data/LabelCategory.d.ts +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; +``` + +- [x] Add tests in `tests/data/labelCategories.test.ts`: `LABEL_CATEGORIES contains famousGalaxy and the four structure categories` (assert sorted equals `['cluster','famousGalaxy','group','supercluster','void']`); `LABEL_CATEGORIES is a superset of STRUCTURE_CATEGORIES`; `LABEL_CATEGORIES excludes bulk surveys` (no `'sdss'`/`'glade'`). +- [x] Run: `npm test -- labelCategories` → FAIL (module absent). +- [x] Implement `labelCategories.ts` (filter `SOURCE_ENTRIES` by `bearsLabel`) and the `LabelCategory` type alias. +- [x] Run: `npm test -- labelCategories` + `npm run typecheck` → PASS. +- [x] Commit. + +### Task A3: Fold the display table into a registry-derived accessor + +**Files:** + +- Create: `src/data/categoryDisplayInfo.ts` (derived `CATEGORY_DISPLAY_INFO` keyed by `LabelCategory`, fields `{ label, shortLabel, plural }` sourced from the registry rows' `detailLabel`/`shortLabel`/`plural`) +- Modify: every `POI_CATEGORY_INFO` consumer (enumerate with `rg -l 'POI_CATEGORY_INFO|poiCategoryInfo|PoiCategoryInfo' src` — expect InfoCard / SettingsPanel family) +- Delete: `src/data/poiCategoryInfo.ts` +- Test: `tests/data/categoryDisplayInfo.test.ts` (rename from any `poiCategoryInfo` test if one exists; else new) + +**Contract:** + +```ts +export type CategoryDisplayInfo = { label: string; shortLabel: string; readonly plural: string }; +export const CATEGORY_DISPLAY_INFO: Readonly>; +``` + +Mapping: `label ← row.detailLabel`; `shortLabel ← row.shortLabel`; `plural ← row.plural` (all explicit registry fields — see A1). + +- [x] Add tests: `CATEGORY_DISPLAY_INFO has a row per LabelCategory`; `cluster renders 'Galaxy Cluster' / 'Cluster' / 'Clusters'`; `famousGalaxy renders 'Famous Galaxy' / 'Galaxy' / 'Famous Galaxies'`. +- [x] Run → FAIL. +- [x] Implement `categoryDisplayInfo.ts` deriving from the registry; repoint every `POI_CATEGORY_INFO[x]` read to `CATEGORY_DISPLAY_INFO[x]`; delete `poiCategoryInfo.ts`. +- [x] Run the touched tests + `npm run typecheck` → PASS. Confirm `rg 'POI_CATEGORY_INFO|poiCategoryInfo' src` is empty. +- [x] Commit. + +### Task A4: Rename `PoiCategory` → `LabelCategory`; retype the visibility records + +**Files (enumerate first with `rg -l 'PoiCategory' src`):** `@types/settings/EngineSettingsState.d.ts`, `@types/settings/UseEngineSettingsState.d.ts`, `@types/engine/EngineCallbacks.d.ts`, `@types/engine/wiring/SettingsCallbackSeed.d.ts`, `@types/engine/subsystems/Selection.d.ts`, `@types/engine/handles/EngineLabelsHandle.d.ts`, `@types/engine/UseEngineReturn.d.ts`, `src/hooks/useEngine.ts`, `src/hooks/useEngineSettings.ts`, `src/components/SettingsPanel/SettingsPanel.tsx`, `src/services/engine/engine.ts`, `src/services/engine/labelStyleOverride.ts`, `src/services/engine/wiring/assetWiring.ts`, the 4 visibility-default fixtures under `tests/`. + +- Delete: `src/@types/engine/data/PoiCategory.d.ts` + +**Contract changes:** + +```ts +readonly labelCategoryVisibility: Readonly>; +readonly markerCategoryVisibility: Readonly>; // narrowed — no famousGalaxy +// labelStyleOverride.ts: +export type LabelStyleOverrideTarget = 'youAreHere' | LabelCategory; +``` + +Default-visibility literals (`engine.ts:451-465`, `useEngineSettings.ts:174-188`) derive from `LABEL_CATEGORIES` / `STRUCTURE_CATEGORIES` instead of hand-listed `{ famousGalaxy: true, cluster: true, ... }`. + +- [x] Update/add fixture + type tests first where assertions exist (`tests/@types/engineSettingsState.labelCategoryVisibility.test.ts`): assert `markerCategoryVisibility` has no `famousGalaxy` key and `labelCategoryVisibility` does. +- [x] Run → FAIL. +- [x] Replace `PoiCategory` with `LabelCategory` import-by-import; narrow the marker record key type to `StructureCategory`; derive the default literals from the category sets; delete `PoiCategory.d.ts`. +- [x] Run `npm run typecheck` + the touched tests → PASS. `rg 'PoiCategory' src tests` empty. +- [x] Commit. + +### Task A5: De-special-case the visibility setters + +**Files:** + +- Modify: `src/services/engine/engine.ts:261-323` (`setCategoryLabelVisible`, `setCategoryMarkerVisible`) +- Test: existing `tests/services/engine/setCategoryVisibleFade.test.ts`, `tests/services/engine/setSourceVisibleFade.test.ts` + +**Contract:** + +```ts +function setCategoryMarkerVisible(state, cb, category: StructureCategory, visible: boolean): void; +function setCategoryLabelVisible(state, cb, category: LabelCategory, visible: boolean): void; +``` + +- `setCategoryMarkerVisible`: drop the `if (category !== 'famousGalaxy')` guard entirely — every `StructureCategory` fires a `markerLayer` fade. +- `setCategoryLabelVisible`: replace the `if (category === 'famousGalaxy')` branch with a dispatch on the row's `labelLayer` field — `'galaxyNames'` fires the `galaxyNames` labelLayer fade (+ `setFamousLabelsVisible`); `'structure'` fires the structure labelLayer fade keyed by `category`. + +- [x] Update tests: `setCategoryMarkerVisible fires a markerLayer fade for every structure category`; `setCategoryLabelVisible routes famousGalaxy to the galaxyNames layer`; `...routes a structure category to the structure label layer`. +- [x] Run → FAIL (guards still present / wrong signatures). +- [x] Rewrite both setters per the contract. +- [x] Run the two test files + `npm run typecheck` → PASS. +- [x] Commit. + +--- + +## Phase B — Fold `#poi=` into `#focus=` + +### Task B1: Add the `structure` variant to the focus codec + +**Files:** + +- Modify: `src/@types/camera/FocusTarget.d.ts` +- Modify: `src/services/url/focusUrl.ts` (`parseFocusHash`, `selectionToFocusId`) +- Test: `tests/services/url/focusUrl.test.ts` + +**Contract:** + +```ts +export type FocusTarget = + | { kind: 'famous'; id: string } + | { kind: 'pgc'; pgc: bigint } + | { kind: 'sdss'; objID: bigint } + | { kind: 'pos'; raDeg: number; decDeg: number } + | { kind: 'structure'; id: string }; // NEW +``` + +- `parseFocusHash`: a `focus=` body whose id starts with `cluster-` / `supercluster-` / `void-` / `group-` → `{ kind: 'structure', id }`; the existing pgc/sdss/pos/famous ladder is otherwise unchanged. Derive the prefix set from `STRUCTURE_CATEGORIES` (no hardcoded list). +- `selectionToFocusId(info: GalaxyInfo)` is galaxy-only; add a sibling path so a structure selection encodes to its `id` verbatim. (Check how `computeDesiredHash` calls it in B3 — the structure branch may bypass `selectionToFocusId` and use `record.id` directly. Pick whichever keeps the codec galaxy/structure split clean.) + +- [x] Add tests: `parseFocusHash routes cluster-virgo-m87 to kind structure`; one per category prefix; `a famous id without a structure prefix still routes to kind famous`; `pgc-/sdss-/pos@ ladder unchanged` (regression). +- [x] Run: `npm test -- focusUrl` → FAIL. +- [x] Implement the variant + prefix routing. +- [x] Run → PASS + `npm run typecheck`. +- [x] Commit. + +### Task B2: Drop `#poi=` from `hasDeepLink` + +**Files:** + +- Modify: `src/utils/url/hasDeepLink.ts:44-58` +- Test: `tests/utils/url/hasDeepLink.test.ts` + +- [x] Add/adjust tests: `#focus=cluster-virgo-m87 is a deep link`; `#poi=... is NOT a deep link` (documents the intentional break); `#focus=m31` regression still true. +- [x] Run → the `#poi=` test FAILs (still treated as deep link). +- [x] Remove the `#poi=` branch (`hasDeepLink.ts:48`); update the docblock (drop the `#poi=` bullet). +- [x] Run → PASS. +- [x] Commit. + +### Task B3: Route structures through the single focus codec in `useUrlSync` + +**Files:** + +- Modify: `src/hooks/useUrlSync.ts` (imports, `InitialPending`, `computeDesiredHash`, `initialPendingFromHash`, hook body slot rename) +- Test: `tests/hooks/useUrlSync.test.ts` + +**Contract:** + +```ts +export type InitialPending = + | { kind: 'galaxy'; target: FocusTarget } + | { kind: 'structure'; id: string } // was { kind: 'poi'; poiId } + | { kind: null }; +``` + +- Remove the `parsePoiHash` import + call. `initialPendingFromHash` parses one `#focus=` hash; a `kind:'structure'` `FocusTarget` → `{ kind:'structure', id }`, everything else → `{ kind:'galaxy', target }`. +- `computeDesiredHash`: a structure focus writes `focus=` (not `poi=`). A galaxy writes `focus=` as today. +- Rename state slot `pendingPoiId` → `pendingStructureId` (and its setter); update the popstate handler + effect 4 (the structure drain) + the `UrlSyncReturn` shape (`@types/engine/UrlSyncReturn.d.ts`). +- Keep BOTH pending slots and both drains — the resolution sources differ (async catalogs vs synchronous structure table); do not merge them. + +- [x] Update tests: `initialPendingFromHash routes #focus=cluster-... to kind structure`; `computeDesiredHash writes focus= for a structure target`; `galaxy hashes unchanged`; rename existing `poi`-named cases. +- [x] Run: `npm test -- useUrlSync` → FAIL. +- [x] Apply the changes (the `camera.focusOn` drain already accepts structures — no logic change there, just the slot/parse rename). +- [x] Run → PASS + `npm run typecheck`. +- [x] Commit. + +### Task B4: Delete the `poiUrl` codec + +**Files:** + +- Delete: `src/services/url/poiUrl.ts`, `tests/services/url/poiUrl.test.ts` + +- [x] Confirm `rg 'poiUrl|parsePoiHash|poiIdToHash' src tests` shows only the files to delete (B1/B3 removed all consumers). +- [x] Delete both files. +- [x] Run `npm run typecheck` + `npm test -- url` → PASS. +- [x] Commit (`git rm` the two paths). + +--- + +## Phase C — Mechanical `poi → structure` renames + +> For each task: enumerate sites with `rg`, rename via edits + `git mv` for files, update the mirrored test, keep typecheck green. These are pure renames — no behaviour change. + +### Task C1: `isPoi` → `isStructure` + +**Files:** `git mv src/services/engine/isPoi.ts src/services/engine/isStructure.ts`; `git mv tests/services/engine/isPoi.test.ts tests/services/engine/isStructure.test.ts`; consumers via `rg -l '\bisPoi\b' src tests` (expect `pickToSelection`/`useUrlSync`/`commitFocus`/`engine.ts` + several `@types` JSDoc refs). + +**Contract:** `export function isStructure(target: FocusableTarget): target is StructureRecord` + +- [x] `git mv` both files; rename the function + all call sites + the test `describe`/imports. +- [x] Run `npm test -- isStructure` + `npm run typecheck` → PASS. `rg '\bisPoi\b' src tests` empty. +- [x] Commit. + +### Task C2: `resolvePoiFromPick` → `resolveStructureFromPick` + +**Files:** `git mv src/services/engine/helpers/resolvePoiFromPick.ts → resolveStructureFromPick.ts`; `git mv` its test; modify `src/services/engine/helpers/pickToSelection.ts` (consumer). + +**Contract:** + +```ts +export type PickStructureInput = { + readonly category: StructureCategory; + readonly structureIndex: number; +}; +export function resolveStructureFromPick( + structures: PickStructureStore, + input: PickStructureInput, +): StructureRecord | null; +``` + +Note: the input `category` can narrow to `StructureCategory` now (the `famousGalaxy` early-return guard at `resolvePoiFromPick.ts:60` becomes unnecessary once the caller passes a `StructureCategory` — verify the caller's type; keep a defensive guard only if the caller still supplies a wider type). + +- [x] Rename file + fn + type + the `poiIndex` field → `structureIndex`; update `pickToSelection.ts` + the test. +- [x] Run the test + `npm run typecheck` → PASS. +- [x] Commit. + +### Task C3: `structurePoiStyles` → `structureMarkerStyles` + +**Files:** `git mv src/services/engine/presentation/structurePoiStyles.ts → structureMarkerStyles.ts`; `git mv tests/data/poiCategories.test.ts → tests/data/structureMarkerStyles.test.ts`; consumers via `rg -l 'STRUCTURE_POI_STYLES|structurePoiStyles' src tests` (expect `produceStructureMarkers`/`produceStructureLabels`). + +**Contract:** export `STRUCTURE_MARKER_STYLES` (type `StructureMarkerStyle` keeps its name); `SIG_MIN_ALPHA` unchanged. + +- [x] `git mv` both; rename the export + consumers; in the test, drop the `PoiCategory` import (use `StructureCategory`) and rename the `describe`. +- [x] Run `npm test -- structureMarkerStyles` + `npm run typecheck` → PASS. `rg 'STRUCTURE_POI_STYLES|structurePoiStyles' src tests` empty. +- [x] Commit. + +### Task C4: `poiIndex` → `structureIndex` in the marker/pick path + +**Files (sites not covered by C2):** `src/services/gpu/renderers/structureMarkerRenderer.ts`, `src/@types/rendering/StructureMarkerRenderer.d.ts`, `src/services/engine/presentation/produceStructureMarkers.ts`, `src/@types/engine/CreateClickResolverInput.d.ts`; mirrored tests (`structureMarkerRenderer.*.test.ts`, `ringPick.test.ts`). + +- [x] `rg -n '\bpoiIndex\b' src tests` → rename every remaining occurrence to `structureIndex` (field names, locals, comments). +- [x] Run the affected renderer tests + `npm run typecheck` → PASS. `rg '\bpoiIndex\b' src tests` empty. +- [x] Commit. + +### Task C5: `FocusState.poiId` → `structureId`; widen `category` + +**Files:** `src/@types/engine/state/FocusState.d.ts`; consumers via `rg -l 'poiId' src` (expect `structureFocusSubsystem`, `commitStructureFocus`, `structureMembership`, the membership-cache key); mirrored tests. + +**Contract:** + +```ts +readonly structureId: string; // was poiId +readonly category: StructureCategory; // was 'cluster' | 'supercluster' | 'void' (now includes group) +``` + +- [x] Update tests asserting `FocusState` shape / the membership cache key first. +- [x] Rename `poiId` → `structureId` everywhere; widen `category` to `StructureCategory`; check the membership-cache key string (`structureMembership.ts:12`) and any focus-framing switch handles `group` (it should, since group is already a focusable structure). +- [x] Run the focus/membership tests + `npm run typecheck` → PASS. +- [x] Commit. + +### Task C6: `selectedPoi` → `selectedStructure` + +**Files:** `rg -ln 'selectedPoi' src` → `src/services/engine/frame/passes/selectionRingPass.ts`, `src/@types/engine/handles/EngineSubsystemHandles.d.ts`, `src/services/engine/engine.ts` (mostly param names + comments). + +- [x] Rename the param/comment occurrences to `selectedStructure`. +- [x] Run `npm run typecheck` + `npm test -- selectionRingPass` → PASS. +- [x] Commit. + +### Task C7: Doc-comment + remaining-token sweep + +**Files:** every `poi`/`POI` left in comments. Enumerate: `rg -n 'poi|POI' src tests -g '*.ts' -g '*.tsx' -g '*.wesl' | rg -iv 'point|poisson|poison'`. + +- [x] For each remaining hit (docblocks in `commitGalaxyFocus`, `commitStructureFocus`, `clearAll`, `selectionSubsystem`, `buildStaticAnchorStructures`, `assetWiring`, `useUrlSync`, `useSplash`, `App.tsx`, etc.) reword "POI"/"poi" to "structure" (or "deep link" where it described the URL generically). Follow `feedback_comment_style` — timeless + terse, no history notes. +- [x] `rg 'poi|POI' src tests -g '*.ts' -g '*.tsx' -g '*.wesl' | rg -iv 'point|poisson|poison'` → empty. +- [x] Run `npm run typecheck` → PASS. +- [x] Commit. + +### Task C8: Rename the remaining `*.poi.test.ts` files + final verification + +**Files:** `git mv tests/services/engine/phases/wireInput.poi.test.ts → wireInput.structure.test.ts`; `git mv tests/services/gpu/renderers/pickRenderer.poi.test.ts → pickRenderer.structure.test.ts`; `git mv tests/@types/engine/EngineCameraHandle.poi.test.ts → EngineCameraHandle.structure.test.ts`. Update each `describe`/imports if they reference renamed symbols. + +- [x] `git mv` the three files; fix any internal `poi` references. +- [x] **Entanglement-radar verification:** run the `entanglement-radar` lens over `git diff main...HEAD` — confirm the spec's un-braided choices survived: `PoiCategory` gone (not re-spelled), marker record narrowed (no phantom `famousGalaxy`), the two URL resolution paths NOT merged, the famousGalaxy `if`-branches dissolved into `labelLayer` data. Note any regressions and fix before finishing. +- [x] **Full sweep:** `find src tests -iname '*poi*'` empty; `rg 'poi|POI' src tests -g '*.ts' -g '*.tsx' -g '*.wesl' | rg -iv 'point|poisson|poison'` empty. +- [x] Run full `npm test` + `npm run typecheck` + `npm run build` → all green. +- [x] Prettier all touched files; commit. + +--- + +## Self-review notes + +**Spec coverage:** A1–A5 cover spec Part A (capability flags A1, display fold A2/A3, derived types A2, record retype A4, setter de-special-case A5). B1–B4 cover Part B (codec variant B1, hasDeepLink B2, useUrlSync B3, delete B4). C1–C8 cover Part C (every rename row in the spec's Part C table + the test renames + the final entanglement-radar gate). + +**Judgement calls (flag for review):** + +1. **`famousGalaxy` display copy — RESOLVED (2026-06-08):** the category display fields are explicit on the row (not derived from the source `label`), so the famous _source_ keeps `label: 'Famous'` while the _category_ reads detail `'Famous Galaxy'` / short `'Galaxy'` / plural **`'Famous Galaxies'`** (user's call: toggle/header shows "Famous Galaxies"). Baked into A1/A3 above. +2. **`tests/data/poiCategories.test.ts` fate:** the spec suggested re-scoping it to the derived sets, but it currently tests the _style table_. This plan instead renames it to `structureMarkerStyles.test.ts` (C3, style assertions) and creates a fresh `labelCategories.test.ts` (A2, derived sets). Two clear tests beats one re-scoped one. +3. **`resolveStructureFromPick` defensive guard:** the `famousGalaxy` early-return may become dead once the caller passes `StructureCategory` (C2). Drop it only if the caller's type genuinely narrows; otherwise keep it. diff --git a/docs/superpowers/specs/completed/2026-06-08-poi-free-codebase-design.md b/docs/superpowers/specs/completed/2026-06-08-poi-free-codebase-design.md new file mode 100644 index 000000000..359096946 --- /dev/null +++ b/docs/superpowers/specs/completed/2026-06-08-poi-free-codebase-design.md @@ -0,0 +1,302 @@ +# Make the codebase `poi`-free — design + +**Date:** 2026-06-08 +**Status:** design (awaiting plan) +**Worktree/branch:** `worktree-make-codebase-poi-free` +**Backlog item:** "Make the codebase `poi`-free (`poi*` → `structure*`)" (Priority: high) + +## Motivation + +The data layer is already `StructureRecord` / `StructureStore` and the InfoCard +family is `StructureDetailCard` / `CompactStructureCard`. The legacy `poi` +("point of interest") vocabulary survives at the engine / identity / URL edges. +An `entanglement-radar` pass (2026-06-08) found the wart is sharper than a +straight rename: **`poi` is one name for two genuinely different concepts**, and +a blind `poi → structure` rename would silently merge them. + +### Concept A — `poi` already _means_ "structure" + +These sites are about structures specifically; renaming to `structure` makes the +name **more** accurate: + +- `isPoi(t): t is StructureRecord` — the predicate literally returns `StructureRecord`. +- `resolvePoiFromPick → StructureRecord`, `PickPoiInput`, `poiIndex` (ring-pick decode). +- `poiUrl.ts` / `parsePoiHash` / `poiIdToHash` / `#poi=` — encodes `StructureRecord.id`. +- `FocusState.poiId` — the focused structure id (membership cache key). +- `selectedPoi` engine param (`selectionRingPass`, `EngineSubsystemHandles`). +- `STRUCTURE_POI_STYLES` / `structurePoiStyles.ts` — already keyed by `StructureCategory`. + +### Concept B — `poi` means "the focusable category superset" + +`PoiCategory = StructureCategory | 'famousGalaxy'` is **not** a synonym for +`structure` — it is a strict superset including `famousGalaxy`. A mechanical +`Poi → Structure` rename here is a type collision (`StructureCategory` exists) +**and** a semantic lie (drops `famousGalaxy`). This type keys the SettingsPanel +**label/marker visibility toggles**. + +Tracing why `famousGalaxy` is a member exposes the actual braid +(`engine.ts:261-323`): + +- **Labels:** `famousGalaxy` _does_ bear labels (routed to the `galaxyNames` + fade layer); structures bear labels (routed to the structure label layer). +- **Markers:** `famousGalaxy` has **no** ring/halo marker — the marker toggle + for it is a dead no-op; only structures bear markers. + +So the two real axes are different sets: + +- **label-bearing** = `{famousGalaxy, cluster, supercluster, void, group}` +- **marker-bearing** = `{cluster, supercluster, void, group}` (structures only) + +The current code crushes both into one `PoiCategory` union and pays for it with +`if (category === 'famousGalaxy')` branches and a **phantom `famousGalaxy` key** +in the marker visibility record. "Having a label / having a marker" is a +_property of the thing_, not a category that singles out famous galaxies — which +is exactly what makes `famousGalaxy` look out of place in the union. + +## Goals + +1. No `poi`/`Poi`/`POI` identifier, filename, or doc-comment survives in `src/` + or the mirrored `tests/`. (`tools/` is already clean.) +2. `PoiCategory` and the `famousGalaxy` special-casing **dissolve** into + registry-derived data — `famousGalaxy` becomes "just a row" with capability + flags, not a hand-listed exception. +3. The `#poi=` deep-link folds entirely into `#focus=`; the `poiUrl` codec is + deleted. + +## Non-goals + +- No back-compat for old `#poi=` shared links — they intentionally fall through + to "no deep link" (decided 2026-06-08). +- No change to the structure data layer, `.ccat` format, or the `Source.Cluster` + /`'cluster'` category vocabulary (already `Structure*`-clean). +- No render/visual behaviour change. The label & marker toggles behave + identically; only the marker record sheds its dead `famousGalaxy` key. + +--- + +## Part A — Dissolve `PoiCategory` into two registry-derived axes + +### A1. Registry capability flags + +Add to `SOURCE_REGISTRY` rows (via `SourceEntryBase` or per-variant): + +```ts +// SourceEntryBase additions +/** True if this category carries toggleable on-screen text labels. */ +readonly bearsLabel: boolean; +/** True if this category carries a ring/halo marker (structures only today). */ +readonly bearsMarker: boolean; +/** + * Which fade layer this category's labels live on. Present iff bearsLabel. + * - 'galaxyNames' — the shared galaxy-name layer (famousGalaxy) + * - 'structure' — the per-structure-category label layer + */ +readonly labelLayer?: 'galaxyNames' | 'structure'; +``` + +Settings per row: + +| row | bearsLabel | bearsMarker | labelLayer | +| ----------------------------------- | ---------- | ----------- | ------------- | +| famousGalaxy | true | false | 'galaxyNames' | +| cluster | true | true | 'structure' | +| supercluster | true | true | 'structure' | +| void | true | true | 'structure' | +| group | true | true | 'structure' | +| sdss/glade/2mrs/milliquas/synthetic | false | false | — | + +### A2. Display metadata folded into the registry + +The standalone `data/poiCategoryInfo.ts` (`POI_CATEGORY_INFO`, `PoiCategoryInfo`) +is **removed**. Its three fields move onto the label-bearing registry rows. The +existing `SourceEntryBase.label` already holds the short UI name +(`'Cluster'`, `'Famous'`), so only the longer + plural forms are new: + +```ts +// SourceEntryBase additions (present iff bearsLabel) +/** Long form for detail surfaces ("Galaxy Cluster"). */ +readonly detailLabel?: string; +/** Plural for list/toggle headers ("Clusters"). */ +readonly plural?: string; +``` + +Mapping from the old table: old `shortLabel` ≡ existing `label`; old `label` +→ `detailLabel`; old `plural` → `plural`. (Note `famousGalaxy`'s existing +registry `label` is `'Famous'` but its old `shortLabel` was `'Galaxy'` — the +detail/short copy is reconciled in the plan; default to the old display-table +strings since those are what the UI renders today.) + +A derived accessor (e.g. `categoryDisplayInfo(id)` or a derived +`CATEGORY_DISPLAY_INFO` map built from the registry) replaces every +`POI_CATEGORY_INFO[cat]` read. + +### A3. Derived category sets + types + +Derive from the registry (companion to `structureCategories.ts`): + +```ts +// data/labelCategories.ts (new) — label-bearing rows, registry order +export const LABEL_CATEGORIES = /* registry rows where bearsLabel */; +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; +// = 'famousGalaxy' | 'cluster' | 'supercluster' | 'void' | 'group' +``` + +- `LabelCategory` **replaces `PoiCategory`** (honest name; still contains + `famousGalaxy` because famous galaxies genuinely _are_ the labelled galaxies — + that is data, not a special case). `@types/engine/data/PoiCategory.d.ts` is + deleted. +- The **marker** axis narrows to `StructureCategory` (markers are structure-only). + +### A4. Visibility records re-typed + +- `labelCategoryVisibility: Readonly>` +- `markerCategoryVisibility: Readonly>` + — **drops the phantom `famousGalaxy` key.** + +Touches `EngineSettingsState`, `UseEngineSettingsState`, `EngineCallbacks`, +`SettingsCallbackSeed`, `Selection`, `EngineLabelsHandle`, `useEngineSettings`, +`SettingsPanel`, `engine.ts` defaults, the 4 test fixtures. The hand-written +`{ famousGalaxy: true, cluster: true, ... }` default literals derive from the +category sets instead (also retires the backlog's "copy-pasted 8× +DEFAULT_CATEGORY_VISIBILITY" sub-item for these records). + +### A5. Setters de-special-cased + +- `setCategoryMarkerVisible(category: StructureCategory, …)` — the + `if (category !== 'famousGalaxy')` no-op guard is **deleted**. +- `setCategoryLabelVisible(category: LabelCategory, …)` — fires the fade on the + layer named by the row's `labelLayer` field (A1), replacing the + `if (category === 'famousGalaxy')` branch. `galaxyNames` → fade + `{kind:'labelLayer', layer:'galaxyNames'}` (plus `setFamousLabelsVisible`); + `structure` → fade `{kind:'labelLayer', layer:'', category}`. + +--- + +## Part B — Fold `#poi=` into `#focus=` + +`camera.focusOn()` is already unified (accepts `GalaxyInfo | StructureRecord`), +so only the URL codec layer is split. + +### B1. Single codec + +- **Delete** `services/url/poiUrl.ts` (`parsePoiHash`, `poiIdToHash`) and its + test. +- `FocusTarget` gains a structure variant: + + ```ts + export type FocusTarget = + | { kind: 'famous'; id: string } + | { kind: 'pgc'; pgc: bigint } + | { kind: 'sdss'; objID: bigint } + | { kind: 'pos'; raDeg: number; decDeg: number } + | { kind: 'structure'; id: string }; // NEW + ``` + +- `parseFocusHash`: a `focus=` body whose id begins with a structure-category + prefix (`cluster-` / `supercluster-` / `void-` / `group-`) parses to + `{ kind: 'structure', id }`; otherwise the existing galaxy ladder is unchanged. + (Galaxy famous-ids never use those prefixes, so no collision.) +- `selectionToFocusId`: for a structure selection, return `info.id` verbatim + (structure ids are already curated + stable, e.g. `cluster-virgo-m87`); the + galaxy ladder is unchanged. + +### B2. `hasDeepLink` + +Drop the `#poi=` branch. The `#focus=` branch now also covers structures. + +### B3. `useUrlSync` + +Keep the **two pending slots** — galaxy resolution is async against loaded +catalogs; structure resolution is a synchronous table lookup. They are genuinely +different resolution sources and must not be complected. The fold is purely at +the codec/parse-routing level: + +- `initialPendingFromHash` parses one `#focus=` hash; a `kind:'structure'` + result routes to the structure slot, everything else to the galaxy slot. The + second parser call (`parsePoiHash`) is removed. +- `InitialPending`'s `{ kind: 'poi'; poiId }` → `{ kind: 'structure'; id }`. +- Slot/state rename: `pendingPoiId` → `pendingStructureId`. +- `computeDesiredHash`: a structure target writes `focus=` (via + `selectionToFocusId`), not `poi=`. + +--- + +## Part C — Mechanical `poi → structure` renames + +All sites where `poi` already means "structure": + +| Now | Becomes | +| ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `services/engine/isPoi.ts` `isPoi()` | `isStructure.ts` `isStructure()` | +| `resolvePoiFromPick.ts`, `resolvePoiFromPick`, `PickPoiInput`, `poiIndex` | `resolveStructureFromPick.ts`, `resolveStructureFromPick`, `PickStructureInput`, `structureIndex` | +| `structurePoiStyles.ts`, `STRUCTURE_POI_STYLES` | `structureMarkerStyles.ts`, `STRUCTURE_MARKER_STYLES` | +| `FocusState.poiId` | `FocusState.structureId` | +| `FocusState.category: 'cluster'\|'supercluster'\|'void'` | `StructureCategory` (also picks up the missing `group`) | +| `selectedPoi` param (`selectionRingPass`, `EngineSubsystemHandles`, `engine.ts`) | `selectedStructure` | +| `structureMarkerRenderer` `poiIndex` packing / `StructureMarkerRenderer` `poiIndex` | `structureIndex` | +| `produceStructureMarkers` `poiIndex` | `structureIndex` | +| `CreateClickResolverInput` `poiIndex` | `structureIndex` | + +Plus all `poi`/`POI` in doc-comments (`commitGalaxyFocus`, `commitStructureFocus`, +`clearAll`, `selectionSubsystem`, `buildStaticAnchorStructures`, +`structureMembership`, `assetWiring`, `FocusState`, …). + +Test files renamed to match: `isPoi.test.ts` → `isStructure.test.ts`, +`resolvePoiFromPick.test.ts` → `resolveStructureFromPick.test.ts`, +`wireInput.poi.test.ts` → `wireInput.structure.test.ts`, +`pickRenderer.poi.test.ts` → `pickRenderer.structure.test.ts`, +`EngineCameraHandle.poi.test.ts` → `EngineCameraHandle.structure.test.ts`, +`poiCategories.test.ts` → `labelCategories.test.ts` (re-scoped to the derived +sets). `poiUrl.test.ts` is **deleted**; its structure-id round-trip cases move +into `focusUrl.test.ts`. + +--- + +## Type changes (contract) + +```ts +// @types/engine/data/LabelCategory.d.ts (replaces PoiCategory.d.ts) +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; + +// @types/camera/FocusTarget.d.ts — add the structure variant (B1) + +// @types/engine/state/FocusState.d.ts +readonly structureId: string; // was poiId +readonly category: StructureCategory; // was 'cluster' | 'supercluster' | 'void' + +// @types/settings/EngineSettingsState.d.ts +readonly labelCategoryVisibility: Readonly>; +readonly markerCategoryVisibility: Readonly>; + +// @types/data/SourceEntryBase.d.ts — add bearsLabel, bearsMarker, labelLayer?, +// detailLabel?, plural? (A1, A2) +``` + +## Testing + +- The rename is type-driven: a missed Concept-A/B site won't compile. + `npm run typecheck` + the full `npm test` suite are the safety net. +- New `focusUrl` tests: `#focus=cluster-virgo-m87` ↔ `{kind:'structure'}` + round-trip; each structure-category prefix; galaxy ladder regression + unaffected; a structure id is never mis-parsed as a famous id. +- New test: a `#poi=…` hash now yields no deep link (documents the intentional + break) — added to `hasDeepLink` / `focusUrl` tests. +- New `labelCategories` test: `LABEL_CATEGORIES` / `MARKER_CATEGORIES` derive + the expected sets from the registry; the marker set excludes `famousGalaxy`. +- `setCategoryMarkerVisible` / `setCategoryLabelVisible` tests updated for the + narrowed types and the `labelLayer`-driven routing. + +## Execution + +Per project workflow: `subagent-driven-development`. Edits delegated to +background implementer subagents; the main thread runs `npm test` / +`typecheck`, prettiers touched files, and commits. Branch + PR (squash-merge). +No data rebuild or R2 sync — this is code-only (the `cluster*→structure*` +artifact rename from #280 already handled the deploy side). + +## Out of scope (follow-ups) + +- The broader `STRUCTURE_CATEGORY_META` DRY consolidation beyond the display-info + fold done here (the `hasBulkCatalog` flag, the `POI_CATEGORIES_WITH_MARKERS` + derivation) — see `docs/BACKLOG.md`. +- Promoting the Milky Way to a first-class `Source` — separate backlog item. diff --git a/src/@types/animation/CategoryLabelLayer.d.ts b/src/@types/animation/CategoryLabelLayer.d.ts new file mode 100644 index 000000000..1443fc61c --- /dev/null +++ b/src/@types/animation/CategoryLabelLayer.d.ts @@ -0,0 +1,14 @@ +import type { LabelLayerId } from './LabelLayerId'; + +/** + * CategoryLabelLayer — the fade layers a label-bearing *category* can route to. + * + * This is the subset of `LabelLayerId` reachable from a SOURCE_REGISTRY row's + * `labelLayer` field. The other two layers (`youAreHere`, `scaleBar`) are + * singleton overlays with no owning category, so they're excluded here. + * + * Derived from `LabelLayerId` via `Extract` rather than re-spelled, so the + * subset can never name a layer that doesn't exist on the fade registry — and + * renaming a layer in `LabelLayerId` propagates here for free. + */ +export type CategoryLabelLayer = Extract; diff --git a/src/@types/animation/FadeHandle.d.ts b/src/@types/animation/FadeHandle.d.ts index fbf21bc1c..d75850345 100644 --- a/src/@types/animation/FadeHandle.d.ts +++ b/src/@types/animation/FadeHandle.d.ts @@ -25,9 +25,9 @@ * Discriminator: `category: StructureCategory`. One * controller per category so a category's rings can * fade independently of the others. - * - labelLayer — one logical label layer (you-are-here, POI, + * - labelLayer — one logical label layer (you-are-here, structure, * galaxy names, scale bar). Discriminator: - * `layer: LabelLayerId`. POI labels additionally key + * `layer: LabelLayerId`. Structure labels additionally key * on `category: StructureCategory` so each structure * category's labels are a distinct controller; the * other layers (youAreHere/galaxyNames/scaleBar) diff --git a/src/@types/animation/LabelLayerId.d.ts b/src/@types/animation/LabelLayerId.d.ts index e7355b7b8..c51e87363 100644 --- a/src/@types/animation/LabelLayerId.d.ts +++ b/src/@types/animation/LabelLayerId.d.ts @@ -11,11 +11,11 @@ * - youAreHere — the "YOU ARE HERE" Milky Way pin (a single label + * marker line). Fades in when the camera reaches the * band where the marker is meaningful. - * - poi — cluster + named-anchor labels emitted by + * - structure — cluster + named-anchor labels emitted by * `produceStructureLabels`. * - galaxyNames — per-galaxy name labels (currently unused but * reserved; see future plans for hover-name overlay). * - scaleBar — the on-screen scale-bar HUD. Constructed by React, * not a GPU layer; reserved for tour integration. */ -export type LabelLayerId = 'youAreHere' | 'poi' | 'galaxyNames' | 'scaleBar'; +export type LabelLayerId = 'youAreHere' | 'structure' | 'galaxyNames' | 'scaleBar'; diff --git a/src/@types/camera/FocusTarget.d.ts b/src/@types/camera/FocusTarget.d.ts index 823327f1a..93645fe2a 100644 --- a/src/@types/camera/FocusTarget.d.ts +++ b/src/@types/camera/FocusTarget.d.ts @@ -20,10 +20,15 @@ * sdss- — SDSS row whose objID is the canonical handle * (19-digit bigint, exceeds JS Number safe range) * pos@, — fallback for 2MRS/GLADE rows without a PGC + * cluster-virgo-m87 — structure record id: `${category}-${seed.id}` + * (category from STRUCTURE_CATEGORIES; stable + * across rebuilds because the seed id is curated) */ export type FocusTarget = | { kind: 'famous'; id: string } | { kind: 'pgc'; pgc: bigint } | { kind: 'sdss'; objID: bigint } - | { kind: 'pos'; raDeg: number; decDeg: number }; + | { kind: 'pos'; raDeg: number; decDeg: number } + /** A galaxy cluster, supercluster, void, or group anchor from the structure store. */ + | { kind: 'structure'; id: string }; diff --git a/src/@types/data/SourceEntryBase.d.ts b/src/@types/data/SourceEntryBase.d.ts index c36478f71..298c623ce 100644 --- a/src/@types/data/SourceEntryBase.d.ts +++ b/src/@types/data/SourceEntryBase.d.ts @@ -4,6 +4,8 @@ * this base and adds its own discriminator (`type: ''`) plus * kind-specific fields. */ +import type { CategoryLabelLayer } from '../animation/CategoryLabelLayer'; + export type SourceEntryBase = { /** Unique readable key — string twin of the numeric `Source` code (e.g. `'sdss'`, `'cluster'`). */ readonly id: string; @@ -11,7 +13,7 @@ export type SourceEntryBase = { readonly label: string; /** * True if the source covers (approximately) the full celestial sphere. - * For surveys this is the footprint flag; for POIs it's trivially true + * For surveys this is the footprint flag; for structures it's trivially true * (anchors are individual points, not survey patches), so the renderer's * coverage-mask logic stays well-behaved across both kinds. */ @@ -23,4 +25,44 @@ export type SourceEntryBase = { * off at runtime — this is purely the default. */ readonly visible: boolean; + /** + * True if this source carries toggleable on-screen text labels. + * Drives the label-visibility record and the fade-layer routing in the + * label subsystem. The two real label sets are: + * - galaxyNames layer: famousGalaxy + * - structure layer: cluster, supercluster, void, group + * All bulk surveys (sdss, glade, 2mrs, milliquas, synthetic) are false. + */ + readonly bearsLabel: boolean; + /** + * True if this source carries a ring/halo marker around its anchor point. + * Today this is exactly the structure category set (cluster, supercluster, + * void, group). famousGalaxy has no ring — only a name label — so the + * marker-visibility record excludes it entirely. + */ + readonly bearsMarker: boolean; + /** + * Which fade layer this source's labels live on. Present iff bearsLabel. + * - 'galaxyNames' — the shared galaxy-name layer (famousGalaxy rows) + * - 'structure' — the per-structure-category label layer + * Absent on non-label-bearing rows (bearsLabel === false). + */ + readonly labelLayer?: CategoryLabelLayer; + /** + * Long-form label for detail surfaces (e.g. 'Galaxy Cluster', 'Famous Galaxy'). + * Present iff bearsLabel. + */ + readonly detailLabel?: string; + /** + * Compact label for chips and previews (e.g. 'Cluster', 'Galaxy'). + * Present iff bearsLabel. The existing `label` field carries the shortest + * UI name ('Cluster', 'Famous') — shortLabel is the one step longer form + * the InfoCard chips use. + */ + readonly shortLabel?: string; + /** + * Plural label for list and toggle headers (e.g. 'Clusters', 'Famous Galaxies'). + * Present iff bearsLabel. + */ + readonly plural?: string; }; diff --git a/src/@types/data/StructureSourceEntry.d.ts b/src/@types/data/StructureSourceEntry.d.ts index 1a737f7c9..dcf774402 100644 --- a/src/@types/data/StructureSourceEntry.d.ts +++ b/src/@types/data/StructureSourceEntry.d.ts @@ -3,9 +3,9 @@ import type { SourceEntryBase } from './SourceEntryBase'; /** * Structure-typed SOURCE_REGISTRY row — the marker-ring codes (Cluster, * Supercluster, Void, Group). No `.bin`, bands, or depth, so it just adds - * `code` to the base. Named `'structure'`, not `'poi'`: famousGalaxy is a POI - * too but rides the `survey` entry, so this discriminator is exactly the - * marker-ring set. + * `code` to the base. The `'structure'` discriminator covers exactly the + * marker-ring set: famousGalaxy is also clickable but rides the `survey` + * entry, so it does not appear here. */ export type StructureSourceEntry = SourceEntryBase & { readonly type: 'structure'; diff --git a/src/@types/engine/CreateClickResolverInput.d.ts b/src/@types/engine/CreateClickResolverInput.d.ts index 29335c097..610423554 100644 --- a/src/@types/engine/CreateClickResolverInput.d.ts +++ b/src/@types/engine/CreateClickResolverInput.d.ts @@ -5,10 +5,10 @@ export type CreateClickResolverInput = { pickRenderer: PickRenderer; /** * Structure store projection for `pickToSelection` to resolve a ring - * hit's `(category, poiIndex)` to its record id. In production + * hit's `(category, structureIndex)` to its record id. In production * `wireInput` passes `state.data.structures`; tests stub a one-method * `{ byCategory }` object. An empty store resolves structure hits to - * null — no phantom POI card. + * null — no phantom selection. */ structures: PickStructureStore; }; diff --git a/src/@types/engine/EngineCallbacks.d.ts b/src/@types/engine/EngineCallbacks.d.ts index f7ffb6816..7f4650f30 100644 --- a/src/@types/engine/EngineCallbacks.d.ts +++ b/src/@types/engine/EngineCallbacks.d.ts @@ -13,7 +13,8 @@ import type { SourceType } from '../data/SourceType'; import type { BiasMode } from '../data/BiasMode'; import type { ToneMapCurve } from '../data/ToneMapCurve'; import type { LoadProgressState } from '../loading/LoadProgressState'; -import type { PoiCategory } from './data/PoiCategory'; +import type { LabelCategory } from './data/LabelCategory'; +import type { StructureCategory } from './data/StructureCategory'; import type { VolumeFieldRowData } from '../settings/VolumeFieldRowData'; /** @@ -81,7 +82,7 @@ export type EngineCallbacks = { /** * Selection-state callbacks. Carry the resolved `FocusableTarget` - * (galaxy or POI) so consumers don't need to branch on a separate + * (galaxy or structure) so consumers don't need to branch on a separate * id callback — they receive the full GalaxyInfo / StructureRecord * directly. Both required: every engine consumer needs hover / * select fan-out (InfoCard text, halo, hover preview). @@ -105,7 +106,7 @@ export type EngineCallbacks = { onAutoRotateChange?: (enabled: boolean) => void; /** * Fired when the camera-focus target changes — i.e. the engine has - * started a tween toward (or away from) a specific galaxy or POI. + * started a tween toward (or away from) a specific galaxy or structure. * * Selection (`onSelectChange`) and focus are separate concepts: * - Selection is the pin state — InfoCard, halo highlight. A bare @@ -118,8 +119,8 @@ export type EngineCallbacks = { * The deep-link URL hook subscribes to focus, not selection, so a * casual click doesn't pollute browser history with hash entries — * only deliberate focus actions do. The `FocusableTarget` union - * means one callback covers both galaxy and POI focus; consumers - * branch on the discriminant (use `isPoi`). + * means one callback covers both galaxy and structure focus; consumers + * branch on the discriminant (use `isStructure`). */ onFocusChange?: (target: FocusableTarget | null) => void; /** @@ -178,9 +179,9 @@ export type EngineCallbacks = { * `onLoadProgress` aggregates byte counts across in-flight slots; * `null` means "no fetches in flight" (the UI fades the bar out). * - * `onStructureCountsChange` reports the number of POIs published per - * marker category (cluster / supercluster / void) after each - * `rebuildAllPois` merge — the structure analogue of the per-survey + * `onStructureCountsChange` reports the number of structures published per + * marker category (cluster / supercluster / void) after each structure + * rebuild — the structure analogue of the per-survey * `onCatalogReady` count, so the Structures panel can show "Clusters * 573" the way the Surveys panel shows "SDSS 1,234,567". Fired with * the full snapshot (not a delta) because the three groups settle @@ -191,7 +192,7 @@ export type EngineCallbacks = { onTierChange?: (tier: Tier) => void; onCatalogReady?: (source: SourceType, count: number) => void; onLoadProgress?: (progress: LoadProgressState | null) => void; - onStructureCountsChange?: (counts: Partial>) => void; + onStructureCountsChange?: (counts: Partial>) => void; }; /** @@ -231,7 +232,7 @@ export type EngineCallbacks = { filaments?: { onReady?: (stripCount: number, vertexCount: number) => void }; /** - * Echoes for the two independent POI visibility axes + * Echoes for the two independent structure visibility axes * (label-text vs marker-glyph). The two records are deliberately * separate — flipping one does NOT fire the other. Label-text and * marker-glyph visibility are independent because a category's ring @@ -243,8 +244,12 @@ export type EngineCallbacks = { * `setCategoryMarkerVisible(...)` call. */ labels?: { - onLabelCategoryVisibilityChange?: (visibility: Readonly>) => void; - onMarkerCategoryVisibilityChange?: (visibility: Readonly>) => void; + onLabelCategoryVisibilityChange?: ( + visibility: Readonly>, + ) => void; + onMarkerCategoryVisibilityChange?: ( + visibility: Readonly>, + ) => void; }; /** diff --git a/src/@types/engine/FocusableTarget.d.ts b/src/@types/engine/FocusableTarget.d.ts index 554ef63da..4947bd801 100644 --- a/src/@types/engine/FocusableTarget.d.ts +++ b/src/@types/engine/FocusableTarget.d.ts @@ -3,11 +3,11 @@ import type { StructureRecord } from './data/StructureRecord'; /** * FocusableTarget — discriminated union of the two things the camera can - * focus on: a single galaxy point or a point-of-interest anchor (cluster, - * supercluster, void, famous-galaxy POI). + * focus on: a single galaxy point or a structure anchor (cluster, + * supercluster, void, famous-galaxy). * * Used by the public `camera.focusOn(target)` handle (which dispatches via - * the `isPoi` predicate in `services/engine/isPoi.ts`) and by InfoCard's + * the `isStructure` predicate in `services/engine/isStructure.ts`) and by InfoCard's * unified `hovered` / `selected` props. Deliberately distinct from * `FocusTarget` in `@types/camera/FocusTarget.d.ts`, which is the * URL-parsed deep-link descriptor (`{ kind: 'pgc' | 'objid' | 'famous', ...}`) diff --git a/src/@types/engine/GalaxyInfo.d.ts b/src/@types/engine/GalaxyInfo.d.ts index 017f44d01..a786dbc1c 100644 --- a/src/@types/engine/GalaxyInfo.d.ts +++ b/src/@types/engine/GalaxyInfo.d.ts @@ -277,7 +277,7 @@ export type GalaxyInfo = { /** * Curated human-friendly display name when one is set on the seed entry * (e.g. `"Andromeda Galaxy"` for M31). When present, both the InfoCard - * headline and the POI label prefer this over `names[0]`; the rest of + * headline and the structure label prefer this over `names[0]`; the rest of * `names` appears as "Also known as" aliases. Absent for most entries — * the seed only sets commonName for galaxies whose widely-recognised * name differs meaningfully from the catalog identifier. diff --git a/src/@types/engine/UrlSyncReturn.d.ts b/src/@types/engine/UrlSyncReturn.d.ts index b53b4504a..33dcb6178 100644 --- a/src/@types/engine/UrlSyncReturn.d.ts +++ b/src/@types/engine/UrlSyncReturn.d.ts @@ -9,5 +9,5 @@ import type { FocusTarget } from '../camera/FocusTarget'; */ export type UrlSyncReturn = { pendingTarget: FocusTarget | null; - pendingPoiId: string | null; + pendingStructureId: string | null; }; diff --git a/src/@types/engine/UseEngineInput.d.ts b/src/@types/engine/UseEngineInput.d.ts index 587c3d07a..843078955 100644 --- a/src/@types/engine/UseEngineInput.d.ts +++ b/src/@types/engine/UseEngineInput.d.ts @@ -7,7 +7,7 @@ import type { EngineCallbacks } from './EngineCallbacks'; * into a fully-populated `EngineCallbacks` object via spread (e.g. * `selection: { onHoverChange: setHovered, onSelectChange: setSelected, * ...extraSelection }`), so a caller that wants to subscribe to *just* - * one method in a required bag (like `selection.onPoiHoverChange`) + * one method in a required bag (like `selection.onStructureHoverChange`) * shouldn't be forced to also re-supply the required ones the hook * already wires up. */ @@ -23,7 +23,7 @@ export type UseEngineInput = { * practice this is the `engineCallbacks` slice from * `useEngineSettings` — settings echoes that drive React-side * SettingsPanel state — plus App-level subscriptions to specific - * methods like `selection.onPoiHoverChange` for the POI hover preview. + * methods like `selection.onStructureHoverChange` for the structure hover preview. * Captured at first render; do not expect subsequent changes to take * effect. */ diff --git a/src/@types/engine/UseEngineReturn.d.ts b/src/@types/engine/UseEngineReturn.d.ts index b6a2f845f..92ed9d3a5 100644 --- a/src/@types/engine/UseEngineReturn.d.ts +++ b/src/@types/engine/UseEngineReturn.d.ts @@ -5,7 +5,7 @@ import type { FocusableTarget } from './FocusableTarget'; import type { ScaleInfo } from './ScaleInfo'; import type { LoadProgressState } from '../loading/LoadProgressState'; import type { Tier } from '../data/Tier'; -import type { PoiCategory } from './data/PoiCategory'; +import type { StructureCategory } from './data/StructureCategory'; export type UseEngineReturn = { canvasRef: React.RefObject; @@ -17,8 +17,8 @@ export type UseEngineReturn = { scale: ScaleInfo; fps: number; sourceCounts: Partial>; - /** Per-marker-category POI counts (cluster / supercluster / void) for the Structures panel. */ - structureCounts: Partial>; + /** Per-category structure counts (cluster / supercluster / void / group) for the Structures panel. */ + structureCounts: Partial>; loadProgress: LoadProgressState | null; currentTier: Tier; }; diff --git a/src/@types/engine/UseKeyboardShortcutsInput.d.ts b/src/@types/engine/UseKeyboardShortcutsInput.d.ts index 79b766372..248b019f2 100644 --- a/src/@types/engine/UseKeyboardShortcutsInput.d.ts +++ b/src/@types/engine/UseKeyboardShortcutsInput.d.ts @@ -3,7 +3,7 @@ import type { EngineHandle } from './EngineHandle'; import type { FocusableTarget } from './FocusableTarget'; export type UseKeyboardShortcutsInput = { - /** The currently-pinned target (galaxy or POI). `f` is a no-op when null. */ + /** The currently-pinned target (galaxy or structure). `f` is a no-op when null. */ selected: FocusableTarget | null; /** Used to gate the `/` shortcut so the palette doesn't reopen on top of itself. */ paletteOpen: boolean; diff --git a/src/@types/engine/UseUrlSyncInput.d.ts b/src/@types/engine/UseUrlSyncInput.d.ts index 4095a75d8..34f5bb9e9 100644 --- a/src/@types/engine/UseUrlSyncInput.d.ts +++ b/src/@types/engine/UseUrlSyncInput.d.ts @@ -7,10 +7,9 @@ import type { FamousMetaEntry } from '../loading/FamousMetaEntry'; import type { StructureRecord } from './data/StructureRecord'; /** - * Combined input for `useUrlSync` — both galaxy-side and POI-side state + * Combined input for `useUrlSync` — both galaxy-side and structure-side state * the hook needs to keep `location.hash` in lock-step with engine - * selection. Merge of the two legacy inputs (`UseFocusUrlInput` + - * `UsePoiUrlSyncInput`) into one bag. + * selection. * * The reactive fields drive their respective drain effects' re-runs * as data lands; `engineHandleRef` is a mutable ref because the engine @@ -18,13 +17,13 @@ import type { StructureRecord } from './data/StructureRecord'; * not retrigger this hook on assignment. */ export type UseUrlSyncInput = { - /** Camera-focus target — galaxy or POI; drives the URL hash. */ + /** Camera-focus target — galaxy or structure; drives the URL hash. */ focused: FocusableTarget | null; status: EngineStatus; sourceCounts: Partial>; famousMeta: readonly FamousMetaEntry[]; aliasMap: ReadonlyMap; ready: boolean; - pois: readonly StructureRecord[]; + structures: readonly StructureRecord[]; engineHandleRef: RefObject; }; diff --git a/src/@types/engine/data/LabelCategory.d.ts b/src/@types/engine/data/LabelCategory.d.ts new file mode 100644 index 000000000..cddb1b01b --- /dev/null +++ b/src/@types/engine/data/LabelCategory.d.ts @@ -0,0 +1,3 @@ +import { LABEL_CATEGORIES } from '../../../data/labelCategories'; + +export type LabelCategory = (typeof LABEL_CATEGORIES)[number]; diff --git a/src/@types/engine/data/PickStructureStore.d.ts b/src/@types/engine/data/PickStructureStore.d.ts index 55ea54d58..bc4a4aa25 100644 --- a/src/@types/engine/data/PickStructureStore.d.ts +++ b/src/@types/engine/data/PickStructureStore.d.ts @@ -3,7 +3,7 @@ import type { StructureCategory } from './StructureCategory'; /** * Minimal projection of the structure store the pick path reads — just the - * per-category lookup `pickToSelection` / `resolvePoiFromPick` need. Narrower + * per-category lookup `pickToSelection` / `resolveStructureFromPick` need. Narrower * than the full `StructureStore` so tests stub a one-method object literal. */ export type PickStructureStore = { diff --git a/src/@types/engine/data/PoiCategory.d.ts b/src/@types/engine/data/PoiCategory.d.ts deleted file mode 100644 index 91c5fa102..000000000 --- a/src/@types/engine/data/PoiCategory.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * PoiCategory — the full set of point-of-interest categories the UI and - * selection layers speak: the three extended-structure categories plus - * `famousGalaxy`. - * - * Defined as `StructureCategory | 'famousGalaxy'` so the structure categories - * and this superset can't drift: adding a structure category in one place adds - * it here too. - * - * Used where the FOUR-category space is genuinely needed — the SettingsPanel - * label/marker visibility toggles, `EngineSettingsState`'s per-category - * visibility records, the label-style override target, and the ring pick - * decode (which can only ever yield a structure category, but shares the - * decode type). The structure-only layers (the store, its records, the - * marker producer) use the narrower `StructureCategory`. - */ - -import type { StructureCategory } from './StructureCategory'; - -export type PoiCategory = StructureCategory | 'famousGalaxy'; diff --git a/src/@types/engine/data/StructureGroupId.d.ts b/src/@types/engine/data/StructureGroupId.d.ts index 6f1fa482d..08edcbc55 100644 --- a/src/@types/engine/data/StructureGroupId.d.ts +++ b/src/@types/engine/data/StructureGroupId.d.ts @@ -2,11 +2,9 @@ * StructureGroupId — which slot a structure record occupies within the * structure store. * - * This is the structure-store rename of `PoiGroupId`'s `staticAnchors` / - * `clusterBulk` pair: `anchors` holds the hand-curated featured structures, - * `bulk` holds the catalog-derived remainder. `PoiGroupId`'s `famous` - * group is dropped here — famous galaxies are galaxy data, not structures, - * so they live outside the structure store entirely. + * `anchors` holds the hand-curated featured structures, + * `bulk` holds the catalog-derived remainder. Famous galaxies are galaxy + * data, not structures, so they live outside the structure store entirely. * * The store's `all()` concatenation order is `anchors` then `bulk`. That * order is load-bearing: it preserves the ring marker pick-index alignment diff --git a/src/@types/engine/frame/RenderFrameSettings.d.ts b/src/@types/engine/frame/RenderFrameSettings.d.ts index 80314d469..d03b5c4b2 100644 --- a/src/@types/engine/frame/RenderFrameSettings.d.ts +++ b/src/@types/engine/frame/RenderFrameSettings.d.ts @@ -20,7 +20,7 @@ export type RenderFrameSettings = { * Selected entity, or `null` when nothing is selected. Galaxy * variants are translated inside `pointSpritesPass` to the packed * u32 `(source << 27) | localIdx` (or the `0xFFFFFFFF` "no selection" - * sentinel) the shader's halo path expects. POI variants don't + * sentinel) the shader's halo path expects. Structure variants don't * drive the halo and are treated as "no galaxy selected" by the pass. */ selected: Selection | null; diff --git a/src/@types/engine/handles/EngineCameraHandle.d.ts b/src/@types/engine/handles/EngineCameraHandle.d.ts index 7800a004e..7caf2b62e 100644 --- a/src/@types/engine/handles/EngineCameraHandle.d.ts +++ b/src/@types/engine/handles/EngineCameraHandle.d.ts @@ -19,12 +19,12 @@ export type EngineCameraHandle = { * focus. Dispatches by type: * - GalaxyInfo → the galaxy focus path (commitFocus + onFocusChange). * - StructureRecord → the structure focus path (commitStructureFocus, - * framing distance derived from the category + onPoiFocusChange). + * framing distance derived from the category + onStructureFocusChange). * - * Discrimination uses the `isPoi` predicate from `services/engine/isPoi.ts`. + * Discrimination uses the `isStructure` predicate from `services/engine/isStructure.ts`. * See `services/engine/helpers/dispatchFocusOn.ts` for the dispatcher * implementation. Pre-bootstrap behaviour mirrors the per-kind paths: - * galaxy focus is a no-op when `state.cam` is null; POI focus still + * galaxy focus is a no-op when `state.cam` is null; structure focus still * fires the subsystem flag + React-side callback even with no camera * (deep-link drains that race bootstrap rely on that). */ diff --git a/src/@types/engine/handles/EngineGpuHandles.d.ts b/src/@types/engine/handles/EngineGpuHandles.d.ts index 2790bac78..b51c9651e 100644 --- a/src/@types/engine/handles/EngineGpuHandles.d.ts +++ b/src/@types/engine/handles/EngineGpuHandles.d.ts @@ -89,7 +89,7 @@ export type EngineGpuHandles = { focusBgl: FocusUniformsBgl | null; /** * The single shared cluster-focus uniform (buffer + bind group + packer). - * Only one POI is focused at a time, so one buffer serves the whole + * Only one structure is focused at a time, so one buffer serves the whole * engine: written once per frame in `renderFrame`, and its bind group — * built against `focusBgl` — is bound by every focus-aware pipeline at * its own group slot (a bind group is tied to a layout, not a group diff --git a/src/@types/engine/handles/EngineLabelsHandle.d.ts b/src/@types/engine/handles/EngineLabelsHandle.d.ts index 4275cac88..4c55c61a9 100644 --- a/src/@types/engine/handles/EngineLabelsHandle.d.ts +++ b/src/@types/engine/handles/EngineLabelsHandle.d.ts @@ -1,20 +1,21 @@ /** - * EngineLabelsHandle — public-handle sub-bag for the POI overlay + * EngineLabelsHandle — public-handle sub-bag for the structure overlay * (clusters / superclusters / voids / famous galaxies). * * Despite the name "labels", this handle exposes setters for BOTH the - * text-label axis AND the marker (ring + halo) axis of POI rendering. + * text-label axis AND the marker (ring + halo) axis of structure rendering. * The two axes are deliberately independent: a category's marker can be * hidden while its text label still renders, and vice versa. */ -import type { PoiCategory } from '../data/PoiCategory'; +import type { LabelCategory } from '../data/LabelCategory'; +import type { StructureCategory } from '../data/StructureCategory'; export type EngineLabelsHandle = { /** - * Show/hide the TEXT LABEL for every POI in the given category. - * Forwards to - * `state.subsystems.pois.setCategoryLabelVisible(category, visible)`. + * Show/hide the TEXT LABEL for every source in the given label + * category. Forwards to + * `state.subsystems.structures.setCategoryLabelVisible(category, visible)`. * Echoes back via `onLabelCategoryVisibilityChange` with the full * label-visibility record so the React shell can keep its checkboxes * in sync from one callback. @@ -22,18 +23,19 @@ export type EngineLabelsHandle = { * Marker (ring + halo) visibility for the same category is untouched * — use `setCategoryMarkerVisible` for that axis. */ - setCategoryLabelVisible(category: PoiCategory, visible: boolean): void; + setCategoryLabelVisible(category: LabelCategory, visible: boolean): void; /** - * Show/hide the MARKER (ring + halo) for every POI in the given - * category. Forwards to - * `state.subsystems.pois.setCategoryMarkerVisible(category, visible)`. + * Show/hide the MARKER (ring + halo) for every structure in the given + * category. Keyed by `StructureCategory` only: famous galaxies bear + * no ring marker. Forwards to + * `state.subsystems.structures.setCategoryMarkerVisible(category, visible)`. * Echoes back via `onMarkerCategoryVisibilityChange` with the full * marker-visibility record. * - * Today the Structures master toggle (Task #6) is the only intended - * consumer; there is currently no per-category marker UI. Label - * visibility for the same category is untouched — use - * `setCategoryLabelVisible` for that axis. + * Today the Structures master toggle is the only intended consumer; + * there is currently no per-category marker UI. Label visibility for + * the same category is untouched — use `setCategoryLabelVisible` for + * that axis. */ - setCategoryMarkerVisible(category: PoiCategory, visible: boolean): void; + setCategoryMarkerVisible(category: StructureCategory, visible: boolean): void; }; diff --git a/src/@types/engine/handles/EngineSelectionHandle.d.ts b/src/@types/engine/handles/EngineSelectionHandle.d.ts index 4f1e8981b..64f3f07ae 100644 --- a/src/@types/engine/handles/EngineSelectionHandle.d.ts +++ b/src/@types/engine/handles/EngineSelectionHandle.d.ts @@ -13,22 +13,21 @@ import type { PgcAliasMap } from '../../loading/PgcAliasMap'; */ export type EngineSelectionHandle = { /** - * Programmatically clear the current selection — galaxy AND POI in one + * Programmatically clear the current selection — galaxy AND structure in one * call. "Close the card" semantic: anywhere a user dismisses the * InfoCard (Esc, the × button, URL drift back to empty hash), both * sides collapse together in a single render frame. * * Order is deterministic: galaxy selection clears first - * (`onSelectChange` / `onFocusChange` fire), then POI selection - * (`onPoiFocusChange` fires). Idempotent: calling with neither - * selected fires only the POI teardown's no-op callback chain - * (preserves the pre-2026-05-19 `clearPoiFocus` semantic — no - * presence gate). + * (`onSelectChange` / `onFocusChange` fire), then structure selection + * (`onStructureFocusChange` fires). Idempotent: calling with neither + * selected fires only the structure teardown's no-op callback chain + * (no presence gate). * - * For code paths that need to clear ONLY the POI without disturbing + * For code paths that need to clear ONLY the structure without disturbing * a pinned galaxy, drop down to the engine internals. There's no * public narrow-clear method (no real consumer existed when this - * was unified on 2026-05-19; revisit if a use case appears). + * was unified; revisit if a use case appears). */ clear: () => void; /** Select (pin) the famous-atlas galaxy with the given id, then focus-tween. */ diff --git a/src/@types/engine/handles/EngineSubsystemHandles.d.ts b/src/@types/engine/handles/EngineSubsystemHandles.d.ts index d61df17f9..dc7984c1b 100644 --- a/src/@types/engine/handles/EngineSubsystemHandles.d.ts +++ b/src/@types/engine/handles/EngineSubsystemHandles.d.ts @@ -100,7 +100,7 @@ export type EngineSubsystemHandles = { * Label director — owns `labelRenderer.setLabels` / * `markerLineRenderer.setLines`, polls every registered `LabelProducer` * each frame, merges outputs, and flushes once. Lets multiple overlays - * (you-are-here pin, cluster POIs, future galaxy/void labels) coexist + * (you-are-here pin, cluster structures, future galaxy/void labels) coexist * without stomping each other's full-set replacements. * * Constructed eagerly; the two renderers are wired during @@ -112,7 +112,7 @@ export type EngineSubsystemHandles = { /** * Cluster focus-mode subsystem — drives the "dim non-members of the * selected cluster/SC/void" effect. Selection-driven: `runFrame` calls - * `update(selectedPoi, nowMs)` each frame and threads + * `update(selectedStructure, nowMs)` each frame and threads * `produceFocusUniforms(nowMs)` into the points draw. Constructed * eagerly; no GPU dep, non-null from t=0. */ diff --git a/src/@types/engine/state/FocusState.d.ts b/src/@types/engine/state/FocusState.d.ts index c65de412a..0dff66d2d 100644 --- a/src/@types/engine/state/FocusState.d.ts +++ b/src/@types/engine/state/FocusState.d.ts @@ -1,10 +1,10 @@ /** - * FocusState — selected-POI state for the cluster-viz focus mode. + * FocusState — focused-structure state for the structure focus mode. * * The focus subsystem (created in plan 4) owns one of these at a time: - * either the user has a POI selected and the field is fully populated, + * either the user has a structure focused and the field is fully populated, * or `active === false` and the rest of the fields hold whatever was - * last selected (so the uniform-write path doesn't have to special-case + * last focused (so the uniform-write path doesn't have to special-case * "no selection" — the shader reads `active === false` and skips the * member alpha-multiplier branch). * @@ -14,7 +14,7 @@ * in the subsystem or a per-frame conditional bind-group rebind. * Carrying `active: boolean` in-band keeps the shader and CPU * paths uniform. - * - The `memberPackedIds` array is reused across same-POI re-focus + * - The `memberPackedIds` array is reused across same-structure re-focus * events; nullifying it on deactivation would force a recomputation * on every reactivation. Same field, `active = false`, no recompute. * @@ -24,22 +24,23 @@ */ import type { Vec3 } from '../../math/Vec3'; +import type { StructureCategory } from '../data/StructureCategory'; export type FocusState = { /** - * Stable POI identifier (matches `StructureRecord.id`). Used to key - * the membership cache (`(poiId, dataRev) → packedIds`) and to wire + * Stable structure identifier (matches `StructureRecord.id`). Used to key + * the membership cache (`(structureId, dataRev) → packedIds`) and to wire * the URL hash echo. */ - readonly poiId: string; + readonly structureId: string; /** - * POI category — drives the camera framing multiplier (plan 3 §5.3) - * and the InfoCard layout. All three categories share one fade rule + * Structure category — drives the camera framing multiplier (plan 3 §5.3) + * and the InfoCard layout. Every category shares one fade rule * (interior galaxies stay bright), so the shader needs no per-category * bit. */ - readonly category: 'cluster' | 'supercluster' | 'void'; + readonly category: StructureCategory; /** * Packed-identity members from `structureMembership(...)`. CPU-side @@ -50,14 +51,14 @@ export type FocusState = { readonly memberPackedIds: readonly number[]; /** - * World-space center of the POI (Mpc). Mirrors + * World-space center of the structure (Mpc). Mirrors * `StructureRecord.worldPos`. Carried separately so the focus - * uniform write doesn't have to re-resolve the POI by id every frame. + * uniform write doesn't have to re-resolve the structure by id every frame. */ readonly center: Vec3; /** - * Physical radius of the structure in Mpc — same value the POI's + * Physical radius of the structure in Mpc — same value the structure's * marker ring is drawn at, and the cone-search radius that produced * `memberPackedIds`. */ diff --git a/src/@types/engine/subsystems/Selection.d.ts b/src/@types/engine/subsystems/Selection.d.ts index 82aa7df7e..8247a7e7c 100644 --- a/src/@types/engine/subsystems/Selection.d.ts +++ b/src/@types/engine/subsystems/Selection.d.ts @@ -14,7 +14,7 @@ import type { SourceType } from '../../data/SourceType'; * The structure variant is structure-only by construction: famous * galaxies are picked through the galaxy point path (so they arrive as * `kind: 'galaxy'`), and the ring pick decode never yields a famous - * category. The wider `PoiCategory` superset (which does include + * category. The wider `LabelCategory` superset (which does include * `famousGalaxy`) belongs to the settings / label layers, not here. * * The two variants share one slot inside `selectionSubsystem` because diff --git a/src/@types/engine/subsystems/SelectionSubsystem.d.ts b/src/@types/engine/subsystems/SelectionSubsystem.d.ts index 88aea56ab..70e933f9e 100644 --- a/src/@types/engine/subsystems/SelectionSubsystem.d.ts +++ b/src/@types/engine/subsystems/SelectionSubsystem.d.ts @@ -28,7 +28,7 @@ export type SelectionSubsystem = { * actual change. Optional `prebuiltInfo` lets callers (e.g. * `selectByAlias`) pass the GalaxyInfo directly when the GPU upload * hasn't settled yet (the catalog is in `state.sources.catalogs` but the - * renderer hasn't received it). Ignored for POI selections. + * renderer hasn't received it). Ignored for structure selections. */ setSelected(sel: Selection | null, prebuiltInfo?: GalaxyInfo | null): void; /** @@ -36,7 +36,7 @@ export type SelectionSubsystem = { * actual change. Symmetric with `setSelected`: the cluster-focus * fade reads `focused()` in `runFrame`, React mirrors the same * target into the URL hash. Optional `prebuiltInfo` short-circuits - * the cloud lookup for the `selectByAlias` race; ignored for POI + * the cloud lookup for the `selectByAlias` race; ignored for structure * focuses. */ setFocused(sel: Selection | null, prebuiltInfo?: GalaxyInfo | null): void; diff --git a/src/@types/engine/subsystems/StructureFocusSubsystem.d.ts b/src/@types/engine/subsystems/StructureFocusSubsystem.d.ts index 312e801bb..2da2e621d 100644 --- a/src/@types/engine/subsystems/StructureFocusSubsystem.d.ts +++ b/src/@types/engine/subsystems/StructureFocusSubsystem.d.ts @@ -4,7 +4,7 @@ import type { Destroyable } from '../../rendering/Destroyable'; /** * StructureFocusSubsystem — owns cluster "focus mode": when a cluster / - * supercluster / void POI is focused, non-member galaxies fade to ~8% + * supercluster / void structure is focused, non-member galaxies fade to ~8% * alpha over ~400 ms so the structure's membership pops out. All three * categories behave identically (the focused structure's interior * galaxies stay bright; voids are just an underdense case). @@ -12,10 +12,10 @@ import type { Destroyable } from '../../rendering/Destroyable'; * ### Focus-driven, not imperative * * The selection subsystem's `focused()` slot is the single source of - * truth ("a cluster POI is focused" *is* "focus active"). So instead of + * truth ("a cluster structure is focused" *is* "focus active"). So instead of * scattering focusOn / clearFocus calls across every focus-mutating - * site, this subsystem exposes one per-frame `update(focusedPoi, now)` - * that diffs the focused POI's id against the currently-active id and + * site, this subsystem exposes one per-frame `update(focusedStructure, now)` + * that diffs the focused structure's id against the currently-active id and * drives the fade. No call sites to keep in sync. * * ### GPU re-derives membership @@ -30,19 +30,19 @@ export type StructureFocusSubsystem = { readonly id: 'structureFocus'; /** - * Per-frame state sync. Diffs `focusedPoi?.id` against the currently + * Per-frame state sync. Diffs `focusedStructure?.id` against the currently * active id: - * - changed to a focus-eligible POI (cluster | supercluster | void): + * - changed to a focus-eligible structure (cluster | supercluster | void): * latch center/radius, fade toward 1 over 400 ms. - * - changed to null OR a non-eligible POI (famousGalaxy): fade toward + * - changed to null OR a non-eligible structure (famousGalaxy): fade toward * 0, keeping the last center/radius until blend settles at 0. * - unchanged id: no-op (no re-fade). */ - update(focusedPoi: StructureRecord | null, nowMs: number): void; + update(focusedStructure: StructureRecord | null, nowMs: number): void; /** * Pure read: ticks the fade and returns the live uniform value. At - * rest (no POI focused) returns an all-zero value (blend=0). + * rest (no structure focused) returns an all-zero value (blend=0). */ produceFocusUniforms(nowMs: number): FocusUniformsValue; diff --git a/src/@types/engine/wiring/SettingsCallbackSeed.d.ts b/src/@types/engine/wiring/SettingsCallbackSeed.d.ts index 011913a6f..cc9efec98 100644 --- a/src/@types/engine/wiring/SettingsCallbackSeed.d.ts +++ b/src/@types/engine/wiring/SettingsCallbackSeed.d.ts @@ -13,7 +13,8 @@ import type { BiasMode } from '../../data/BiasMode'; import type { ToneMapCurve } from '../../data/ToneMapCurve'; -import type { PoiCategory } from '../data/PoiCategory'; +import type { LabelCategory } from '../data/LabelCategory'; +import type { StructureCategory } from '../data/StructureCategory'; export type SettingsCallbackSeed = { pointSize: number; @@ -31,15 +32,17 @@ export type SettingsCallbackSeed = { exposure: number; visibleSourceMask: number; /** - * Initial per-category POI label visibility — fired through + * Initial per-category label visibility — fired through * `cb.labels?.onLabelCategoryVisibilityChange?.(...)` so the React * shell seeds its label checkboxes from engine truth on startup. */ - labelCategoryVisibility: Readonly>; + labelCategoryVisibility: Readonly>; /** - * Initial per-category POI MARKER visibility — fired through - * `cb.labels?.onMarkerCategoryVisibilityChange?.(...)`. Independent - * axis from the label record; defaults to every category visible. + * Initial per-category MARKER visibility — fired through + * `cb.labels?.onMarkerCategoryVisibilityChange?.(...)`. Keyed by + * `StructureCategory` only (no ring marker for famous galaxies); + * independent axis from the label record; defaults to every category + * visible. */ - markerCategoryVisibility: Readonly>; + markerCategoryVisibility: Readonly>; }; diff --git a/src/@types/loading/AssetKey.d.ts b/src/@types/loading/AssetKey.d.ts index d0b876c9e..f5a8dfaaa 100644 --- a/src/@types/loading/AssetKey.d.ts +++ b/src/@types/loading/AssetKey.d.ts @@ -8,7 +8,7 @@ import type { SourceType } from '../data/SourceType'; * single `Source`: * * - `'structureCatalog'` — the `.ccat` seed shared by Cluster, Supercluster, - * and Void POIs. All three `Source` codes pull their geometry from one file, + * and Void structures. All three `Source` codes pull their geometry from one file, * so a per-source fetch key would be wrong: there is no `structureCatalog` * `Source`, and a single fetch must not trigger three loads. * diff --git a/src/@types/loading/FamousMetaEntry.d.ts b/src/@types/loading/FamousMetaEntry.d.ts index 1662fb176..0df5c16fd 100644 --- a/src/@types/loading/FamousMetaEntry.d.ts +++ b/src/@types/loading/FamousMetaEntry.d.ts @@ -7,7 +7,7 @@ export type FamousMetaEntry = { /** * Curated human-friendly display name (e.g. `"Andromeda Galaxy"`). * Mirrors the optional field on the seed entry. Absent for most - * entries — the POI label producer falls back to `names`/`id`. + * entries — the famous-label producer falls back to `names`/`id`. */ commonName?: string; description: string; diff --git a/src/@types/loading/StructureCatalogPayload.d.ts b/src/@types/loading/StructureCatalogPayload.d.ts index 197020011..d8885ce03 100644 --- a/src/@types/loading/StructureCatalogPayload.d.ts +++ b/src/@types/loading/StructureCatalogPayload.d.ts @@ -20,7 +20,7 @@ export type StructureMetaEntry = { names: string[]; /** Normalized Abell/ACO designation (e.g. 'A2670'), or null. */ abell: string | null; - /** One-liner shown in the POI info panel. */ + /** One-liner shown in the structure info panel. */ description: string; }; diff --git a/src/@types/rendering/FocusUniformsValue.d.ts b/src/@types/rendering/FocusUniformsValue.d.ts index 5d854f218..620b330cc 100644 --- a/src/@types/rendering/FocusUniformsValue.d.ts +++ b/src/@types/rendering/FocusUniformsValue.d.ts @@ -6,16 +6,16 @@ import type { Vec3 } from '../math/Vec3'; * byte layout). Produced each frame by structureFocusSubsystem and packed * into the points pipeline's singleton focus buffer. * - * At rest (no POI focused) every field is zero — `blend: 0` makes the + * At rest (no structure focused) every field is zero — `blend: 0` makes the * shader's per-vertex multiplier collapse to 1.0, so center/radius are * don't-cares. */ export type FocusUniformsValue = { - /** POI world-space centre in Mpc. `Readonly` because Vec3 is mutable. */ + /** Focused structure world-space centre in Mpc. `Readonly` because Vec3 is mutable. */ readonly center: Readonly; /** * Apparent (outer) radius in Mpc = `apparentRadiusMpc ?? physicalRadiusMpc` - * on the POI. The smoothstep edge where the fade reaches full recede (0.08). + * on the focused structure. The smoothstep edge where the fade reaches full recede (0.08). */ readonly apparentRadiusMpc: number; /** diff --git a/src/@types/rendering/InstancedQuadConfig.d.ts b/src/@types/rendering/InstancedQuadConfig.d.ts index 25ba63323..d932bedaf 100644 --- a/src/@types/rendering/InstancedQuadConfig.d.ts +++ b/src/@types/rendering/InstancedQuadConfig.d.ts @@ -33,7 +33,7 @@ export type InstancedQuadConfig = { * HDR offscreen `'rgba16float'`. */ format: GPUTextureFormat; /** Canonical cluster-focus bind-group layout, bound at `@group(1)`. - * The focus dim (non-members of a focused POI fade to 8%) is computed + * The focus dim (non-members of a focused structure fade to 8%) is computed * per instance in each consumer's vertex stage via * `focusAlphaMultiplier`; the same shared layout serves every impostor * pipeline so the points pass and the disks fade in lockstep. */ diff --git a/src/@types/rendering/Label.d.ts b/src/@types/rendering/Label.d.ts index 8765d9f51..e6c7bd00b 100644 --- a/src/@types/rendering/Label.d.ts +++ b/src/@types/rendering/Label.d.ts @@ -101,7 +101,7 @@ export type Label = { /** * Vertical alignment of the text relative to `worldPos`. * Default 'baseline' (anchor sits on the text baseline; descenders - * hang below). POI rings use 'center' so the label visually + * hang below). Structure rings use 'center' so the label visually * straddles the ring centre rather than hanging beneath it. */ readonly alignY?: LabelAlignY; diff --git a/src/@types/rendering/LabelAlignY.d.ts b/src/@types/rendering/LabelAlignY.d.ts index 78e9c6094..016ad15ec 100644 --- a/src/@types/rendering/LabelAlignY.d.ts +++ b/src/@types/rendering/LabelAlignY.d.ts @@ -7,7 +7,7 @@ * the typographic convention every Y offset * in the atlas is measured against. * `'center'` — visual centre of the glyph bounding box - * sits on the anchor. Used by POI labels + * sits on the anchor. Used by structure labels * (cluster / supercluster / void) that * anchor on a ring centre and want the * label text symmetrically straddled diff --git a/src/@types/rendering/StructureMarkerRenderer.d.ts b/src/@types/rendering/StructureMarkerRenderer.d.ts index 0fe53182f..0294e5645 100644 --- a/src/@types/rendering/StructureMarkerRenderer.d.ts +++ b/src/@types/rendering/StructureMarkerRenderer.d.ts @@ -6,7 +6,7 @@ * (cluster / supercluster / void / group). Per-category source-code * differentiation happens inside the renderer (one pre-built per-source * bind group each) so the pick path gets the correct - * (sourceCode << 27) | poiIndex packing for free. + * (sourceCode << 27) | structureIndex packing for free. */ import type { StructureMarkerDescriptor } from './StructureMarkerDescriptor'; diff --git a/src/@types/settings/EngineSettingsState.d.ts b/src/@types/settings/EngineSettingsState.d.ts index 966feb4de..d7d472ba5 100644 --- a/src/@types/settings/EngineSettingsState.d.ts +++ b/src/@types/settings/EngineSettingsState.d.ts @@ -52,7 +52,8 @@ import type { BiasMode } from '../data/BiasMode'; import type { ToneMapCurve } from '../data/ToneMapCurve'; -import type { PoiCategory } from '../engine/data/PoiCategory'; +import type { LabelCategory } from '../engine/data/LabelCategory'; +import type { StructureCategory } from '../engine/data/StructureCategory'; import type { FlowSettings } from './FlowSettings'; import type { VolumeFieldId } from '../data/VolumeFieldId'; import type { VolumeFieldSettings } from './VolumeFieldSettings'; @@ -177,23 +178,24 @@ export type EngineSettingsState = { }; /** - * Per-category visibility for the POI TEXT LABEL overlay. Keyed by - * the canonical `PoiCategory` union (`@types/engine/data/PoiCategory`). - * Defaults to every category visible. + * Per-category visibility for the TEXT LABEL overlay. Keyed by + * `LabelCategory` — the label-bearing sources (`famousGalaxy` plus the + * structure categories). Defaults to every category visible. * * This is one of two orthogonal records — see `markerCategoryVisibility` * for the marker (ring + halo) counterpart. Label-text and marker * visibility are independent so a category's ring can be hidden while * its label still renders, and vice versa. */ - labelCategoryVisibility: Record; + labelCategoryVisibility: Record; /** - * Per-category visibility for the POI MARKER overlay — the ring + - * halo glyph drawn at the POI's world anchor by - * `structureMarkerRenderer`. Symmetric to `labelCategoryVisibility`; - * the two records are deliberately independent so the SettingsPanel - * can offer separate master toggles for "Labels" (text) and - * "Structures" (markers). Defaults to every category visible. + * Per-category visibility for the MARKER overlay — the ring + halo glyph + * drawn at the structure's world anchor by `structureMarkerRenderer`. + * Keyed by `StructureCategory` only: famous galaxies bear no ring marker. + * Symmetric to `labelCategoryVisibility`; the two records are deliberately + * independent so the SettingsPanel can offer separate master toggles for + * "Labels" (text) and "Structures" (markers). Defaults to every category + * visible. */ - markerCategoryVisibility: Record; + markerCategoryVisibility: Record; }; diff --git a/src/@types/settings/UseEngineSettingsState.d.ts b/src/@types/settings/UseEngineSettingsState.d.ts index c6c84603b..eef622ff0 100644 --- a/src/@types/settings/UseEngineSettingsState.d.ts +++ b/src/@types/settings/UseEngineSettingsState.d.ts @@ -30,7 +30,8 @@ import type { BiasMode } from '../data/BiasMode'; import type { ToneMapCurve } from '../data/ToneMapCurve'; import type { FlowSettings } from './FlowSettings'; import type { VolumeFieldRowData } from './VolumeFieldRowData'; -import type { PoiCategory } from '../engine/data/PoiCategory'; +import type { LabelCategory } from '../engine/data/LabelCategory'; +import type { StructureCategory } from '../engine/data/StructureCategory'; export type UseEngineSettingsState = { pointSize: number; @@ -68,25 +69,24 @@ export type UseEngineSettingsState = { */ volumeFields: ReadonlyArray; /** - * Per-category visibility for the POI TEXT LABEL overlay. Mirrors - * the engine-side `EngineSettingsState.labelCategoryVisibility`; the + * Per-category visibility for the TEXT LABEL overlay. Mirrors the + * engine-side `EngineSettingsState.labelCategoryVisibility`; the * SettingsPanel reads from this record to render the per-category * label checkboxes. Engine echoes the whole record on every * `handle.labels.setCategoryLabelVisible(cat, visible)` call so the * UI stays in sync from a single subscription. */ - labelCategoryVisibility: Record; + labelCategoryVisibility: Record; /** - * Per-category visibility for the POI MARKER overlay (ring + halo). - * Mirrors the engine-side + * Per-category visibility for the MARKER overlay (ring + halo), keyed + * by `StructureCategory` only. Mirrors the engine-side * `EngineSettingsState.markerCategoryVisibility`. Today there is no * per-category marker UI — every entry stays `true` unless the - * Structures master toggle (Task #6 of the 2026-05-19 audit) flips - * them as a batch. Kept in state regardless so the React shell can - * present a snapshot and so the upcoming Structures toggle has a - * stable mirror to subscribe to. + * Structures master toggle flips them as a batch. Kept in state + * regardless so the React shell can present a snapshot and so the + * Structures toggle has a stable mirror to subscribe to. */ - markerCategoryVisibility: Record; + markerCategoryVisibility: Record; /** * Whether a 3Dconnexion SpaceMouse is currently paired and feeding * input reports. Engine echoes this through diff --git a/src/components/App/App.tsx b/src/components/App/App.tsx index aeacfbe17..4064ebabb 100644 --- a/src/components/App/App.tsx +++ b/src/components/App/App.tsx @@ -118,7 +118,7 @@ export function App(): React.ReactElement { // Live "N galaxies" figure for a pinned cluster/SC/void card. Recomputes // on selection / tier swap / catalog landing (`sourceCounts`) / survey - // toggle — null for galaxy selections and famous-galaxy POIs. + // toggle — null for galaxy selections and famous-galaxy structures. const selectedMemberCount = useStructureMemberCount({ selected, engineHandleRef: handleRef, @@ -175,7 +175,7 @@ export function App(): React.ReactElement { // Static structure table for the URL drain. The engine owns the merged // list (static anchors + the async bulk cluster catalog), but threading // that as a reactive React slice would re-render App on every catalog load. - // Deep-link arrivals only need the static subset (`#poi=cluster-…` / + // Deep-link arrivals only need the static subset (`#focus=cluster-…` / // `supercluster-…` / `void-…`). `useMemo([])` so the drain effect // doesn't re-fire on every render. const staticStructures = useMemo(() => buildStaticAnchorStructures(), []); @@ -186,7 +186,7 @@ export function App(): React.ReactElement { famousMeta, aliasMap, ready: status.kind === 'ready', - pois: staticStructures, + structures: staticStructures, engineHandleRef: handleRef, }); diff --git a/src/components/DebugPanel/LabelEffectsSection.tsx b/src/components/DebugPanel/LabelEffectsSection.tsx index b268f7292..f7a0f84a6 100644 --- a/src/components/DebugPanel/LabelEffectsSection.tsx +++ b/src/components/DebugPanel/LabelEffectsSection.tsx @@ -2,7 +2,7 @@ * LabelEffectsSection — live-tuning controls for the label outline. * * Pick a target category, tune outline colour + width, then bake the - * values into `POI_STYLES.` or `youAreHereSubsystem.ts`. The + * values into `STRUCTURE_MARKER_STYLES.` or `youAreHereSubsystem.ts`. The * override is a temporary hook, not a storage location. * * `setLabelStyleOverride` runs in `useEffect`, not during render — @@ -15,16 +15,13 @@ import { clearLabelStyleOverride, type LabelStyleOverrideTarget, } from '../../services/engine/labelStyleOverride'; +import { LABEL_CATEGORIES } from '../../data/labelCategories'; import type { Vec4 } from '../../@types/math/Vec4'; -const CATEGORIES: readonly LabelStyleOverrideTarget[] = [ - 'youAreHere', - 'cluster', - 'supercluster', - 'famousGalaxy', - 'void', - 'group', -]; +// `LabelStyleOverrideTarget` is `'youAreHere' | LabelCategory`, so the dropdown +// is exactly the registry's label-bearing categories plus the you-are-here pin. +// 'youAreHere' leads so it stays first in the dropdown. +const CATEGORIES: readonly LabelStyleOverrideTarget[] = ['youAreHere', ...LABEL_CATEGORIES]; function hexToRgb(hex: string): [number, number, number] { const m = /^#?([0-9a-f]{6})$/i.exec(hex); diff --git a/src/components/InfoCard/CompactStructureCard.tsx b/src/components/InfoCard/CompactStructureCard.tsx index 6d51b3110..3a8c2f3cd 100644 --- a/src/components/InfoCard/CompactStructureCard.tsx +++ b/src/components/InfoCard/CompactStructureCard.tsx @@ -7,7 +7,7 @@ import type { ReactNode } from 'react'; import type { StructureRecord } from '../../@types/engine/data/StructureRecord'; import { formatDistance } from '../../utils/format/distance'; -import { POI_CATEGORY_INFO } from '../../data/poiCategoryInfo'; +import { CATEGORY_DISPLAY_INFO } from '../../data/categoryDisplayInfo'; import styles from './CompactStructureCard.module.css'; export type CompactStructureCardProps = { @@ -28,7 +28,9 @@ export function CompactStructureCard({ structure }: CompactStructureCardProps):
{structure.name}
- {POI_CATEGORY_INFO[structure.category].shortLabel} + + {CATEGORY_DISPLAY_INFO[structure.category].shortLabel} +
{formatDistance(distanceMpc)} diff --git a/src/components/InfoCard/InfoCard.tsx b/src/components/InfoCard/InfoCard.tsx index 858f49a54..4ab13c616 100644 --- a/src/components/InfoCard/InfoCard.tsx +++ b/src/components/InfoCard/InfoCard.tsx @@ -1,5 +1,5 @@ /** - * InfoCard — routes hovered/selected galaxy + POI state into the detail and + * InfoCard — routes hovered/selected galaxy + structure state into the detail and * preview cards. Renders nothing when both slots are null. * * Always renders the same outer wrapper across all states. An earlier version @@ -8,9 +8,9 @@ * which lost the native `
` "More details" open state on every hover. * * Both `hovered` and `selected` accept the full `FocusableTarget` union - * (`GalaxyInfo | StructureRecord`). App.tsx merges POI and galaxy state - * before handing them here — POI wins when both are present. InfoCard then - * dispatches via `isPoi` into typed sub-slots and picks the right detail-card + * (`GalaxyInfo | StructureRecord`). App.tsx merges structure and galaxy state + * before handing them here — structure wins when both are present. InfoCard then + * dispatches via `isStructure` into typed sub-slots and picks the right detail-card * variant (`GalaxyDetailCard` vs `StructureDetailCard`). */ @@ -18,7 +18,7 @@ import type { ReactNode } from 'react'; import cx from 'classnames'; import type { GalaxyInfo } from '../../@types/engine/GalaxyInfo'; import type { FocusableTarget } from '../../@types/engine/FocusableTarget'; -import { isPoi } from '../../services/engine/isPoi'; +import { isStructure } from '../../services/engine/isStructure'; import { GalaxyDetailCard } from './GalaxyDetailCard'; import { StructureDetailCard } from './StructureDetailCard'; import { CompactCard } from './CompactCard'; @@ -29,14 +29,14 @@ export type InfoCardProps = { /** * The point currently under the cursor, or null when the cursor is on empty * sky. Can be either a galaxy or a structure — InfoCard dispatches via - * `isPoi` to render the appropriate hover variant. + * `isStructure` to render the appropriate hover variant. */ hovered: FocusableTarget | null; /** * The pinned/selected target, or null when nothing is pinned. Same dispatch * as `hovered`. When both `hovered` and `selected` are non-null and of the * same kind (galaxy/galaxy or structure/structure), the stacked-pair layout applies; - * when they're different kinds (e.g. galaxy pinned, POI hovered), both render + * when they're different kinds (e.g. galaxy pinned, structure hovered), both render * in their respective slots. */ selected: FocusableTarget | null; @@ -49,14 +49,14 @@ export type InfoCardProps = { selectedMemberCount?: number | null; /** * Optional callback fired when the user clicks "Focus" (galaxy) or "Fly here" - * (POI) on the pinned card. Caller routes to the unified handle method + * (structure) on the pinned card. Caller routes to the unified handle method * `handle.camera.focusOn(target)`. */ onFocus?: (target: FocusableTarget) => void; /** * Optional callback fired when the user clicks the Close (×) button on the * pinned card. Same effect as pressing Esc — clears the selection. Caller - * routes to `handle.selection.clear()` which tears down both galaxy AND POI + * routes to `handle.selection.clear()` which tears down both galaxy AND structure * selection in one call. */ onClose?: () => void; @@ -71,13 +71,13 @@ export function InfoCard({ }: InfoCardProps): ReactNode { if (!hovered && !selected) return null; - // Dispatch via isPoi into typed sub-slots. A StructureRecord is identified + // Dispatch via isStructure into typed sub-slots. A StructureRecord is identified // by a top-level `category` field; GalaxyInfo carries category only at - // `galaxyType.category`. See isPoi.ts for the discriminant rationale. - const selectedStructure = selected && isPoi(selected) ? selected : null; - const selectedGalaxy = selected && !isPoi(selected) ? (selected as GalaxyInfo) : null; - const hoveredStructure = hovered && isPoi(hovered) ? hovered : null; - const hoveredGalaxy = hovered && !isPoi(hovered) ? (hovered as GalaxyInfo) : null; + // `galaxyType.category`. See isStructure.ts for the discriminant rationale. + const selectedStructure = selected && isStructure(selected) ? selected : null; + const selectedGalaxy = selected && !isStructure(selected) ? (selected as GalaxyInfo) : null; + const hoveredStructure = hovered && isStructure(hovered) ? hovered : null; + const hoveredGalaxy = hovered && !isStructure(hovered) ? (hovered as GalaxyInfo) : null; // Structure hover wins over galaxy hover when both are non-null (a transient // cross-render race; the engine's hover throttler normally clears the diff --git a/src/components/InfoCard/StructureDetailCard.tsx b/src/components/InfoCard/StructureDetailCard.tsx index e983b287a..b1415c5be 100644 --- a/src/components/InfoCard/StructureDetailCard.tsx +++ b/src/components/InfoCard/StructureDetailCard.tsx @@ -8,7 +8,7 @@ import type { ReactNode } from 'react'; import type { StructureRecord } from '../../@types/engine/data/StructureRecord'; import { formatDistance } from '../../utils/format/distance'; import { formatAbellDesignation } from '../../utils/format/formatAbellDesignation'; -import { POI_CATEGORY_INFO } from '../../data/poiCategoryInfo'; +import { CATEGORY_DISPLAY_INFO } from '../../data/categoryDisplayInfo'; import { CardHeader } from './CardHeader'; import { CardRow } from './CardRow'; import { DescriptionBlock } from './DescriptionBlock'; @@ -55,7 +55,9 @@ export function StructureDetailCard({
{structure.name}
- {POI_CATEGORY_INFO[structure.category].label} + + {CATEGORY_DISPLAY_INFO[structure.category].label} +
diff --git a/src/components/SettingsPanel/SettingsPanel.tsx b/src/components/SettingsPanel/SettingsPanel.tsx index b19a9f531..8de5b2ade 100644 --- a/src/components/SettingsPanel/SettingsPanel.tsx +++ b/src/components/SettingsPanel/SettingsPanel.tsx @@ -87,8 +87,10 @@ import { BiasMode } from '../../data/biasMode'; import type { BiasMode as BiasModeT } from '../../@types/data/BiasMode'; import { ALL_TONE_MAP_CURVES, toneMapCurveLabel } from '../../data/toneMapCurve'; import type { ToneMapCurve as ToneMapCurveT } from '../../@types/data/ToneMapCurve'; -import type { PoiCategory } from '../../@types/engine/data/PoiCategory'; -import { POI_CATEGORY_INFO } from '../../data/poiCategoryInfo'; +import type { LabelCategory } from '../../@types/engine/data/LabelCategory'; +import type { StructureCategory } from '../../@types/engine/data/StructureCategory'; +import { CATEGORY_DISPLAY_INFO } from '../../data/categoryDisplayInfo'; +import { LABEL_CATEGORIES } from '../../data/labelCategories'; import { STRUCTURE_CATEGORIES } from '../../data/structureCategories'; import type { ScalarFieldPaletteId } from '../../@types/data/ScalarFieldPaletteId'; import type { VolumeFieldRowData } from '../../@types/settings/VolumeFieldRowData'; @@ -125,21 +127,6 @@ const TOGGLEABLE_SOURCES: readonly SourceType[] = [ Source.Milliquas, ]; -/** - * The label categories the "Labels" master toggle batches over. All five - * PoiCategory values — labels are independent of marker visibility (axis - * separation landed in PR #160 / audit Q11). - * - * `famousGalaxy` is listed first — it's the label set the explorer is - * most likely to recognise + most likely to toggle (named galaxies vs. - * astronomer-jargon structure labels), so it gets the top row. The - * remaining four are spread from the canonical `STRUCTURE_CATEGORIES` so a - * future fifth structure category flows through here automatically — the - * structures master (which iterates `STRUCTURE_CATEGORIES` directly) and - * the labels master can never drift apart. - */ -const LABEL_CATEGORIES: readonly PoiCategory[] = ['famousGalaxy', ...STRUCTURE_CATEGORIES]; - /** * High-level Style picker options for the Cosmic web group. Derived from * (volumes master, filaments master) at render time — see the picker's @@ -178,13 +165,13 @@ type Props = { sourceCounts?: Partial>; /** - * Per-marker-category POI counts (cluster / supercluster / void) for + * Per-marker-category structure counts (cluster / supercluster / void) for * the Structures section, shown beside each toggle the same way * `sourceCounts` annotates the Surveys rows. A category absent from * the map (or the whole prop undefined, before the bulk `.ccat` * lands) renders the toggle without a count rather than "0". */ - structureCounts?: Partial>; + structureCounts?: Partial>; /** Current point size in pixels. Lives under Galaxies → Advanced. */ pointSize: number; @@ -249,13 +236,13 @@ type Props = { * toggle. Wires to `handle.labels.setCategoryMarkerVisible` (added by * PR #160). */ - markerCategoryVisibility?: Readonly>; - onSetMarkerCategoryVisibility?: (category: PoiCategory, visible: boolean) => void; + markerCategoryVisibility?: Readonly>; + onSetMarkerCategoryVisibility?: (category: StructureCategory, visible: boolean) => void; // ── Labels group (ALL text annotations) ──────────────────────────────── /** Per-category LABEL visibility — independent of marker visibility. */ - labelCategoryVisibility: Readonly>; - onSetLabelCategoryVisibility: (category: PoiCategory, visible: boolean) => void; + labelCategoryVisibility: Readonly>; + onSetLabelCategoryVisibility: (category: LabelCategory, visible: boolean) => void; // ── Display group (power-user disclosure) ────────────────────────────── /** Currently-selected tone-mapping curve. */ @@ -485,7 +472,7 @@ export function SettingsPanel({ })() : null; - // ── Labels master (over all four PoiCategory entries) ─────────────────── + // ── Labels master (over every LABEL_CATEGORIES entry) ─────────────────── const labelsMaster = (() => { const enabledCount = LABEL_CATEGORIES.reduce( (n, cat) => (labelCategoryVisibility[cat] ? n + 1 : n), @@ -818,7 +805,7 @@ export function SettingsPanel({ return (