diff --git a/.claude/skills/add-data-source/SKILL.md b/.claude/skills/add-data-source/SKILL.md index 1b0c04f0f..401fda9ff 100644 --- a/.claude/skills/add-data-source/SKILL.md +++ b/.claude/skills/add-data-source/SKILL.md @@ -85,21 +85,21 @@ errors guide you to the totality sites. | 4 | Pick decode | `src/data/selectionEncoding.ts` (`PickResult` kind + `unpackPick`) | ⚠️ **silent** inverse map | | 4b | WESL pick parity | `src/services/gpu/shaders/lib/selectionEncoding.wesl` (`SOURCE_CODE_X`) | ⚠️ parity test only | | 5 | Click guard | `src/services/engine/interaction/clickHandler.ts` (`\|\| kind === 'X'`) | ⚠️ **silent** | -| 6 | Seed parser | `tools/parsers/parseClusterSeed.ts` (`VALID_CATEGORIES`) | ⚠️ **silent** | +| 6 | Seed parser | `tools/parsers/parseStructureSeed.ts` (`VALID_CATEGORIES`) | ⚠️ **silent** | | 7 | Marker style row | `src/services/engine/presentation/structurePoiStyles.ts` | ✅ totality Record | | 8 | Build records | `src/data/buildStaticAnchorStructures.ts` (`SeedEntry.category` + switch) | ✅ switch exhaustiveness | -| 9 | Marker renderer | `src/services/gpu/renderers/clusterMarkerRenderer.ts` (**~11 sites**, below) | mixed | +| 9 | Marker renderer | `src/services/gpu/renderers/structureMarkerRenderer.ts` (**~11 sites**, below) | mixed | | 10 | UI naming | `src/data/poiCategoryInfo.ts` (`label` / `shortLabel` / `plural`) | ✅ totality Record | | 11 | Settings lists | `src/components/SettingsPanel/SettingsPanel.tsx` (`STRUCTURE_CATEGORIES`, `LABEL_CATEGORIES`) | ⚠️ **silent** arrays | | 12 | Bulk-fetch gate | `src/services/engine/wiring/assetWiring.ts` (`BULK_CATALOG_CATEGORIES`) | ⚠️ include **only if** it has a `.ccat` | -| 13 | Focus predicate | `src/services/engine/subsystems/clusterFocusSubsystem.ts` (`\|\| category === 'X'`) | ⚠️ **silent** | +| 13 | Focus predicate | `src/services/engine/subsystems/structureFocusSubsystem.ts` (`\|\| category === 'X'`) | ⚠️ **silent** | | 14 | Settings count | `src/services/engine/wiring/wireStructureProjection.ts` (`emitCounts`) | ⚠️ **silent** — no count shown if missed | | 15 | Visibility defaults | `useEngineSettings.ts` ×2, `engine.ts` ×2, **+ test fixtures** | ⚠️ **copy-paste ×8** | | 16 | Debug panel | `src/components/DebugPanel/LabelEffectsSection.tsx` (`CATEGORIES`) | ⚠️ **silent** | -| 17 | Seed data | `data/cluster_anchors.seed.json` (**gitignored — `git add -f`**) | — | +| 17 | Seed data | `data/structure_anchors.seed.json` (**gitignored — `git add -f`**) | — | | 18 | Runtime enumeration tests | `tests/data/poiCategories.test.ts` (key counts, "N-category" titles) | ⚠️ **silent** — assert the new total | -`clusterMarkerRenderer.ts` is the densest — the ~11 sites: `SOURCE_CODE_BY_CATEGORY`, +`structureMarkerRenderer.ts` is the densest — the ~11 sites: `SOURCE_CODE_BY_CATEGORY`, `POI_CATEGORIES_WITH_MARKERS`, the per-category `Record` literals (`bucketOffsets` / `bucketCounts` / `sourceBuffers` / `sourceBindGroups` / `writeCursor`), the `setMarkers` reset / count-guard / write-guard, and the diff --git a/.gitignore b/.gitignore index baa885a46..f212bb28d 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,7 @@ pnpm-debug.log* # Exception: hand-curated catalog seed files (small, hand-authored, tracked). !/data/famous_galaxies.seed.json -!/data/cluster_anchors.seed.json +!/data/structure_anchors.seed.json # Exception: hand-curated override index produced by the famous-galaxy # curator (tools/famous-curator). Small JSON (~75 entries × ~150 B) that @@ -118,7 +118,7 @@ pnpm-debug.log* # Exception: MCXC + MSCC cluster/supercluster raw-data READMEs + sha256 # sidecars. The .dat tables and VizieR ReadMes stay gitignored (fetched -# on demand via `npm run fetch-clusters`), but the READMEs document +# on demand via `npm run fetch-structures`), but the READMEs document # provenance and the .sha256 files let the parser detect truncated # downloads — both must be in git. !/data/raw/mcxc/README.md diff --git a/data/cluster_anchors.seed.json b/data/structure_anchors.seed.json similarity index 100% rename from data/cluster_anchors.seed.json rename to data/structure_anchors.seed.json diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index 13908cef6..b3ef65d81 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -62,7 +62,7 @@ Diagnosed but unplanned. Captured here so they don't get lost; promote to a spec - **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. - **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/cluster_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. +- **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. - **Milliquas colour check** — Milliquas points currently all render blue; verify the colour-index / colour mapping for the quasar source isn't collapsing to a single hue. - **Tour feature (full)** — finish the camera tour beyond the Part-2 seed. Tracked design: [splash-screen Part 2 plan](superpowers/plans/2026-05-20-splash-screen-02-stub-tour.md) (ready to execute; engine-side `engine.tour` seed, verified current 2026-06-04) and [2026-05-07 tour-animation spec](superpowers/specs/2026-05-07-tour-animation-design.md) (re-grounded 2026-06-04; labels + MW impostor + engine-API decisions resolved, cinematic palette documented). Execute Part 2 first, then resolve the spec's remaining open decisions (rotation slerp, caption producer, beat list/timing), promote to a plan, and extend the seed into the real waypoint tour. - **Thumbnail quality (SDSS / DSS branches)** — the auto-fetched SDSS-cutout and CDS-DSS thumbnails still have the original quality issues: ranked fix options are mask, sky-sub, per-galaxy size, DESI source, brightness norm (see memory `project_thumbnail_quality`). The *famous-galaxy* branch is now fully addressed — procedural-disk fade-out, high-res LOD (#214), and thumbnail calibration + square deproject + disk-plane unification (#229/#234/#235/#240) all shipped — so this item is scoped to the non-curated SDSS/DSS path only. @@ -80,9 +80,9 @@ Diagnosed but unplanned. Captured here so they don't get lost; promote to a spec - **Promote the Milky Way to a first-class `Source` (streamline its identity)** — surfaced 2026-06-04 alongside the category-DRY item. The Milky Way is **first-class on the rendering axis** (`milkyWayRenderer` procedural impostor, `milkyWayPass`, `EngineMilkyWayHandle`) but a **pseudo-everything on the identity axis**, modelled ~5 different ways: a `FamousMetaEntry` palette pseudo-entry with sentinel id `__milky-way__` intercepted in `App.tsx` onSelect (`data/milkyWayEntry.ts`); "expressed via the famousGalaxy category" for the settings toggle (`SettingsPanel.tsx:812`); a bespoke `'youAreHere'` member bolted onto `PoiCategory` via `LabelStyleOverrideTarget = 'youAreHere' | PoiCategory` (`labelStyleOverride.ts:40`); and a bespoke visibility/fade path (`youAreHereSubsystem`, `youAreHereVisibility`) that bypasses the `labelCategoryVisibility` Record every other category uses. `data/milkyWayEntry.ts` even documents the deferral: *"we don't promote the Milky Way to a real `Source` (that's a larger architectural decision the user deferred)."* **Goal:** make the MW a real `Source` (next free code = **16**, pick-only / not persisted to `.bin`, non-breaking — same as the POI codes) so its visibility, label identity, selection, and focus flow through the *same* plumbing as every other source, retiring the sentinel hack, the famousGalaxy masquerade, the `'youAreHere'` union extension, and the bespoke youAreHere subsystem. **Boundary (important):** "similar to other sources" means the *identity / visibility / selection / focus* plumbing — the MW's bespoke **renderer** (the procedural impostor disk) stays its own subsystem; it just hangs off a registry entry. **Open design decisions for the brainstorm:** (a) does MW reuse an existing `SourceEntry` `type` or get a new one (e.g. `'impostor'`)?; (b) the **pick surface** — today you can't click the MW; making it selectable needs a pick target (the impostor billboard? a galactic-centre marker?); (c) mapping `focusOnHome` onto the standard selection→focus path (a MW-specific framing distance); (d) keep or retire the palette pseudo-entry. **Depends on / co-designs with** the structure-category-identity consolidation above — both hinge on making the *marker-bearing vs label-only* distinction explicit (the MW is label-only + bespoke-render; famousGalaxy is label-only + point-picked). Needs a brainstorm → spec → plan; do **not** drop-in. -- **`cluster*` → `structure*` naming migration** — surfaced 2026-06-04 while adding `group`. The featured-structure seam is named after one of the four categories it now holds (cluster / supercluster / void / group): the seed file `data/cluster_anchors.seed.json`, the `rawDataRegistry` key `clusters.seed`, `parseClusterSeed` / `ClusterSeedEntry` / `validateClusterSeedEntry`, plus `clusterMarkerRenderer`, `clusterFocusSubsystem`, `clusterCatalogSlot`, `clusters.ccat` / `clusters_meta.json`. The rest of the codebase moved to `Structure*` vocabulary in #253/#254 (`StructureCategory`, `StructureRecord`, `structures` store, `structurePoiStyles`, `produceStructure*`), so these are the holdouts. Rename to `structure_anchors.seed.json` / `parseStructureSeed` / `StructureSeedEntry` / `structureMarkerRenderer` etc. Pure clarity (no behaviour change), but touches a tracked data file (`git mv` + `.gitignore` exception update), the registry key, the parser, and consumers (`buildClusters.ts`, audit scripts) — own focused diff, pairs with the identity consolidation above. Mechanical; typecheck + tests are the safety net. +- ~~**`cluster*` → `structure*` naming migration**~~ — **DONE (PR #280).** The featured-structure seam was named after one of the four categories it holds (cluster / supercluster / void / group); the rest of the codebase had already moved to `Structure*` vocabulary in #253/#254. The whole holdout set is now renamed: the catalog family (`StructureCatalog*`, `structureCatalogFormat`, `structureCatalogSlot`, `structureCatalogFetcher`, `structureCatalogToStructures`), the focus subsystem (`structureFocusSubsystem`), the seed parser (`parseStructureSeed` / `StructureSeedEntry`) + seed file (`data/structure_anchors.seed.json`) + registry key (`structures.seed`), the membership util (`structureMembership`), the build tool (`tools/structures/buildStructures.ts`) and fetcher (`fetchStructureCatalogs`), and the served artifacts (`structures.ccat` / `structures_meta.json`) + their npm scripts (`build-structures` / `fetch-structures`). The cluster CATEGORY (`Source.Cluster`, the `'cluster'` id, X-ray clusters/MCXC, member clusters/MSCC) and the `.ccat` extension / `CCAT` magic stay verbatim. **Deploy:** the artifact rename means a `npm run build-structures` + `npm run sync-r2-secure` re-publish from the main worktree is required so R2 serves `structures.ccat` / `structures_meta.json` under the new names. - **Cosmic zoom plan** — 60-doc "Powers of Ten" walkthrough plan drafted in worktree `cosmic-zoom-plan` (2026-05-08), awaiting user review. See memory `project_cosmic_zoom_plan`. -- **Structure search (cluster / supercluster / void)** — the command palette (`CommandPalette.tsx`) only indexes the famous-galaxy atlas (~75) and the PGC alias index (~48k GLADE+2MRS rows). Structure POIs — clusters, superclusters, and voids (MCXC + MSCC, names + Abell numbers + descriptions already in `public/data/clusters_meta.json`) — aren't searchable, so there's no way to look up "Coma", "A2703", "MSCC 216", a named void, etc. and fly to them. Add a third search index over the structure catalog (all three categories) + a select handler that selects the structure POI and frames the camera. Naturally pairs with naming large-scale structures (e.g. a "Sloan Great Wall" / "CfA Great Wall" entry) so they become navigable by name. +- **Structure search (cluster / supercluster / void)** — the command palette (`CommandPalette.tsx`) only indexes the famous-galaxy atlas (~75) and the PGC alias index (~48k GLADE+2MRS rows). Structure POIs — clusters, superclusters, and voids (MCXC + MSCC, names + Abell numbers + descriptions already in `public/data/structures_meta.json`) — aren't searchable, so there's no way to look up "Coma", "A2703", "MSCC 216", a named void, etc. and fly to them. Add a third search index over the structure catalog (all three categories) + a select handler that selects the structure POI and frames the camera. Naturally pairs with naming large-scale structures (e.g. a "Sloan Great Wall" / "CfA Great Wall" entry) so they become navigable by name. - **No general `add-data-source` skill (+ its checklist)** — surfaced 2026-06-04 while adding `group`. Skills exist for the *narrow* cases (`add-famous` for the famous-galaxy pipeline, `link-data` for symlinking real catalogs into a worktree) but there's no skill that walks the full "add a new data source / featured category" path, so steps get missed piecemeal. Concrete checklist items discovered the hard way, each of which should live in such a skill: - **Settings-panel per-category count.** A new structure category must be added to the `onStructureCountsChange` emission (`wireStructureProjection.ts` `emitCounts`) — the SettingsPanel only renders a count when `structureCounts?.[cat] !== undefined`, so a missing category shows a toggle with no number (the `group` bug, fixed 2026-06-04 + guarded by a test). Any per-source/per-category count surfaced in the UI has this shape. - **Seed the real data early** (memory `feedback_seed_data_early`): wire real data right after the parser, not as a late task, so the rest of the work has something to look at. diff --git a/docs/superpowers/plans/2026-05-27-renderer-interface-extraction.md b/docs/superpowers/plans/2026-05-27-renderer-interface-extraction.md index d45bf8672..f157c27ee 100644 --- a/docs/superpowers/plans/2026-05-27-renderer-interface-extraction.md +++ b/docs/superpowers/plans/2026-05-27-renderer-interface-extraction.md @@ -158,7 +158,7 @@ plugin), Vitest (`node` env). No build-time additions. `EngineSettingsState` and `requestRender()`. The projection picks them up next frame. - `src/services/gpu/renderers/filamentRenderer.ts`, - `pointRenderer.ts`, `clusterMarkerRenderer.ts` — drop their own + `pointRenderer.ts`, `structureMarkerRenderer.ts` — drop their own `fadeBuffer` / `fadeBindGroup` per-instance fields; ask the registry for the bind group at bind time. - `src/services/gpu/renderers/labelRenderer*.ts` — same (only if their @@ -457,12 +457,12 @@ Per ADR 0001 §Decision items 3. **Files:** - Modify: `filamentRenderer.ts`, `pointRenderer.ts`, - `clusterMarkerRenderer.ts`, and the label renderers if applicable. + `structureMarkerRenderer.ts`, and the label renderers if applicable. Mechanical repeat of T9. Per ADR 0001 §Implementation Notes item 1, order is lowest-risk first: -- [ ] **Step 1: `clusterMarkerRenderer`** (smallest surface, fewest +- [ ] **Step 1: `structureMarkerRenderer`** (smallest surface, fewest tests touched). Follow T9's Step 1–4 shape: failing buffer-spy test → migrate → strip from type → green. - [ ] **Step 2: `filamentRenderer`.** Same shape. diff --git a/docs/superpowers/plans/2026-06-06-focus-recession-fade.md b/docs/superpowers/plans/2026-06-06-focus-recession-fade.md index cd35a5d24..c03f87478 100644 --- a/docs/superpowers/plans/2026-06-06-focus-recession-fade.md +++ b/docs/superpowers/plans/2026-06-06-focus-recession-fade.md @@ -68,7 +68,7 @@ Large diffuse fields (filaments/volumes) recede harder; markers/labels dim moder `blend` is `FocusUniformsValue.blend`, produced once per frame by `state.subsystems.clusterFocus.produceFocusUniforms(nowMs)` -(`clusterFocusSubsystem.ts:104`). That call **ticks the fade controller**, so it must +(`structureFocusSubsystem.ts:104`). That call **ticks the fade controller**, so it must run **exactly once per frame** — calling it twice double-advances the ramp. Today it's computed late, at `runFrame.ts:262–296`, after the label director diff --git a/docs/superpowers/specs/2026-06-05-focus-recession-fade-interface-design.md b/docs/superpowers/specs/2026-06-05-focus-recession-fade-interface-design.md index b09df6a14..c7dcd9dd2 100644 --- a/docs/superpowers/specs/2026-06-05-focus-recession-fade-interface-design.md +++ b/docs/superpowers/specs/2026-06-05-focus-recession-fade-interface-design.md @@ -56,15 +56,15 @@ two parts and combine them with their focused-instance exemption. The obvious-looking move — have the registry store the blend (`setFocusBlend`) and return `toggle × recession` from `opacityOf` — **complects two independent concerns -and mirrors state**. The blend's authoritative home is `clusterFocusSubsystem` +and mirrors state**. The blend's authoritative home is `structureFocusSubsystem` (`FocusUniformsValue.blend`); caching it in the registry is a value×place mirror (stale-mirror bug class), and it drags the focus concept into a module whose sole job is fade controllers. Toggle fade and focus recession vary independently, so they are *composed*, not braided. (Radar finding, 2026-06-06 — see `docs/superpowers/conventions/simplicity.md` #5, #8.) -`blend` is the **same 0→1 value `clusterFocusSubsystem` already produces** — -`FocusUniformsValue.blend` (`clusterFocusSubsystem.ts:106`), already computed once +`blend` is the **same 0→1 value `structureFocusSubsystem` already produces** — +`FocusUniformsValue.blend` (`structureFocusSubsystem.ts:106`), already computed once per frame at `runFrame.ts:296` and threaded into the render settings. Consumers read it from there as a **value** (an argument), never from a mirror. It already gates render-on-demand via `clusterFocus.isAwake()`, so no wake logic changes. diff --git a/package.json b/package.json index 66d5d78f7..ba2629a7c 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "build-cf4-density": "tsx tools/volumes/buildCf4Density.ts", "build-flow-field": "tsx tools/flow/buildFlowField.ts", "verify-flow-field": "tsx tools/flow/verifyFlowField.ts", - "build-clusters": "tsx tools/clusters/buildClusters.ts", + "build-structures": "tsx tools/structures/buildStructures.ts", "build-mcpm": "tsx tools/volumes/buildMcpmVolume.ts --all", "build-famous": "tsx tools/famous/buildFamous.ts", "build-famous-hires": "tsx tools/famous/copyHiResToPublic.ts", @@ -54,7 +54,7 @@ "dev": "vite", "fetch-2mass-xsc": "tsx tools/fetch/fetch2massXsc.ts", "fetch-cf4": "tsx tools/fetch/fetchCosmicflows4.ts", - "fetch-clusters": "tsx tools/fetch/fetchClusterCatalogs.ts", + "fetch-structures": "tsx tools/fetch/fetchStructureCatalogs.ts", "fetch-famous-images": "tsx tools/famous/fetchFamousImages.ts", "fetch-hyperleda": "tsx tools/fetch/fetchHyperLeda.ts", "fetch-milliquas": "tsx tools/fetch/fetchMilliquas.ts", diff --git a/src/@types/data/SkyCoord.d.ts b/src/@types/data/SkyCoord.d.ts index 585d9b72d..754f0d495 100644 --- a/src/@types/data/SkyCoord.d.ts +++ b/src/@types/data/SkyCoord.d.ts @@ -1,7 +1,7 @@ /** * SkyCoord — RA hours, declination degrees, distance Mpc. * - * Base shape used by `data/cluster_anchors.seed.json` entries and + * Base shape used by `data/structure_anchors.seed.json` entries and * their CF-4 audit consumers. RA in HOURS (not degrees) follows the * astronomical convention for catalogue tables; the standard * `raHours * 15 * π/180` conversion to radians lives in diff --git a/src/@types/data/ClusterCatalog.d.ts b/src/@types/data/StructureCatalog.d.ts similarity index 95% rename from src/@types/data/ClusterCatalog.d.ts rename to src/@types/data/StructureCatalog.d.ts index e9705ae3d..816656419 100644 --- a/src/@types/data/ClusterCatalog.d.ts +++ b/src/@types/data/StructureCatalog.d.ts @@ -1,5 +1,5 @@ /** - * ClusterCatalog — the runtime decoded shape of a `.ccat` binary file. + * StructureCatalog — the runtime decoded shape of a `.ccat` binary file. * * Mirrors the SoA layout of `GalaxyCatalog`: separate typed arrays for each * field so the renderer can pass them straight to `device.queue.writeBuffer` @@ -34,13 +34,13 @@ */ /** Cluster (0) vs. supercluster (1) marker; higher values reserved. */ -export type ClusterCategoryByte = 0 | 1; +export type StructureCategoryByte = 0 | 1; /** * Cluster / supercluster catalog in renderer-ready layout — a struct of * arrays rather than an array of objects. Parallel to `GalaxyCatalog`. */ -export type ClusterCatalog = { +export type StructureCatalog = { /** Number of structures. All typed arrays derive their length from this. */ readonly count: number; diff --git a/src/@types/engine/frame/ReadyFrameContext.d.ts b/src/@types/engine/frame/ReadyFrameContext.d.ts index 56690abd7..921e7b2a4 100644 --- a/src/@types/engine/frame/ReadyFrameContext.d.ts +++ b/src/@types/engine/frame/ReadyFrameContext.d.ts @@ -68,7 +68,7 @@ export type ReadyFrameContext = { drawCamPos: Readonly; /** `canvasSize.height / (2·tan(fovY/2))` — pinhole radian→pixel conversion. */ drawPxPerRad: number; - /** Cluster-focus recession blend 0→1, from clusterFocus.produceFocusUniforms (ticked once/frame). */ + /** Structure-focus recession blend 0→1, from structureFocus.produceFocusUniforms (ticked once/frame). */ focusBlend: number; /** * Non-null GPU + subsystem handles, narrowed across the bootstrap diff --git a/src/@types/engine/frame/RenderFrameSettings.d.ts b/src/@types/engine/frame/RenderFrameSettings.d.ts index 182306798..80314d469 100644 --- a/src/@types/engine/frame/RenderFrameSettings.d.ts +++ b/src/@types/engine/frame/RenderFrameSettings.d.ts @@ -47,7 +47,7 @@ export type RenderFrameSettings = { pxFadeEndPoints: number; /** * Cluster focus-mode uniform for the points pass's @group(3) binding. - * Produced once per frame by `clusterFocusSubsystem.produceFocusUniforms` + * Produced once per frame by `structureFocusSubsystem.produceFocusUniforms` * in `runFrame` (so it shares the frame's single `nowMs`). At rest * (`blend: 0`) the shader's per-vertex multiplier collapses to 1.0. */ diff --git a/src/@types/engine/handles/EngineGpuHandles.d.ts b/src/@types/engine/handles/EngineGpuHandles.d.ts index 23a1cd7ae..2790bac78 100644 --- a/src/@types/engine/handles/EngineGpuHandles.d.ts +++ b/src/@types/engine/handles/EngineGpuHandles.d.ts @@ -47,7 +47,7 @@ import type { FilamentRenderer } from '../../rendering/FilamentRenderer'; import type { LabelRenderer } from '../../rendering/LabelRenderer'; import type { MarkerLineRenderer } from '../../rendering/MarkerLineRenderer'; import type { SelectionRingRenderer } from '../../rendering/SelectionRingRenderer'; -import type { ClusterMarkerRenderer } from '../../rendering/ClusterMarkerRenderer'; +import type { StructureMarkerRenderer } from '../../rendering/StructureMarkerRenderer'; import type { ScalarVolumeRenderer } from '../../rendering/ScalarVolumeRenderer'; import type { FlowFieldRenderer } from '../../rendering/FlowFieldRenderer'; import type { VolumeUpsample } from '../../rendering/VolumeUpsample'; @@ -153,16 +153,16 @@ export type EngineGpuHandles = { */ selectionRingRenderer: SelectionRingRenderer | null; /** - * Cluster-marker renderer — draws halo + ring overlays for POI clusters - * (one renderer for all POI source categories; per-source bind groups - * live inside the renderer). Null until `initGpu` constructs it. + * Structure-marker renderer — draws halo + ring overlays for every + * structure category (cluster / supercluster / void / group; per-source + * bind groups live inside the renderer). Null until `initGpu` constructs it. * Excluded from the `isEngineReady` predicate for the same reason as * `markerLineRenderer` — null-checked at point of use by the - * cluster-marker frame pass. Stored here so `destroy()` can release + * structure-marker frame pass. Stored here so `destroy()` can release * the renderer's GPU buffers (per-category bind groups + per-instance * buffer + corner VBO). */ - clusterMarkerRenderer: ClusterMarkerRenderer | null; + structureMarkerRenderer: StructureMarkerRenderer | null; /** * Atlas-bound 3D-oriented disk renderer for large galaxy thumbnails * (close-approach view). Null until `initGpu` constructs it from a diff --git a/src/@types/engine/handles/EngineSubsystemHandles.d.ts b/src/@types/engine/handles/EngineSubsystemHandles.d.ts index da1884f9b..d61df17f9 100644 --- a/src/@types/engine/handles/EngineSubsystemHandles.d.ts +++ b/src/@types/engine/handles/EngineSubsystemHandles.d.ts @@ -28,7 +28,7 @@ import type { SelectionSubsystem } from '../subsystems/SelectionSubsystem'; import type { BiasCorrectionSubsystem } from '../subsystems/BiasCorrectionSubsystem'; import type { YouAreHereSubsystem } from '../subsystems/YouAreHereSubsystem'; import type { LabelDirectorSubsystem } from '../subsystems/LabelDirectorSubsystem'; -import type { ClusterFocusSubsystem } from '../subsystems/ClusterFocusSubsystem'; +import type { StructureFocusSubsystem } from '../subsystems/StructureFocusSubsystem'; import type { TweenManager } from '../../camera/TweenManager'; import type { ClickResolver } from '../ClickResolver'; import type { InputBindings } from '../../input/InputBindings'; @@ -116,7 +116,7 @@ export type EngineSubsystemHandles = { * `produceFocusUniforms(nowMs)` into the points draw. Constructed * eagerly; no GPU dep, non-null from t=0. */ - clusterFocus: ClusterFocusSubsystem; + structureFocus: StructureFocusSubsystem; /** * Per-engine download-progress emitter — instantiated inside the GPU * init IIFE so `cb.onLoadProgress` and the slot registry are in scope. diff --git a/src/@types/engine/state/EngineAssetSlots.d.ts b/src/@types/engine/state/EngineAssetSlots.d.ts index 2a0e56636..f8236c925 100644 --- a/src/@types/engine/state/EngineAssetSlots.d.ts +++ b/src/@types/engine/state/EngineAssetSlots.d.ts @@ -31,8 +31,8 @@ import type { ScalarCube } from '../../data/ScalarCube'; import type { SyntheticVolumeReq } from '../../loading/SyntheticVolumeReq'; import type { MCPMReq } from '../../loading/MCPMReq'; import type { CompanionAssetReq } from '../../loading/CompanionAssetReq'; -import type { ClusterCatalogPayload } from '../../loading/ClusterCatalogPayload'; -import type { ClusterCatalogReq } from '../../loading/ClusterCatalogReq'; +import type { StructureCatalogPayload } from '../../loading/StructureCatalogPayload'; +import type { StructureCatalogReq } from '../../loading/StructureCatalogReq'; import type { SourceType } from '../../data/SourceType'; export type EngineAssetSlots = { @@ -58,7 +58,7 @@ export type EngineAssetSlots = { */ famousMeta: AssetSlot | null; /** - * Cluster/supercluster coverage layer (`clusters.ccat` + `clusters_meta.json`) + * Cluster/supercluster coverage layer (`structures.ccat` + `structures_meta.json`) * routed through a slot for parity with the other CPU-side sidecars. Loaded * eagerly at engine boot; the payload is small. * @@ -67,7 +67,7 @@ export type EngineAssetSlots = { * written into the structure store. Null until the IIFE mints it * (matches `famousMeta` for the same lifecycle reason). */ - clusterCatalog: AssetSlot | null; + structureCatalog: AssetSlot | null; /** * PGC → human-name alias map (`pgc_aliases.json`, ~1.7 MB). Lazy: * the engine never auto-loads it; the public-handle's diff --git a/src/@types/engine/state/FocusState.d.ts b/src/@types/engine/state/FocusState.d.ts index 148ad8620..c65de412a 100644 --- a/src/@types/engine/state/FocusState.d.ts +++ b/src/@types/engine/state/FocusState.d.ts @@ -42,7 +42,7 @@ export type FocusState = { readonly category: 'cluster' | 'supercluster' | 'void'; /** - * Packed-identity members from `clusterMembership(...)`. CPU-side + * Packed-identity members from `structureMembership(...)`. CPU-side * consumers (InfoCard count text, tour iterator, etc.) read this * directly; the shader's membership test recomputes per-vertex from * `(center, radiusMpc)` rather than uploading this array. diff --git a/src/@types/engine/subsystems/ClusterFocusSubsystem.d.ts b/src/@types/engine/subsystems/StructureFocusSubsystem.d.ts similarity index 90% rename from src/@types/engine/subsystems/ClusterFocusSubsystem.d.ts rename to src/@types/engine/subsystems/StructureFocusSubsystem.d.ts index 786d05788..312e801bb 100644 --- a/src/@types/engine/subsystems/ClusterFocusSubsystem.d.ts +++ b/src/@types/engine/subsystems/StructureFocusSubsystem.d.ts @@ -3,7 +3,7 @@ import type { FocusUniformsValue } from '../../rendering/FocusUniformsValue'; import type { Destroyable } from '../../rendering/Destroyable'; /** - * ClusterFocusSubsystem — owns cluster "focus mode": when a cluster / + * StructureFocusSubsystem — owns cluster "focus mode": when a cluster / * supercluster / void POI 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 @@ -23,11 +23,11 @@ import type { Destroyable } from '../../rendering/Destroyable'; * The points vertex shader re-derives `distance(p.position, center) < * radius` per-vertex, so this subsystem never computes a CPU member list * — it only supplies center, radius, and the smoothstep blend. (The pure - * `clusterMembership` fn stays available if a future feature needs an + * `structureMembership` fn stays available if a future feature needs an * explicit count/list.) */ -export type ClusterFocusSubsystem = { - readonly id: 'clusterFocus'; +export type StructureFocusSubsystem = { + readonly id: 'structureFocus'; /** * Per-frame state sync. Diffs `focusedPoi?.id` against the currently diff --git a/src/@types/loading/AssetKey.d.ts b/src/@types/loading/AssetKey.d.ts index 039a2704b..d0b876c9e 100644 --- a/src/@types/loading/AssetKey.d.ts +++ b/src/@types/loading/AssetKey.d.ts @@ -7,9 +7,9 @@ import type { SourceType } from '../data/SourceType'; * a set of auxiliary string keys for assets that don't map one-to-one to a * single `Source`: * - * - `'clusterCatalog'` — the `.ccat` seed shared by Cluster, Supercluster, + * - `'structureCatalog'` — the `.ccat` seed shared by Cluster, Supercluster, * and Void POIs. All three `Source` codes pull their geometry from one file, - * so a per-source fetch key would be wrong: there is no `clusterCatalog` + * so a per-source fetch key would be wrong: there is no `structureCatalog` * `Source`, and a single fetch must not trigger three loads. * * - `'famousMeta'` — the `famous_meta.json` sidecar that accompanies the @@ -39,7 +39,7 @@ import type { SourceType } from '../data/SourceType'; * asset key to route through `slotFor`. * * The asymmetry cuts both ways: some `Source`s are NOT fetched individually - * (Cluster / Supercluster / Void all arrive via `'clusterCatalog'`), and the + * (Cluster / Supercluster / Void all arrive via `'structureCatalog'`), and the * string keys are NOT all `Source`s. "Source" (stable identity code, persisted * to `.bin` + GPU buffers) and "Asset" (fetchable network resource) are * different sets; this type is the asset set. @@ -48,7 +48,7 @@ import type { SourceType } from '../data/SourceType'; */ export type AssetKey = | SourceType - | 'clusterCatalog' + | 'structureCatalog' | 'famousMeta' | 'pgcAlias' | 'filaments' diff --git a/src/@types/loading/ClusterCatalogPayload.d.ts b/src/@types/loading/StructureCatalogPayload.d.ts similarity index 70% rename from src/@types/loading/ClusterCatalogPayload.d.ts rename to src/@types/loading/StructureCatalogPayload.d.ts index cf79b02dc..197020011 100644 --- a/src/@types/loading/ClusterCatalogPayload.d.ts +++ b/src/@types/loading/StructureCatalogPayload.d.ts @@ -1,19 +1,19 @@ -import type { ClusterCatalog } from '../data/ClusterCatalog'; +import type { StructureCatalog } from '../data/StructureCatalog'; /** - * One entry of the `clusters_meta.json` sidecar — the string/identity fields + * One entry of the `structures_meta.json` sidecar — the string/identity fields * that the numeric `.ccat` deliberately omits. Position in the meta array * equals the record's localIdx in the parallel `.ccat`, so the runtime can * look up a clicked structure's human-readable label by the index the * pick-renderer returns. * - * Shape mirrors what `tools/clusters/buildClusters.ts` writes via its `toMeta` + * Shape mirrors what `tools/structures/buildStructures.ts` writes via its `toMeta` * mapping: `{ id, names, abell, description }` with `abell` a nullable string * (null for superclusters and any cluster lacking an Abell/ACO designation). * The build's own meta type is local + unexported, so this is the canonical * runtime mirror — keep the two in lock-step. */ -export type ClusterMetaEntry = { +export type StructureMetaEntry = { /** URL-safe slug of names[0] — the localIdx lookup key. */ id: string; /** Display names; names[0] is the primary label. */ @@ -25,12 +25,12 @@ export type ClusterMetaEntry = { }; /** - * The decoded cluster-catalog asset: the numeric `.ccat` catalog paired with + * The decoded structure-catalog asset: the numeric `.ccat` catalog paired with * its string sidecar. The two are built in lock-step and index-parallel — * `catalog.count === meta.length` is an invariant the fetcher enforces — so a * later merge step can attach names + descriptions to each record by localIdx. */ -export type ClusterCatalogPayload = { - catalog: ClusterCatalog; - meta: readonly ClusterMetaEntry[]; +export type StructureCatalogPayload = { + catalog: StructureCatalog; + meta: readonly StructureMetaEntry[]; }; diff --git a/src/@types/loading/ClusterCatalogReq.d.ts b/src/@types/loading/StructureCatalogReq.d.ts similarity index 67% rename from src/@types/loading/ClusterCatalogReq.d.ts rename to src/@types/loading/StructureCatalogReq.d.ts index 3dfb93d51..477de2b88 100644 --- a/src/@types/loading/ClusterCatalogReq.d.ts +++ b/src/@types/loading/StructureCatalogReq.d.ts @@ -1,8 +1,8 @@ /** - * The request shape `clusterCatalogFetcher` accepts. Empty — the cluster + * The request shape `structureCatalogFetcher` accepts. Empty — the cluster * catalog is a standalone boot-time asset, neither tiered (unlike * `FilamentReq`, which carries `tier`) nor per-survey (unlike * `CompanionAssetReq`). The empty-object type keeps the `Fetcher` * generic honest: there is genuinely nothing to vary the fetch on. */ -export type ClusterCatalogReq = Record; +export type StructureCatalogReq = Record; diff --git a/src/@types/rendering/FocusUniformsValue.d.ts b/src/@types/rendering/FocusUniformsValue.d.ts index b16e7d345..5d854f218 100644 --- a/src/@types/rendering/FocusUniformsValue.d.ts +++ b/src/@types/rendering/FocusUniformsValue.d.ts @@ -3,7 +3,7 @@ import type { Vec3 } from '../math/Vec3'; /** * FocusUniformsValue — CPU mirror of the 32-byte FocusUniforms block * (see src/services/gpu/shaders/lib/focusUniforms.wesl for the WGSL - * byte layout). Produced each frame by clusterFocusSubsystem and packed + * 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 diff --git a/src/@types/rendering/PickDebugOverlay.d.ts b/src/@types/rendering/PickDebugOverlay.d.ts index 6dc865554..1870371d9 100644 --- a/src/@types/rendering/PickDebugOverlay.d.ts +++ b/src/@types/rendering/PickDebugOverlay.d.ts @@ -19,7 +19,7 @@ * * Premultiplied OVER ('srcFactor: "one", dstFactor: "one-minus-src- * alpha"') against the swap chain. Matches the marker-lines / labels / - * clusterMarker overlay convention so future composite changes only + * structureMarker overlay convention so future composite changes only * have to consider one blend mode. * * ### Why no resize method diff --git a/src/@types/rendering/ClusterMarkerDescriptor.d.ts b/src/@types/rendering/StructureMarkerDescriptor.d.ts similarity index 91% rename from src/@types/rendering/ClusterMarkerDescriptor.d.ts rename to src/@types/rendering/StructureMarkerDescriptor.d.ts index 68810e65e..13998140f 100644 --- a/src/@types/rendering/ClusterMarkerDescriptor.d.ts +++ b/src/@types/rendering/StructureMarkerDescriptor.d.ts @@ -1,6 +1,6 @@ /** * One per-structure marker descriptor produced by `produceStructureMarkers` - * and consumed by `clusterMarkerRenderer.setMarkers`. + * and consumed by `structureMarkerRenderer.setMarkers`. * * Why a separate descriptor type instead of reusing `StructureRecord`? * Separation of concerns: a descriptor carries only what the renderer @@ -12,9 +12,9 @@ import type { Vec3 } from '../math/Vec3'; import type { Vec4 } from '../math/Vec4'; -import type { PoiCategory } from '../engine/data/PoiCategory'; +import type { StructureCategory } from '../engine/data/StructureCategory'; -export type ClusterMarkerDescriptor = { +export type StructureMarkerDescriptor = { /** * Stable structure id (mirrors `StructureRecord.id`). CPU-side metadata * only — the renderer ignores this field when packing the GPU @@ -27,7 +27,7 @@ export type ClusterMarkerDescriptor = { */ readonly id: string; /** Category — drives which draw bucket this descriptor lands in (per-category source-code uniform). */ - readonly category: PoiCategory; + readonly category: StructureCategory; /** World-space centre. */ readonly worldPos: Vec3; /** diff --git a/src/@types/rendering/ClusterMarkerRenderer.d.ts b/src/@types/rendering/StructureMarkerRenderer.d.ts similarity index 67% rename from src/@types/rendering/ClusterMarkerRenderer.d.ts rename to src/@types/rendering/StructureMarkerRenderer.d.ts index 1171cb3aa..0fe53182f 100644 --- a/src/@types/rendering/ClusterMarkerRenderer.d.ts +++ b/src/@types/rendering/StructureMarkerRenderer.d.ts @@ -1,17 +1,17 @@ /** - * Public handle returned by `createClusterMarkerRenderer`. Mirrors + * Public handle returned by `createStructureMarkerRenderer`. Mirrors * `MarkerLineRenderer`'s shape: typed methods, no internals leaked. * - * One renderer draws halos + rings for ALL POI categories. Per-category - * source-code differentiation happens inside the renderer (three - * pre-built per-source bind groups) so plan 3's pick path inherits the - * correct (sourceCode << 27) | poiIndex packing without further - * scaffolding. + * One renderer draws halos + rings for ALL structure categories + * (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. */ -import type { ClusterMarkerDescriptor } from './ClusterMarkerDescriptor'; +import type { StructureMarkerDescriptor } from './StructureMarkerDescriptor'; -export type ClusterMarkerRenderer = { +export type StructureMarkerRenderer = { /** Human-readable identifier. */ readonly label: string; /** @@ -21,19 +21,18 @@ export type ClusterMarkerRenderer = { * each bound to that category's SourceUniforms. * * Designed to be called by `runFrame.ts` once per frame from the - * output of `state.subsystems.pois.produceMarkers(state, ctx)`. + * output of `produceStructureMarkers(state, ctx)`. */ - setMarkers(descriptors: readonly ClusterMarkerDescriptor[]): void; + setMarkers(descriptors: readonly StructureMarkerDescriptor[]): void; /** * Issue the draws inside an in-flight render pass against the HDR target. * * `fadeOpacity` is the per-frame opacity scalar for the entire marker * layer. Folded into the alpha output via the shared * `lib::fadeUniforms::applyFade` helper — same contract as - * `filamentRenderer.draw(... fadeOpacity)`. At v1 the pass file - * passes a constant 1.0; a future FadeRegistry handle for cluster - * markers (e.g. for layer-toggle animations) can substitute its - * per-frame value here. + * `filamentRenderer.draw(... fadeOpacity)`. The pass file passes a + * constant 1.0; a FadeRegistry handle for structure markers (e.g. for + * layer-toggle animations) can substitute its per-frame value here. */ render( pass: GPURenderPassEncoder, @@ -44,13 +43,13 @@ export type ClusterMarkerRenderer = { /** Number of markers last passed to setMarkers. Used by the pass `enabled()` check. */ markerCount(): number; /** - * Issue one ring-pick draw per POI category (cluster / supercluster / - * void) into the caller-supplied render pass. The pass MUST already - * have: + * Issue one ring-pick draw per structure category (cluster / + * supercluster / void / group) into the caller-supplied render pass. + * The pass MUST already have: * * - The pick-pass colour attachment (r32uint pick texture) bound. * - A `depth24plus` depth attachment bound (this pipeline writes + - * tests depth so a galaxy in front of a POI ring claims the pixel). + * tests depth so a galaxy in front of a ring claims the pixel). * - `@group(0)` (CameraUniforms) already set by the caller — * `pickRing` deliberately does NOT bind it. The galaxy pick draws * bind the same canonical `@group(0)` immediately beforehand and diff --git a/src/@types/settings/EngineSettingsState.d.ts b/src/@types/settings/EngineSettingsState.d.ts index e42a2effd..966feb4de 100644 --- a/src/@types/settings/EngineSettingsState.d.ts +++ b/src/@types/settings/EngineSettingsState.d.ts @@ -190,7 +190,7 @@ export type EngineSettingsState = { /** * Per-category visibility for the POI MARKER overlay — the ring + * halo glyph drawn at the POI's world anchor by - * `clusterMarkerRenderer`. Symmetric to `labelCategoryVisibility`; + * `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. diff --git a/src/data/buildStaticAnchorStructures.ts b/src/data/buildStaticAnchorStructures.ts index 3308a9ca0..b6f967381 100644 --- a/src/data/buildStaticAnchorStructures.ts +++ b/src/data/buildStaticAnchorStructures.ts @@ -1,7 +1,7 @@ /** * buildStaticAnchorStructures — assemble the static `StructureRecord[]` list * from the curated cluster/supercluster/void/group seed in - * `data/cluster_anchors.seed.json`. + * `data/structure_anchors.seed.json`. * * ### Why a separate module? * @@ -19,7 +19,7 @@ * Keeping a single helper here means both call sites agree on: * * - The id rule: `${category}-${seed.id}`, where `seed.id` is the - * curated identifier in `cluster_anchors.seed.json`. Using the seed + * curated identifier in `structure_anchors.seed.json`. Using the seed * field directly means the deep-link hash is the single canonical * identity — no slug-function drift for names that contain non-ASCII * characters or punctuation. @@ -61,14 +61,14 @@ import type { StructureRecord } from '../@types/engine/data/StructureRecord'; // Vite resolves JSON imports at build time; TypeScript narrows the type // via `resolveJsonModule: true`. We cast to the fields we consume so // new seed columns don't require a type update here. The JSON's shape is -// validated at build time by `tools/parsers/parseClusterSeed.ts` (run via -// `buildClusters.ts`), so this module trusts the cast and skips a runtime +// validated at build time by `tools/parsers/parseStructureSeed.ts` (run via +// `buildStructures.ts`), so this module trusts the cast and skips a runtime // re-validator. -import clusterSeedJson from '../../data/cluster_anchors.seed.json'; +import structureSeedJson from '../../data/structure_anchors.seed.json'; /** * Minimal shape we need from each seed entry — a strict subset of - * ClusterSeedEntry from `tools/parsers/parseClusterSeed.ts`. Defined + * StructureSeedEntry from `tools/parsers/parseStructureSeed.ts`. Defined * locally so the src/ tsconfig (which excludes tools/) doesn't need to * reach across the boundary for a runtime-erased type. */ @@ -138,5 +138,5 @@ function buildAnchorStructure(a: SeedEntry): StructureRecord { * reference identity is preserved across renders). */ export function buildStaticAnchorStructures(): StructureRecord[] { - return (clusterSeedJson as SeedEntry[]).map(buildAnchorStructure); + return (structureSeedJson as SeedEntry[]).map(buildAnchorStructure); } diff --git a/src/data/clusterCatalogFormat.ts b/src/data/structureCatalogFormat.ts similarity index 88% rename from src/data/clusterCatalogFormat.ts rename to src/data/structureCatalogFormat.ts index f7f058224..d968b143d 100644 --- a/src/data/clusterCatalogFormat.ts +++ b/src/data/structureCatalogFormat.ts @@ -1,15 +1,16 @@ /** - * Binary on-disk format for a `ClusterCatalog` — version 1 (CCAT). + * Binary on-disk format for a `StructureCatalog` — version 1 (CCAT). * - * Encodes the MCXC cluster + MSCC supercluster catalog into a compact - * fixed-record binary so the browser can fetch and decode it in a single + * Encodes the featured-structure catalog (clusters, superclusters, voids, + * groups) into a compact fixed-record binary so the browser can fetch and + * decode it in a single * pass without JSON parsing overhead. The approach mirrors * `galaxyCatalogFormat.ts` exactly: a 16-byte magic+version header * followed by fixed-size per-record blocks decoded into struct-of-arrays * output. * * Why a new format rather than extending `galaxyCatalogFormat`? - * Clusters and superclusters have fundamentally different fields (two + * Structures have fundamentally different fields from galaxies (two * radii, a mass/richness proxy, a category byte) and are drawn by a * separate renderer pass — co-opting the galaxy record layout would * waste most of its 64 bytes and couple two unrelated catalog shapes. @@ -40,7 +41,7 @@ * the single source of truth for "do I understand this file?". */ -import type { ClusterCatalog } from '../@types/data/ClusterCatalog'; +import type { StructureCatalog } from '../@types/data/StructureCatalog'; // "CCAT" as a little-endian uint32: // bytes in memory order: C=0x43, C=0x43, A=0x41, T=0x54 @@ -55,7 +56,7 @@ const BYTES_PER_RECORD = 28; // record start — indexed as f+0 … f+5 in the loop bodies below. const OFF_CAT = 24; -export function encodeClusterCatalog(catalog: ClusterCatalog): ArrayBuffer { +export function encodeStructureCatalog(catalog: StructureCatalog): ArrayBuffer { const { count, positions, physicalRadiusMpc, apparentRadiusMpc, significance, category } = catalog; @@ -105,19 +106,19 @@ export function encodeClusterCatalog(catalog: ClusterCatalog): ArrayBuffer { return buf; } -export function decodeClusterCatalog(buf: ArrayBuffer): ClusterCatalog { +export function decodeStructureCatalog(buf: ArrayBuffer): StructureCatalog { const dv = new DataView(buf); if (dv.getUint32(0, true) !== MAGIC) throw new Error('bad magic — not a CCAT file'); // Version mismatch surfaces as the documented "regenerate" error. // Stale .ccat files (built before this format version) trigger this on - // every load until `npm run build-clusters` is re-run. Keep the + // every load until `npm run build-structures` is re-run. Keep the // message instructive — it is the cure. const version = dv.getUint32(4, true); if (version !== VERSION) { throw new Error( - `unsupported cluster-catalog version: ${version} — please regenerate the .ccat via "npm run build-clusters"`, + `unsupported structure-catalog version: ${version} — please regenerate the .ccat via "npm run build-structures"`, ); } @@ -159,7 +160,7 @@ export function decodeClusterCatalog(buf: ArrayBuffer): ClusterCatalog { return { count, positions, physicalRadiusMpc, apparentRadiusMpc, significance, category }; } -export function emptyClusterCatalog(): ClusterCatalog { +export function emptyStructureCatalog(): StructureCatalog { return { count: 0, positions: new Float32Array(0), diff --git a/src/hooks/useStructureMemberCount.ts b/src/hooks/useStructureMemberCount.ts index 27c65c230..0cf51b863 100644 --- a/src/hooks/useStructureMemberCount.ts +++ b/src/hooks/useStructureMemberCount.ts @@ -18,7 +18,7 @@ import { useMemo } from 'react'; import { isPoi } from '../services/engine/isPoi'; -import { structureMemberCount } from '../utils/cluster/structureMemberCount'; +import { structureMemberCount } from '../utils/structure/structureMemberCount'; import type { UseStructureMemberCountInput } from '../@types/engine/UseStructureMemberCountInput'; export function useStructureMemberCount({ diff --git a/src/services/engine/engine.ts b/src/services/engine/engine.ts index 8c7feeeb2..2c22b732f 100644 --- a/src/services/engine/engine.ts +++ b/src/services/engine/engine.ts @@ -113,7 +113,7 @@ import { createLabelDirectorSubsystem } from './subsystems/labelDirectorSubsyste import { registerLabelStyleOverrideWake } from './labelStyleOverride'; import { produceStructureLabels } from './presentation/produceStructureLabels'; import { produceFamousLabels } from './presentation/produceFamousLabels'; -import { createClusterFocusSubsystem } from './subsystems/clusterFocusSubsystem'; +import { createStructureFocusSubsystem } from './subsystems/structureFocusSubsystem'; import { createFpsCounter } from './subsystems/fpsCounter'; import { HDR_PASSES, UI_PASSES } from './frame/passes'; import { buildGalaxyInfo } from './helpers/galaxyInfoBuilder'; @@ -526,7 +526,7 @@ export function createEngine(canvas: HTMLCanvasElement, cb: EngineCallbacks): En markerLineRenderer: null, // null until initGpu; excluded from isEngineReady, null-checked at use. selectionRingRenderer: null, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, // texturedDiskRenderer / proceduralDiskRenderer / milkyWayRenderer: // null until initGpu constructs them. The frame body reads them via // RunFrameDeps; they live here so `destroy()` can reach them and so @@ -633,7 +633,7 @@ export function createEngine(canvas: HTMLCanvasElement, cb: EngineCallbacks): En // Selection-driven: `runFrame` calls `update(selectedPoi, nowMs)` to // drive the 400 ms member-isolation fade and threads // `produceFocusUniforms` into the points draw. Eager, no GPU dep. - clusterFocus: createClusterFocusSubsystem(), + structureFocus: createStructureFocusSubsystem(), // ── Render scheduler — eager, capture-safe ──────────────────── // Created here (not a deferred shim): its `onFrame` closes over the @@ -673,7 +673,7 @@ export function createEngine(canvas: HTMLCanvasElement, cb: EngineCallbacks): En points: new Map(), filaments: null, famousMeta: null, - clusterCatalog: null, + structureCatalog: null, pgcAlias: null, cf4Density: null, // Tier-aware (unlike cf4Density): setTier reloads on tier change. @@ -1211,7 +1211,7 @@ export function createEngine(canvas: HTMLCanvasElement, cb: EngineCallbacks): En state.subsystems.biasCorrection.destroy(); state.subsystems.youAreHere.destroy(); state.subsystems.labelDirector.destroy(); - state.subsystems.clusterFocus.destroy(); + state.subsystems.structureFocus.destroy(); // Impostor teardown order matters: texturedDisks subscribes to // galaxyAtlas's eviction handler (destroy it first); hiResFamous // subscribes to its texture's evict handler (destroy the planner before @@ -1250,8 +1250,8 @@ export function createEngine(canvas: HTMLCanvasElement, cb: EngineCallbacks): En state.gpu.markerLineRenderer = null; state.gpu.selectionRingRenderer?.destroy(); state.gpu.selectionRingRenderer = null; - state.gpu.clusterMarkerRenderer?.destroy(); - state.gpu.clusterMarkerRenderer = null; + state.gpu.structureMarkerRenderer?.destroy(); + state.gpu.structureMarkerRenderer = null; state.gpu.texturedDiskRenderer?.destroy(); state.gpu.texturedDiskRenderer = null; state.gpu.proceduralDiskRenderer?.destroy(); diff --git a/src/services/engine/frame/frameContext.ts b/src/services/engine/frame/frameContext.ts index d3a253f15..fd154f9f4 100644 --- a/src/services/engine/frame/frameContext.ts +++ b/src/services/engine/frame/frameContext.ts @@ -147,7 +147,7 @@ export function deriveFrameContext(state: EngineState, canvas: HTMLCanvasElement // `focusBlend` is seeded to 0 (the at-rest, no-recession value) and then // overwritten by `runFrame` with this frame's real blend the moment the // ready gate passes. It can't be derived here: computing it ticks the - // clusterFocus fade controller, a side effect that must fire exactly + // structureFocus fade controller, a side effect that must fire exactly // once per frame — and `deriveFrameContext` is deliberately pure (it may // be called speculatively, and double-ticking would double-advance the // ramp). So the value is a placeholder until `runFrame` fills it in, diff --git a/src/services/engine/frame/passes/horizonShellPass.ts b/src/services/engine/frame/passes/horizonShellPass.ts index f9d1b3ba8..2f8137455 100644 --- a/src/services/engine/frame/passes/horizonShellPass.ts +++ b/src/services/engine/frame/passes/horizonShellPass.ts @@ -24,7 +24,7 @@ * `draw` and handed to the renderer; both reads use the frame-frozen * `ctx.drawCamPos`, so they agree. * - * ### Why drawn after `volume-upsample` and before `cluster-markers` + * ### Why drawn after `volume-upsample` and before `structure-markers` * * The shell is a background contributor — drawing it after the volume * passes means the cosmic-web densities composite over it cleanly, diff --git a/src/services/engine/frame/passes/index.ts b/src/services/engine/frame/passes/index.ts index 42ed30d33..327367f14 100644 --- a/src/services/engine/frame/passes/index.ts +++ b/src/services/engine/frame/passes/index.ts @@ -33,7 +33,7 @@ * 5. filaments — cosmic-web skeleton overlay * 6. volume-upsample — upsamples the half-res volume offscreen target * into the HDR target (when active fields exist) - * 7. cluster-markers — at-rest halo + ring for cluster / SC / void POIs + * 7. structure-markers — at-rest halo + ring for cluster / SC / void POIs * * `textured-disks` is what remains of the briefly-split (and never-shipped) * `textured-quads` + `textured-disks` pair from 2026-05-18. The quad @@ -105,7 +105,7 @@ import { milkyWayPass } from './milkyWayPass'; import { horizonShellPass } from './horizonShellPass'; import { markerLinesPass } from './markerLinesPass'; import { labelsPass } from './labelsPass'; -import { clusterMarkersPass } from './clusterMarkersPass'; +import { structureMarkersPass } from './structureMarkersPass'; import { selectionRingPass } from './selectionRingPass'; import { diskRadiusRingPass } from './diskRadiusRingPass'; @@ -119,7 +119,7 @@ export const HDR_PASSES: readonly Pass[] = [ flowFieldPass, volumeUpsamplePass, horizonShellPass, - clusterMarkersPass, + structureMarkersPass, ]; /** @@ -177,6 +177,6 @@ export { milkyWayPass } from './milkyWayPass'; export { horizonShellPass } from './horizonShellPass'; export { markerLinesPass } from './markerLinesPass'; export { labelsPass } from './labelsPass'; -export { clusterMarkersPass } from './clusterMarkersPass'; +export { structureMarkersPass } from './structureMarkersPass'; export { selectionRingPass } from './selectionRingPass'; export { diskRadiusRingPass } from './diskRadiusRingPass'; diff --git a/src/services/engine/frame/passes/clusterMarkersPass.ts b/src/services/engine/frame/passes/structureMarkersPass.ts similarity index 68% rename from src/services/engine/frame/passes/clusterMarkersPass.ts rename to src/services/engine/frame/passes/structureMarkersPass.ts index 3898ebc96..94eb4df49 100644 --- a/src/services/engine/frame/passes/clusterMarkersPass.ts +++ b/src/services/engine/frame/passes/structureMarkersPass.ts @@ -1,6 +1,6 @@ /** - * clusterMarkersPass — halo + ring draws for cluster / supercluster / - * void POIs. + * structureMarkersPass — halo + ring draws for every structure category + * (cluster / supercluster / void / group). * * Lives in `HDR_PASSES` (NOT `UI_PASSES`) because halos are additive * emissive content — they participate in tone-map alongside point @@ -13,9 +13,9 @@ * (in UI_PASSES) still draw on top of everything HDR via the post- * tone-map overlay pass. * - * Enabled when: clusterMarkerRenderer is non-null AND has at least + * Enabled when: structureMarkerRenderer is non-null AND has at least * one marker queued for this frame. When the camera is sufficiently - * far that every POI's apparent ring is sub-pixel, the renderer + * far that every ring is sub-pixel, the renderer * still emits descriptors (the per-pixel fragment write degenerates * to ~zero alpha) — this is intentional, keeps the pass cheap and * uniformly enabled. @@ -23,23 +23,23 @@ import type { Pass } from '../../../../@types/engine/frame/Pass'; -export const clusterMarkersPass: Pass = { - name: 'cluster-markers', +export const structureMarkersPass: Pass = { + name: 'structure-markers', enabled(state, _ctx, _settings) { - if (state.gpu.clusterMarkerRenderer === null) return false; - return state.gpu.clusterMarkerRenderer.markerCount() > 0; + if (state.gpu.structureMarkerRenderer === null) return false; + return state.gpu.structureMarkerRenderer.markerCount() > 0; }, draw(pass, ctx, state, _settings, _deps) { - // fadeOpacity = 1 at v1 — the cluster-markers layer has no + // fadeOpacity = 1 at v1 — the structure-markers layer has no // FadeRegistry handle yet. The renderer still binds a real fade // group at @group(1) so the BGL matches what filaments (and other // HDR passes) bind at the same slot on the shared encoder. A - // future opacityOf({kind:'clusterMarkers'}, nowMs) substitution + // future opacityOf({kind:'structureMarkers'}, nowMs) substitution // would let the layer animate in/out via the unified fade // architecture (see lib/fadeUniforms.wesl module header). - state.gpu.clusterMarkerRenderer!.render( + state.gpu.structureMarkerRenderer!.render( pass, ctx.vp as Float32Array, [ctx.canvasSize.width, ctx.canvasSize.height], diff --git a/src/services/engine/frame/runFrame.ts b/src/services/engine/frame/runFrame.ts index b6a2a2c94..faf46fdec 100644 --- a/src/services/engine/frame/runFrame.ts +++ b/src/services/engine/frame/runFrame.ts @@ -182,7 +182,7 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): return; } - // ── Cluster-focus recession (computed ONCE, EARLY) ──────────────── + // ── Structure-focus recession (computed ONCE, EARLY) ──────────────── // // Focus mode fades non-member galaxies away when a cluster / // supercluster / void / group POI is focused. Resolve the FOCUSED POI @@ -204,8 +204,8 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): focusSel !== null && focusSel.kind === 'structure' ? (state.data.structures.byId(focusSel.id) ?? null) : null; - state.subsystems.clusterFocus.update(focusedStructure, nowMs); - const focusUniforms = state.subsystems.clusterFocus.produceFocusUniforms(nowMs); + state.subsystems.structureFocus.update(focusedStructure, nowMs); + const focusUniforms = state.subsystems.structureFocus.produceFocusUniforms(nowMs); ctx.focusBlend = focusUniforms.blend; // ── Per-frame impostor planners ─────────────────────────────────── @@ -261,10 +261,10 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): // Like the label flush above: produceStructureMarkers walks the structure // store, applies fade math, and hands descriptors to the renderer. Must run // BEFORE the GPU dispatch so the instance buffer is uploaded before - // clusterMarkersPass reads it. Null-checked for the pre-initGpu window. - if (state.gpu.clusterMarkerRenderer !== null) { + // structureMarkersPass reads it. Null-checked for the pre-initGpu window. + if (state.gpu.structureMarkerRenderer !== null) { const markers = produceStructureMarkers(state, ctx); - state.gpu.clusterMarkerRenderer.setMarkers(markers); + state.gpu.structureMarkerRenderer.setMarkers(markers); } // ── GPU dispatch ────────────────────────────────────────────────── @@ -341,7 +341,7 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): const { visibleSources: overlaySources, hasAny } = collectPickTargets( ctx.renderer, state.sources.pickMask, - state.gpu.clusterMarkerRenderer, + state.gpu.structureMarkerRenderer, ); if (hasAny) { const pickTex = state.gpu.pickRenderer.renderForDebug( @@ -408,11 +408,11 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): const { visibleSources, hasAny } = collectPickTargets( ctx.renderer, state.sources.pickMask, - state.gpu.clusterMarkerRenderer, + state.gpu.structureMarkerRenderer, ); if (!hasAny) { // Nothing pickable (every survey off AND no cluster ring visible). - // Let the loop sleep — the next setSourceVisible / cluster-marker + // Let the loop sleep — the next setSourceVisible / structure-marker // change wakes it. This `return` skips the keep-rendering predicate // at the tail, which is correct: with nothing pickable there's // nothing to animate, so the predicate would return false anyway. @@ -475,7 +475,7 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): // - fades.isAnyAnimating(): a survey / filament handle is ramping its // opacity from a recent upload (the FadeRegistry owns every clock, // filaments included). - // - clusterFocus.isAwake(): the member-isolation fade (its own + // - structureFocus.isAwake(): the member-isolation fade (its own // controller, not in the registry) across the 400 ms ramp. // - flowFieldRenderer.isAnimating(): the flow layer is enabled + loaded; // both modes animate (advect drifts, streamline pulses), so the loop @@ -496,7 +496,7 @@ export function runFrame(state: EngineState, deps: RunFrameDeps, nowMs: number): state.subsystems.spaceMouse.hasAxes() || (ready && state.subsystems.texturedDisks.hasInFlightWork()) || state.subsystems.fades.isAnyAnimating(nowMs) || - state.subsystems.clusterFocus.isAwake(nowMs) || + state.subsystems.structureFocus.isAwake(nowMs) || state.gpu.flowFieldRenderer?.isAnimating(state.settings.flow) === true; if (stillAnimating) state.subsystems.scheduler.requestRender(); } diff --git a/src/services/engine/helpers/collectPickTargets.ts b/src/services/engine/helpers/collectPickTargets.ts index 0102ef96b..0233aff6b 100644 --- a/src/services/engine/helpers/collectPickTargets.ts +++ b/src/services/engine/helpers/collectPickTargets.ts @@ -11,28 +11,25 @@ * * ### Why a helper rather than the gate repeated at each site * - * Before the bulk cluster catalog landed, part (b) was simply - * "visibleSources is non-empty", written as a bare `length === 0` bail - * at each of the three sites. Clusters then became independently - * pickable: `clusterMarkerRenderer.pickRing` draws cluster / SC / void - * rings into the SAME pick texture, but they are NOT galaxy surveys and - * never appear in `loadedSources()`. So the old gate made every cluster - * unpickable — and the pick-debug overlay black — the instant all galaxy - * surveys were toggled off. Patching the extra `|| hasClusterMarkers` - * clause into three (four, counting the renderer's own guard) gates - * would have re-spread the same logic. Folding both parts into one - * helper keeps the "is anything pickable" rule in exactly one place: a - * future pickable layer is added here once and all callers inherit it. + * Structure markers are independently pickable: `structureMarkerRenderer.pickRing` + * draws cluster / supercluster / void / group rings into the SAME pick + * texture, but they are NOT galaxy surveys and never appear in + * `loadedSources()`. A naive "visibleSources is non-empty" gate would + * make every structure ring unpickable — and the pick-debug overlay + * black — the instant all galaxy surveys are toggled off. Folding both + * parts into one helper keeps the "is anything pickable" rule in exactly + * one place: a future pickable layer is added here once and all callers + * inherit it. * - * `markerCount()` mirrors `clusterMarkersPass`'s enable gate — it drops - * to 0 when the cluster category is hidden or every ring has faded out, - * so `hasAny` correctly reflects whether a cluster ring is actually on + * `markerCount()` mirrors `structureMarkersPass`'s enable gate — it drops + * to 0 when every structure category is hidden or every ring has faded + * out, so `hasAny` correctly reflects whether a ring is actually on * screen to be hit. */ import type { PointRenderer } from '../../../@types/rendering/PointRenderer'; import type { PickSourceDraw } from '../../../@types/rendering/PickSourceDraw'; -import type { ClusterMarkerRenderer } from '../../../@types/rendering/ClusterMarkerRenderer'; +import type { StructureMarkerRenderer } from '../../../@types/rendering/StructureMarkerRenderer'; export type PickTargets = { /** Loaded galaxy surveys whose pick-mask bit is set, in `Source` enum order. */ @@ -47,12 +44,12 @@ export type PickTargets = { export function collectPickTargets( renderer: PointRenderer, pickMask: number, - clusterMarkerRenderer: ClusterMarkerRenderer | null, + structureMarkerRenderer: StructureMarkerRenderer | null, ): PickTargets { const visibleSources = Array.from(renderer.loadedSources()).filter( (s) => ((pickMask >> s.source) & 1) !== 0, ); - const hasClusterMarkers = - clusterMarkerRenderer !== null && clusterMarkerRenderer.markerCount() > 0; - return { visibleSources, hasAny: visibleSources.length > 0 || hasClusterMarkers }; + const hasStructureMarkers = + structureMarkerRenderer !== null && structureMarkerRenderer.markerCount() > 0; + return { visibleSources, hasAny: visibleSources.length > 0 || hasStructureMarkers }; } diff --git a/src/services/engine/helpers/resolvePoiFromPick.ts b/src/services/engine/helpers/resolvePoiFromPick.ts index 28e62184c..7a7cdba73 100644 --- a/src/services/engine/helpers/resolvePoiFromPick.ts +++ b/src/services/engine/helpers/resolvePoiFromPick.ts @@ -5,13 +5,13 @@ * * ### Contract (inherited from the cluster-viz plan-3 pick path) * - * `clusterMarkerRenderer.pickRing` issues one instanced draw per + * `structureMarkerRenderer.pickRing` issues one instanced draw per * category (cluster / supercluster / void) with `firstInstance` set to * that category's bucket offset; the fragment packs * `@builtin(instance_index) - bucketOffset` worth of slot info into the * pick texture as `poiIndex`. `unpackPick` already returns the * per-category-local 0-based index (see `selectionEncoding.ts` and the - * dispatch comment in `clusterMarkerRenderer.pickRing`), so the array + * dispatch comment in `structureMarkerRenderer.pickRing`), so the array * `structures.byCategory(cat)` is the canonical lookup. * * ### Why the array lookup is safe diff --git a/src/services/engine/phases/initGpu.ts b/src/services/engine/phases/initGpu.ts index a2ad13a48..434c06ef8 100644 --- a/src/services/engine/phases/initGpu.ts +++ b/src/services/engine/phases/initGpu.ts @@ -53,7 +53,7 @@ import { createFilamentRenderer } from '../../gpu/renderers/filamentRenderer'; import { createLabelRenderer } from '../../gpu/renderers/labelRenderer'; import { createMarkerLineRenderer } from '../../gpu/renderers/markerLineRenderer'; import { createSelectionRingRenderer } from '../../gpu/renderers/selectionRingRenderer'; -import { createClusterMarkerRenderer } from '../../gpu/renderers/clusterMarkerRenderer'; +import { createStructureMarkerRenderer } from '../../gpu/renderers/structureMarkerRenderer'; import { createScalarVolumeRenderer } from '../../gpu/renderers/scalarVolumeRenderer'; import { createFlowFieldRenderer } from '../../gpu/renderers/flowFieldRenderer'; import { createVolumeUpsample } from '../../gpu/passes/volumeUpsample'; @@ -197,7 +197,7 @@ export async function initGpu(state: EngineState, deps: BootstrapDeps): Promise< // swap chain. The fadeBgl placeholder at @group(1) must match what the // other HDR passes (filaments) bind at the same slot on the shared // RenderPassEncoder; see the renderer's pipeline-layout comment. - state.gpu.clusterMarkerRenderer = createClusterMarkerRenderer( + state.gpu.structureMarkerRenderer = createStructureMarkerRenderer( uiCtx, 'rgba16float', state.gpu.fadeBgl!, diff --git a/src/services/engine/phases/clusterCatalogToStructures.ts b/src/services/engine/phases/structureCatalogToStructures.ts similarity index 95% rename from src/services/engine/phases/clusterCatalogToStructures.ts rename to src/services/engine/phases/structureCatalogToStructures.ts index 203aa905e..bc880c0bd 100644 --- a/src/services/engine/phases/clusterCatalogToStructures.ts +++ b/src/services/engine/phases/structureCatalogToStructures.ts @@ -1,12 +1,12 @@ /** - * clusterCatalogToStructures — assemble the bulk (non-featured) + * structureCatalogToStructures — assemble the bulk (non-featured) * cluster/supercluster `StructureRecord`s from the decoded `.ccat` catalog + * its meta sidecar. * * ### Why a separate module * * A pure transform that `wireStructureProjection` installs into the structure - * store's `bulk` group once the cluster-catalog slot lands, kept out of the + * store's `bulk` group once the structure-catalog slot lands, kept out of the * wiring so it's unit-testable without booting the engine. Every record here * is `featured: false` — these ~375 structures render through the ring/halo * marker pass, NOT the @@ -50,7 +50,7 @@ * them as non-deep-linkable (the URL drain only resolves featured ids). */ -import type { ClusterCatalogPayload } from '../../../@types/loading/ClusterCatalogPayload'; +import type { StructureCatalogPayload } from '../../../@types/loading/StructureCatalogPayload'; import type { StructureRecord } from '../../../@types/engine/data/StructureRecord'; import type { Vec3 } from '../../../@types/math/Vec3'; @@ -102,7 +102,7 @@ function makeNormaliser( return (raw: number) => (transform(raw) - min) / span; } -export function clusterCatalogToStructures(payload: ClusterCatalogPayload): StructureRecord[] { +export function structureCatalogToStructures(payload: StructureCatalogPayload): StructureRecord[] { const { catalog, meta } = payload; if (catalog.count === 0) return []; diff --git a/src/services/engine/phases/wireInput.ts b/src/services/engine/phases/wireInput.ts index 340a888f2..cb61b59a8 100644 --- a/src/services/engine/phases/wireInput.ts +++ b/src/services/engine/phases/wireInput.ts @@ -60,7 +60,7 @@ export async function wireInput(state: EngineState, deps: BootstrapDeps): Promis // The live shared focus buffer — so the pick pass excludes non-members // of a focused structure from hit-testing (vertex shader culls them). state.gpu.focusUniform!.bindGroup, - state.gpu.clusterMarkerRenderer ?? undefined, + state.gpu.structureMarkerRenderer ?? undefined, ); state.gpu.pickRenderer = pickRenderer; // The resolver hands back the freshly-decoded `(source, localIdx)` @@ -201,7 +201,7 @@ export async function wireInput(state: EngineState, deps: BootstrapDeps): Promis const { visibleSources, hasAny } = collectPickTargets( r, state.sources.pickMask, - state.gpu.clusterMarkerRenderer, + state.gpu.structureMarkerRenderer, ); if (!hasAny) return null; diff --git a/src/services/engine/phases/wireSlots.ts b/src/services/engine/phases/wireSlots.ts index a018d6ef7..261da9a38 100644 --- a/src/services/engine/phases/wireSlots.ts +++ b/src/services/engine/phases/wireSlots.ts @@ -34,7 +34,7 @@ * * ### State writes * - * - `state.assetSlots.{filaments,famousMeta,clusterCatalog,pgcAlias, + * - `state.assetSlots.{filaments,famousMeta,structureCatalog,pgcAlias, * cf4Density,mcpm}` (via `installSlots`) + `.syntheticVolumes` (DEV). * - `state.subsystems.{loadProgress, pois}` + the impostor subsystem handles. * - `state.requests` may gain `'syntheticFallback'` (via the gate). diff --git a/src/services/engine/presentation/focusRecession.ts b/src/services/engine/presentation/focusRecession.ts index 8b8b0a8b7..ddc78dd1a 100644 --- a/src/services/engine/presentation/focusRecession.ts +++ b/src/services/engine/presentation/focusRecession.ts @@ -15,7 +15,7 @@ * factor lives HERE, as a stateless function of the handle and the focus * `blend`. We deliberately do NOT fold recession into the registry * (no `setFocusBlend`, no `toggle × recession` baked into `opacityOf`): - * the blend's authoritative home is `clusterFocusSubsystem` + * the blend's authoritative home is `structureFocusSubsystem` * (`FocusUniformsValue.blend`), and caching it in the registry would be a * value×place mirror (the stale-mirror bug class). Toggle fade and focus * recession vary independently, so they are *composed* at the consumer, diff --git a/src/services/engine/presentation/produceStructureMarkers.ts b/src/services/engine/presentation/produceStructureMarkers.ts index fce4f6185..83f5ebf5a 100644 --- a/src/services/engine/presentation/produceStructureMarkers.ts +++ b/src/services/engine/presentation/produceStructureMarkers.ts @@ -30,7 +30,7 @@ import type { ReadyFrameContext } from '../../../@types/engine/frame/ReadyFrameContext'; import type { EngineState } from '../../../@types/engine/state/EngineState'; import type { Vec4 } from '../../../@types/math/Vec4'; -import type { ClusterMarkerDescriptor } from '../../../@types/rendering/ClusterMarkerDescriptor'; +import type { StructureMarkerDescriptor } from '../../../@types/rendering/StructureMarkerDescriptor'; import { STRUCTURE_POI_STYLES, SIG_MIN_ALPHA } from './structurePoiStyles'; import { focusRecession } from './focusRecession'; import { structureIdOf } from '../helpers/structureIdOf'; @@ -38,8 +38,8 @@ import { structureIdOf } from '../helpers/structureIdOf'; export function produceStructureMarkers( state: EngineState, ctx: ReadyFrameContext, -): readonly ClusterMarkerDescriptor[] { - const out: ClusterMarkerDescriptor[] = []; +): readonly StructureMarkerDescriptor[] { + const out: StructureMarkerDescriptor[] = []; const halfH = ctx.canvasSize.height * 0.5; const fovYRad = 2 * Math.atan(halfH / ctx.drawPxPerRad); const pxPerRad = (ctx.canvasSize.height * 0.5) / Math.tan(fovYRad * 0.5); diff --git a/src/services/engine/subsystems/clusterFocusSubsystem.ts b/src/services/engine/subsystems/structureFocusSubsystem.ts similarity index 93% rename from src/services/engine/subsystems/clusterFocusSubsystem.ts rename to src/services/engine/subsystems/structureFocusSubsystem.ts index a6c58b04d..651d8d74d 100644 --- a/src/services/engine/subsystems/clusterFocusSubsystem.ts +++ b/src/services/engine/subsystems/structureFocusSubsystem.ts @@ -1,5 +1,5 @@ /** - * clusterFocusSubsystem — focus-driven cluster "focus mode". + * structureFocusSubsystem — focus-driven structure "focus mode". * * When a cluster / supercluster / void / group POI is focused, non-member * galaxies fade to ~8% alpha over ~400 ms (the shader does the @@ -7,7 +7,7 @@ * radius, and the smoothstep blend). All four categories behave * identically — the focused structure's interior galaxies stay bright; * voids are just an underdense case of the same rule. See - * `ClusterFocusSubsystem.d.ts` for the rationale (focus as single + * `StructureFocusSubsystem.d.ts` for the rationale (focus as single * source of truth; GPU re-derivation instead of a CPU member list). * * ### Why a `focusedId` separate from the display target @@ -24,7 +24,7 @@ */ import { createFadeController } from '../../animation/fadeController'; -import type { ClusterFocusSubsystem } from '../../../@types/engine/subsystems/ClusterFocusSubsystem'; +import type { StructureFocusSubsystem } from '../../../@types/engine/subsystems/StructureFocusSubsystem'; import type { StructureRecord } from '../../../@types/engine/data/StructureRecord'; import type { FocusUniformsValue } from '../../../@types/rendering/FocusUniformsValue'; import type { Vec3 } from '../../../@types/math/Vec3'; @@ -53,9 +53,9 @@ type ActiveFocus = { readonly physicalRadiusMpc: number; }; -export function createClusterFocusSubsystem( +export function createStructureFocusSubsystem( initialNowMs: number = performance.now(), -): ClusterFocusSubsystem { +): StructureFocusSubsystem { const fade = createFadeController(0, initialNowMs); // The POI we emit centre/radius for. Latched through fade-out. let active: ActiveFocus | null = null; @@ -121,7 +121,7 @@ export function createClusterFocusSubsystem( } return { - id: 'clusterFocus', + id: 'structureFocus', update, produceFocusUniforms, isAwake, diff --git a/src/services/engine/wiring/assetWiring.ts b/src/services/engine/wiring/assetWiring.ts index d139c18ac..2321a12d4 100644 --- a/src/services/engine/wiring/assetWiring.ts +++ b/src/services/engine/wiring/assetWiring.ts @@ -24,7 +24,7 @@ * * - **filaments** gates on `settings.filaments.enabled` — the real master * toggle, so a disabled filament overlay never fetches the skeleton. - * - **clusterCatalog** gates on structure-category visibility: it loads when + * - **structureCatalog** gates on structure-category visibility: it loads when * ANY of the cluster / supercluster / void categories is visible in either * the marker or the label overlay. There is no `settings.structures.enabled` * flag — structures are controlled per-category via the two visibility @@ -36,7 +36,7 @@ * ### What is NOT a row here * * - **Cluster / Supercluster / Void** `Source`s — they have no individual - * fetch; their geometry arrives via the single `'clusterCatalog'` row. + * fetch; their geometry arrives via the single `'structureCatalog'` row. * - **DEV synthetic volumes** (`debug-gaussian` / `-cartesian` / `-spherical`) * — minted only under `import.meta.env.DEV` via `createSyntheticVolumeSlots` * and triggered there. Keeping them out of the production table lets Vite @@ -58,7 +58,7 @@ import type { PoiCategory } from '../../../@types/engine/data/PoiCategory'; import { Source, SOURCE_REGISTRY } from '../../../data/sources'; import { createFilamentSlot } from '../../loading/slots/filamentSlot'; import { createFamousMetaSlot } from '../../loading/slots/famousMetaSlot'; -import { createClusterCatalogSlot } from '../../loading/slots/clusterCatalogSlot'; +import { createStructureCatalogSlot } from '../../loading/slots/structureCatalogSlot'; import { createCf4DensitySlot } from '../../loading/slots/cf4DensitySlot'; import { createFlowFieldSlot } from '../../loading/slots/flowFieldSlot'; import { createMcpmSlot } from '../../loading/slots/mcpmSlot'; @@ -67,7 +67,7 @@ import type { SourceType } from '../../../@types/data/SourceType'; /** * The categories backed by the bulk `.ccat` catalog — their visibility - * gates the cluster-catalog fetch. `famousGalaxy` is excluded (Famous + * gates the structure-catalog fetch. `famousGalaxy` is excluded (Famous * `.bin` + meta sidecar), and `group` is excluded (seed-only, no `.ccat` * — adding it here would trigger a pointless fetch when group visibility * toggles). Spelled as `PoiCategory` members so a type error surfaces @@ -180,8 +180,8 @@ export const ASSET_WIRING: readonly AssetWiringRow[] = [ // category is visible in either the marker or label overlay. Empty // request — the .ccat is a standalone boot asset. { - key: 'clusterCatalog', - factory: (deps) => createClusterCatalogSlot(deps.state, deps.cb), + key: 'structureCatalog', + factory: (deps) => createStructureCatalogSlot(deps.state, deps.cb), req: () => ({}), demand: (ctx) => BULK_CATALOG_CATEGORIES.some( diff --git a/src/services/engine/wiring/installLoadProgress.ts b/src/services/engine/wiring/installLoadProgress.ts index b053506bd..78ef64952 100644 --- a/src/services/engine/wiring/installLoadProgress.ts +++ b/src/services/engine/wiring/installLoadProgress.ts @@ -39,7 +39,7 @@ export function installLoadProgress(state: EngineState, deps: BootstrapDeps): vo const sidecars = [ state.assetSlots.filaments, state.assetSlots.famousMeta, - state.assetSlots.clusterCatalog, + state.assetSlots.structureCatalog, state.assetSlots.pgcAlias, state.assetSlots.cf4Density, state.assetSlots.mcpm, diff --git a/src/services/engine/wiring/wireStructureProjection.ts b/src/services/engine/wiring/wireStructureProjection.ts index 945063522..a5bdd68b9 100644 --- a/src/services/engine/wiring/wireStructureProjection.ts +++ b/src/services/engine/wiring/wireStructureProjection.ts @@ -8,7 +8,7 @@ * - `anchors` — hand-curated cluster/SC/void anchors from * `buildStaticAnchorStructures`. Published synchronously at boot so the * Structures panel has counts from frame 1. - * - `bulk` — built from the cluster-catalog slot's ready value when it + * - `bulk` — built from the structure-catalog slot's ready value when it * lands (a single subscription). A slot error clears the group * (graceful degradation — bulk structures don't appear but the engine * continues normally). @@ -29,7 +29,7 @@ */ import { buildStaticAnchorStructures } from '../../../data/buildStaticAnchorStructures'; -import { clusterCatalogToStructures } from '../phases/clusterCatalogToStructures'; +import { structureCatalogToStructures } from '../phases/structureCatalogToStructures'; import type { EngineState } from '../../../@types/engine/state/EngineState'; import type { EngineCallbacks } from '../../../@types/engine/EngineCallbacks'; @@ -37,7 +37,7 @@ import type { EngineCallbacks } from '../../../@types/engine/EngineCallbacks'; /** * Wire the structure groups into the `structureStore`. * - * Precondition: `state.assetSlots.clusterCatalog` is minted by `wireSlots` + * Precondition: `state.assetSlots.structureCatalog` is minted by `wireSlots` * before this function is called. */ export function wireStructureProjection(state: EngineState, cb: EngineCallbacks): void { @@ -68,12 +68,12 @@ export function wireStructureProjection(state: EngineState, cb: EngineCallbacks) // ── Group 2: bulk clusters/superclusters ───────────────────────────── // - // The bulk records come straight off the cluster-catalog slot's ready + // The bulk records come straight off the structure-catalog slot's ready // value. A slot error clears the group (graceful degradation — bulk // structures don't appear but the engine continues normally). - state.assetSlots.clusterCatalog?.subscribe((s) => { + state.assetSlots.structureCatalog?.subscribe((s) => { if (s.kind === 'ready') { - state.data.structures.setGroup('bulk', clusterCatalogToStructures(s.value)); + state.data.structures.setGroup('bulk', structureCatalogToStructures(s.value)); } else if (s.kind === 'error') { state.data.structures.clearGroup('bulk'); } diff --git a/src/services/gpu/passes/pickDebugOverlay.ts b/src/services/gpu/passes/pickDebugOverlay.ts index 3df0f9e3d..4ffb53be2 100644 --- a/src/services/gpu/passes/pickDebugOverlay.ts +++ b/src/services/gpu/passes/pickDebugOverlay.ts @@ -60,7 +60,7 @@ export function createPickDebugOverlay( { format: swapChainFormat, // Premultiplied OVER — same convention as markerLines, - // labels, clusterMarker. Fragment shader emits + // labels, structureMarker. Fragment shader emits // 'vec4(col * alpha, alpha)' so this blend produces // the standard "src on top of dst" composite. Background // pixels emit alpha = 0, which evaluates to a no-op blend diff --git a/src/services/gpu/renderers/pickRenderer.ts b/src/services/gpu/renderers/pickRenderer.ts index f8d91d775..f96492dec 100644 --- a/src/services/gpu/renderers/pickRenderer.ts +++ b/src/services/gpu/renderers/pickRenderer.ts @@ -33,7 +33,7 @@ import type { PointRenderer } from '../../../@types/rendering/PointRenderer'; import type { FadeUniformsBgl } from '../../../@types/rendering/FadeUniformsBgl'; import type { SourceUniformsBgl } from '../../../@types/rendering/SourceUniformsBgl'; import type { FocusUniformsBgl } from '../../../@types/rendering/FocusUniformsBgl'; -import type { ClusterMarkerRenderer } from '../../../@types/rendering/ClusterMarkerRenderer'; +import type { StructureMarkerRenderer } from '../../../@types/rendering/StructureMarkerRenderer'; import { POINT_STRIDE, POINT_VERTEX_ATTRIBUTES, @@ -74,13 +74,13 @@ export function createPickRenderer( // shader can cull non-members of a focused structure from hit-testing. focusBindGroup: GPUBindGroup, // Optional POI-ring pick provider. When present, the pick pass - // calls `clusterMarkerRenderer.pickRing(pass)` after the galaxy + // calls `structureMarkerRenderer.pickRing(pass)` after the galaxy // draws so cluster / supercluster / void ring hits land in the same // texture. Shared depth state means a foreground galaxy still // claims the pixel — clicks through a ring select the galaxy. // Optional so tests can construct the picker in isolation; passing // `undefined` yields a galaxy-only pick pass. - clusterMarkerRenderer?: ClusterMarkerRenderer, + structureMarkerRenderer?: StructureMarkerRenderer, ): PickRenderer { const vsModule = createShaderModuleWithDevLog(device, vsCode, 'pick.vertex'); const fsModule = createShaderModuleWithDevLog(device, pickFsCode, 'pick.pickFragment'); @@ -95,7 +95,11 @@ export function createPickRenderer( device.createBindGroupLayout({ label: 'pick-bgl-group0', entries: [ - { binding: 0, visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } }, + { + binding: 0, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform' }, + }, ], }), fadeBgl, @@ -329,8 +333,8 @@ export function createPickRenderer( // POI ring picks share depth state with the galaxy draws, so a // foreground galaxy claims the pixel — clicks through a ring at a // galaxy select the galaxy. Skipped when no marker renderer. - if (clusterMarkerRenderer) { - clusterMarkerRenderer.pickRing(pass); + if (structureMarkerRenderer) { + structureMarkerRenderer.pickRing(pass); } pass.end(); @@ -339,15 +343,15 @@ export function createPickRenderer( // Whether this pick pass has anything to draw — galaxy sources OR // cluster / SC / void ring markers (drawn by - // `clusterMarkerRenderer.pickRing` inside `recordPickPass`). Shared + // `structureMarkerRenderer.pickRing` inside `recordPickPass`). Shared // by `pick` and `renderForDebug` so a galaxy-empty scene with visible // rings still picks (and the pick-debug texture isn't black when every // survey is toggled off). `markerCount() > 0` mirrors - // `clusterMarkersPass`'s enable gate (0 when the category is hidden or + // `structureMarkersPass`'s enable gate (0 when the category is hidden or // every ring has faded out). const hasAnyPickTarget = (sourceList: readonly PickSourceDraw[]): boolean => sourceList.length > 0 || - (clusterMarkerRenderer !== undefined && clusterMarkerRenderer.markerCount() > 0); + (structureMarkerRenderer !== undefined && structureMarkerRenderer.markerCount() > 0); async function pick( viewportPx: Vec2, diff --git a/src/services/gpu/renderers/clusterMarkerRenderer.ts b/src/services/gpu/renderers/structureMarkerRenderer.ts similarity index 82% rename from src/services/gpu/renderers/clusterMarkerRenderer.ts rename to src/services/gpu/renderers/structureMarkerRenderer.ts index 83f343082..9b9701216 100644 --- a/src/services/gpu/renderers/clusterMarkerRenderer.ts +++ b/src/services/gpu/renderers/structureMarkerRenderer.ts @@ -1,32 +1,32 @@ /** - * clusterMarkerRenderer — instanced halo + ring overlay for cluster / - * supercluster / void POIs. + * structureMarkerRenderer — instanced halo + ring overlay for every + * `type:'structure'` category: cluster, supercluster, void, and group. + * The producer (`produceStructureMarkers`) feeds it descriptors; the + * store it visualises is `state.data.structures`. * - * ### Why one renderer for two pipelines? + * ### Why one renderer for three pipelines? * - * Halos and rings share the same per-POI instance data (position, - * radius, tints, alphas) and the same camera uniform; only the - * fragment math differs (additive radial gradient vs. screen-AA ring). - * One renderer that owns both pipelines + one shared instance vertex - * buffer lets `setMarkers` upload once per frame and dispatch two - * draws — versus two factory call sites maintaining two parallel - * instance buffers. + * Halo, ring, and ringPick share the same per-instance data (position, + * radius, tints, alphas) and the same camera uniform; only the fragment + * math differs (additive radial gradient vs. screen-AA ring vs. pick + * encode). One renderer owning all three pipelines + one shared + * instance buffer lets `setMarkers` upload once per frame, versus + * parallel instance buffers per pipeline. * - * ### Why one draw per category (cluster / supercluster / void)? + * ### One draw per category * - * The marker renderer pre-architects for plan 3's pick fragment. - * Plan 3 will add a `ringPick.wesl` whose fragment composes - * `(source.sourceCode << 27) | poiIndex + PICK_SENTINEL_OFFSET` from - * a per-source uniform — identical to `pointRenderer`'s per-survey - * uniform pattern. Issuing one draw per category here (with the - * per-category SourceUniforms bound at `@group(2)`) means plan 3 - * adds the pick pipeline without re-shaping how descriptors are - * batched. + * `render`/`pickRing` partition descriptors by category and issue one + * instanced draw per non-empty bucket, binding that category's + * SourceUniforms at `@group(2)`. The uniform carries the category's + * 5-bit `sourceCode`, which the ringPick fragment composes into + * `(sourceCode << 27) | poiIndex + PICK_SENTINEL_OFFSET` — the same + * per-source pattern `pointRenderer` uses per survey. Buckets are + * data-driven from `STRUCTURE_CATEGORIES`, so a new structure source + * needs no change here. * - * Voids skip the halo draw entirely (per the spec — a halo would - * imply matter where the structure is defined by absence). The - * descriptor's `haloAlpha === 0` is the gate; descriptors flow into - * the partition but the halo draw for the void bucket is skipped. + * Voids skip the halo draw — a halo implies matter where the structure + * is defined by absence. The descriptor's `haloColor` alpha 0 is the + * gate; the descriptor still flows into the partition for ring + pick. * * ### CPU-only mode * @@ -35,52 +35,37 @@ * CPU scratch buffer + bumps the counter without touching the GPU. * Mirrors `markerLineRenderer.ts`'s null-device pattern. * - * ### Pipeline shape (Task 7) + * ### Pipeline layout * - * Two pipelines built from one module each (never share GPUShaderModule - * across pipelines — WebGPU layout: 'auto' bites otherwise; see the - * MEMORY note `feedback_webgpu_auto_layout_trap.md`): - * - * - Halo: additive blend (one, one), vertex 'vs' + fragment 'fs' - * from halo.wesl - * - Ring: premultiplied-OVER blend, vertex 'vs' + fragment 'fs' - * from ring.wesl - * - * Both pipelines share an EXPLICIT pipeline layout — not 'auto' — - * built from one CameraUniforms BGL (`@group(0)`) and one - * SourceUniforms BGL (`@group(2)`). An explicit shared layout means - * one `device.createBindGroup(...)` is valid against both pipelines - * (which `layout: 'auto'` does NOT guarantee). - * - * ### Per-category source uniforms (pre-architects pick path) - * - * Three pre-built SourceUniforms buffers (one each for cluster=5, - * supercluster=6, void=7). The `render` method partitions descriptors - * by category, binds the matching SourceUniforms, and issues one - * instanced draw per non-empty bucket. Plan 3's pick pipeline will - * reuse this same per-category dispatch — its ringPick fragment reads - * `source.sourceCode` to compose `(sourceCode << 27) | poiIndex + 1`. + * Each pipeline gets its own GPUShaderModule (never share one across + * pipelines — WebGPU `layout:'auto'` bites otherwise; see the MEMORY + * note `feedback_webgpu_auto_layout_trap.md`). All share an EXPLICIT + * pipeline layout — CameraUniforms BGL at `@group(0)`, a placeholder + * FadeUniforms BGL at `@group(1)` (unused by these shaders but it must + * match whatever a prior HDR pass left bound at slot 1), SourceUniforms + * BGL at `@group(2)` — so one `device.createBindGroup(...)` is valid + * against every pipeline (which `layout:'auto'` does NOT guarantee). */ import type { GpuContext } from '../../../@types/rendering/GpuContext'; import type { Renderer } from '../../../@types/rendering/Renderer'; -import type { ClusterMarkerRenderer } from '../../../@types/rendering/ClusterMarkerRenderer'; -import type { ClusterMarkerDescriptor } from '../../../@types/rendering/ClusterMarkerDescriptor'; +import type { StructureMarkerRenderer } from '../../../@types/rendering/StructureMarkerRenderer'; +import type { StructureMarkerDescriptor } from '../../../@types/rendering/StructureMarkerDescriptor'; import type { FadeUniformsBgl } from '../../../@types/rendering/FadeUniformsBgl'; import { STRUCTURE_CATEGORIES, STRUCTURE_CATEGORY_CODES } from '../../../data/structureCategories'; import type { StructureCategory } from '../../../@types/engine/data/StructureCategory'; -import haloVsCode from '../shaders/clusterMarker/halo.wesl?static'; -import haloFsCode from '../shaders/clusterMarker/halo.wesl?static'; -import ringVsCode from '../shaders/clusterMarker/ring.wesl?static'; -import ringFsCode from '../shaders/clusterMarker/ring.wesl?static'; -import ringPickVsCode from '../shaders/clusterMarker/ring.wesl?static'; -import ringPickFsCode from '../shaders/clusterMarker/ringPick.wesl?static'; +import haloVsCode from '../shaders/structureMarker/halo.wesl?static'; +import haloFsCode from '../shaders/structureMarker/halo.wesl?static'; +import ringVsCode from '../shaders/structureMarker/ring.wesl?static'; +import ringFsCode from '../shaders/structureMarker/ring.wesl?static'; +import ringPickVsCode from '../shaders/structureMarker/ring.wesl?static'; +import ringPickFsCode from '../shaders/structureMarker/ringPick.wesl?static'; import { createShaderModuleWithDevLog } from '../shaderCompileLogger'; /** * 12 floats per instance × 4 bytes = 48 bytes/instance. * - * Layout (matches VsIn in clusterMarker/io.wesl): + * Layout (matches VsIn in structureMarker/io.wesl): * [0..2] position.xyz — world-space centre * [3] radiusMpc — world-space half-extent (ring + halo) * [4..7] haloColor.rgba — additive halo tint + final alpha @@ -109,7 +94,7 @@ function byCategory(init: T): Record { >; } -export function createClusterMarkerRenderer( +export function createStructureMarkerRenderer( ctx: GpuContext, /** * The colour-attachment format the halo + ring pipelines write into. @@ -136,7 +121,7 @@ export function createClusterMarkerRenderer( */ fadeBgl: FadeUniformsBgl, initialCapacity = 64, -): ClusterMarkerRenderer { +): StructureMarkerRenderer { const device = ctx.device as GPUDevice | null; const format = hdrFormat; @@ -199,7 +184,7 @@ export function createClusterMarkerRenderer( if (device) { const cameraBgl = device.createBindGroupLayout({ - label: 'cluster-marker-camera-bgl', + label: 'structure-marker-camera-bgl', entries: [ { binding: 0, @@ -209,7 +194,7 @@ export function createClusterMarkerRenderer( ], }); const sourceBgl = device.createBindGroupLayout({ - label: 'cluster-marker-source-bgl', + label: 'structure-marker-source-bgl', entries: [ { binding: 0, @@ -218,7 +203,7 @@ export function createClusterMarkerRenderer( }, ], }); - // @group(1) FadeUniforms slot — the cluster-marker shaders DO NOT + // @group(1) FadeUniforms slot — the structure-marker shaders DO NOT // reference this slot (alpha rides on the per-descriptor fields the // CPU bakes in produceMarkers), but we MUST list the canonical // shared fadeBgl in the layout at slot 1. @@ -234,14 +219,14 @@ export function createClusterMarkerRenderer( // the pipeline layout-compatible with whatever the prior pass // bound. We never create a BindGroup against it ourselves. const pipelineLayout = device.createPipelineLayout({ - label: 'cluster-marker-pipeline-layout', + label: 'structure-marker-pipeline-layout', bindGroupLayouts: [cameraBgl, fadeBgl, sourceBgl], }); - const haloVs = createShaderModuleWithDevLog(device, haloVsCode, 'clusterMarker.halo.vs'); - const haloFs = createShaderModuleWithDevLog(device, haloFsCode, 'clusterMarker.halo.fs'); - const ringVs = createShaderModuleWithDevLog(device, ringVsCode, 'clusterMarker.ring.vs'); - const ringFs = createShaderModuleWithDevLog(device, ringFsCode, 'clusterMarker.ring.fs'); + const haloVs = createShaderModuleWithDevLog(device, haloVsCode, 'structureMarker.halo.vs'); + const haloFs = createShaderModuleWithDevLog(device, haloFsCode, 'structureMarker.halo.fs'); + const ringVs = createShaderModuleWithDevLog(device, ringVsCode, 'structureMarker.ring.vs'); + const ringFs = createShaderModuleWithDevLog(device, ringFsCode, 'structureMarker.ring.fs'); const vertexBuffers: GPUVertexBufferLayout[] = [ { @@ -256,7 +241,7 @@ export function createClusterMarkerRenderer( ]; haloPipeline = device.createRenderPipeline({ - label: 'cluster-marker-halo-pipeline', + label: 'structure-marker-halo-pipeline', layout: pipelineLayout, vertex: { module: haloVs, entryPoint: 'vs', buffers: vertexBuffers }, fragment: { @@ -278,7 +263,7 @@ export function createClusterMarkerRenderer( }); ringPipeline = device.createRenderPipeline({ - label: 'cluster-marker-ring-pipeline', + label: 'structure-marker-ring-pipeline', layout: pipelineLayout, vertex: { module: ringVs, entryPoint: 'vs', buffers: vertexBuffers }, fragment: { @@ -328,15 +313,15 @@ export function createClusterMarkerRenderer( const ringPickVs = createShaderModuleWithDevLog( device, ringPickVsCode, - 'clusterMarker.pick.vs', + 'structureMarker.pick.vs', ); const ringPickFs = createShaderModuleWithDevLog( device, ringPickFsCode, - 'clusterMarker.pick.fs', + 'structureMarker.pick.fs', ); ringPickPipeline = device.createRenderPipeline({ - label: 'cluster-marker-ring-pick-pipeline', + label: 'structure-marker-ring-pick-pipeline', layout: pipelineLayout, vertex: { module: ringPickVs, entryPoint: 'vs', buffers: vertexBuffers }, fragment: { @@ -359,30 +344,30 @@ export function createClusterMarkerRenderer( // UNIFORM only (no COPY_DST): we never write to it, the default- // zero contents are what we want. pickDummyFadeBuffer = device.createBuffer({ - label: 'cluster-marker-pick-fade-dummy', + label: 'structure-marker-pick-fade-dummy', size: 16, usage: GPUBufferUsage.UNIFORM, }); pickDummyFadeBindGroup = device.createBindGroup({ - label: 'cluster-marker-pick-fade-bg-dummy', + label: 'structure-marker-pick-fade-bg-dummy', layout: fadeBgl, entries: [{ binding: 0, resource: { buffer: pickDummyFadeBuffer } }], }); uniformBuffer = device.createBuffer({ - label: 'cluster-marker-uniforms', + label: 'structure-marker-uniforms', size: UNIFORM_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); instanceBuffer = device.createBuffer({ - label: 'cluster-marker-instances', + label: 'structure-marker-instances', size: capacity * MARKER_INSTANCE_BYTES, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); cameraBindGroup = device.createBindGroup({ - label: 'cluster-marker-camera-bg', + label: 'structure-marker-camera-bg', layout: cameraBgl, entries: [{ binding: 0, resource: { buffer: uniformBuffer } }], }); @@ -391,12 +376,12 @@ export function createClusterMarkerRenderer( // fade.opacity scalar into the first 4 bytes each frame. Bind // group lives forever; only the buffer contents change. fadeBuffer = device.createBuffer({ - label: 'cluster-marker-fade-uniform', + label: 'structure-marker-fade-uniform', size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); fadeBindGroup = device.createBindGroup({ - label: 'cluster-marker-fade-bg', + label: 'structure-marker-fade-bg', layout: fadeBgl, entries: [{ binding: 0, resource: { buffer: fadeBuffer } }], }); @@ -404,7 +389,7 @@ export function createClusterMarkerRenderer( // Per-category SourceUniforms — written once at construction. for (const cat of STRUCTURE_CATEGORIES) { const buf = device.createBuffer({ - label: `cluster-marker-source-${cat}`, + label: `structure-marker-source-${cat}`, size: SOURCE_UNIFORM_BYTES, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); @@ -414,7 +399,7 @@ export function createClusterMarkerRenderer( device.queue.writeBuffer(buf, 0, u32); sourceBuffers[cat] = buf; sourceBindGroups[cat] = device.createBindGroup({ - label: `cluster-marker-source-bg-${cat}`, + label: `structure-marker-source-bg-${cat}`, layout: sourceBgl, entries: [{ binding: 0, resource: { buffer: buf } }], }); @@ -440,17 +425,17 @@ export function createClusterMarkerRenderer( if (device) { instanceBuffer?.destroy(); instanceBuffer = device.createBuffer({ - label: 'cluster-marker-instances', + label: 'structure-marker-instances', size: capacity * MARKER_INSTANCE_BYTES, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST, }); } } - function setMarkers(descriptors: readonly ClusterMarkerDescriptor[]): void { + function setMarkers(descriptors: readonly StructureMarkerDescriptor[]): void { // Partition descriptors by category — preserves order within each - // category and keeps the instance buffer cache-friendly. Three - // categories means three passes over the input is fine. + // category and keeps the instance buffer cache-friendly. A handful + // of categories means a few passes over the input is fine. currentMarkerCount = 0; for (const c of STRUCTURE_CATEGORIES) bucketCounts[c] = 0; @@ -463,9 +448,7 @@ export function createClusterMarkerRenderer( // First pass: count per category to compute offsets. const count = descriptors.length; for (let i = 0; i < count; i++) { - const d = descriptors[i]!; - // famousGalaxy (label-only) has no markers; skip. - if (d.category !== 'famousGalaxy') bucketCounts[d.category]++; + bucketCounts[descriptors[i]!.category]++; } // Prefix-sum the counts into per-category run offsets. let acc = 0; @@ -478,7 +461,6 @@ export function createClusterMarkerRenderer( const writeCursor: Record = { ...bucketOffsets }; for (let i = 0; i < count; i++) { const d = descriptors[i]!; - if (d.category === 'famousGalaxy') continue; const slot = writeCursor[d.category]; writeCursor[d.category]++; const base = slot * MARKER_INSTANCE_FLOATS; @@ -590,7 +572,7 @@ export function createClusterMarkerRenderer( /** * Issue per-category POI ring pick draws into the caller-supplied - * render pass. See the docstring on ClusterMarkerRenderer.pickRing + * render pass. See the docstring on StructureMarkerRenderer.pickRing * for the binding contract — short version: caller bound @group(0), * we bind @group(1) (dummy fade) + @group(2) (per-category source) * and emit one `draw(6, count)` per non-empty bucket. @@ -634,8 +616,8 @@ export function createClusterMarkerRenderer( } } - const renderer: ClusterMarkerRenderer = { - label: 'clusterMarkerRenderer', + const renderer: StructureMarkerRenderer = { + label: 'structureMarkerRenderer', setMarkers, render, markerCount, diff --git a/src/services/gpu/shaders/lib/focusUniforms.wesl b/src/services/gpu/shaders/lib/focusUniforms.wesl index 7dda4374f..9af220dfd 100644 --- a/src/services/gpu/shaders/lib/focusUniforms.wesl +++ b/src/services/gpu/shaders/lib/focusUniforms.wesl @@ -1,5 +1,5 @@ // lib/focusUniforms.wesl — shared 'which POI is focused' uniform, written -// once per frame from clusterFocusSubsystem state. blend=0 collapses the +// once per frame from structureFocusSubsystem state. blend=0 collapses the // per-vertex multiplier to 1.0 (no effect); when active, galaxies in the // focused structure's core (within physicalRadiusMpc) keep alpha 1.0, and // the rest ramp down to 0.08 across the band out to the apparent edge diff --git a/src/services/gpu/shaders/pickDebugOverlay/fragment.wesl b/src/services/gpu/shaders/pickDebugOverlay/fragment.wesl index 34aeb8830..eee167cd9 100644 --- a/src/services/gpu/shaders/pickDebugOverlay/fragment.wesl +++ b/src/services/gpu/shaders/pickDebugOverlay/fragment.wesl @@ -30,7 +30,7 @@ // Drawn via OVER blend ('srcFactor: one, dstFactor: one-minus-src- // alpha') so the overlay sits on top of the tone-mapped frame. // Pre-multiplying RGB by alpha at the fragment matches the rest of -// the UI overlay stack (markerLines, labels, clusterMarker). +// the UI overlay stack (markerLines, labels, structureMarker). import package::pickDebugOverlay::io::VSOut; import package::lib::selectionEncoding::PICK_SENTINEL_OFFSET; diff --git a/src/services/gpu/shaders/clusterMarker/halo.wesl b/src/services/gpu/shaders/structureMarker/halo.wesl similarity index 74% rename from src/services/gpu/shaders/clusterMarker/halo.wesl rename to src/services/gpu/shaders/structureMarker/halo.wesl index 293c3faaa..fdec7a4d4 100644 --- a/src/services/gpu/shaders/clusterMarker/halo.wesl +++ b/src/services/gpu/shaders/structureMarker/halo.wesl @@ -1,30 +1,20 @@ -// clusterMarker/halo.wesl — additive radial-gradient billboard for -// cluster / supercluster halos. +// structureMarker/halo.wesl — additive radial-gradient billboard for +// structure halos (clusters / superclusters; voids opt out). // -// One instance per POI. The vertex stage projects the POI's -// world-space centre to clip space, then expands a unit quad whose -// world-space half-extent equals physicalRadiusMpc using the shared -// 'expandBillboardWorld' helper in lib/billboard.wesl — screen-axis- -// aligned (no shear under orbit), world-sized (grows naturally as the -// camera approaches), zero extra uniforms. +// One instance per structure marker. The vertex stage projects the +// marker's world-space centre to clip space, then expands a unit quad +// whose world-space half-extent equals physicalRadiusMpc using the +// shared 'expandBillboardWorld' helper in lib/billboard.wesl — +// screen-axis-aligned (no shear under orbit), world-sized (grows +// naturally as the camera approaches), zero extra uniforms. See +// lib/billboard.wesl's "Screen-aligned vs view-aligned" subsection. // // The fragment stage emits an additive radial gradient: centre alpha // full, edge alpha zero, smoothstep falloff across the quad. -// -// ## History -// -// v1 of this shader derived the billboard basis by projecting -// 'worldPos + (radiusMpc, 0, 0)' and 'worldPos + (0, radiusMpc, 0)' -// and taking per-axis max — fixed world axes, not camera-aligned, so -// the halo visibly sheared as the user orbited. The current -// 'expandBillboardWorld' helper uses clip.w as a depth proxy and -// expands along screen axes, fixing the shear with no extra -// CameraUniforms fields. See lib/billboard.wesl's "Screen-aligned vs -// view-aligned" subsection for the full reasoning. -import package::clusterMarker::io::Uniforms; -import package::clusterMarker::io::VsIn; -import package::clusterMarker::io::VsOut; +import package::structureMarker::io::Uniforms; +import package::structureMarker::io::VsIn; +import package::structureMarker::io::VsOut; import package::lib::camera::worldToClip; import package::lib::billboard::quadCorner; import package::lib::billboard::expandBillboardWorld; diff --git a/src/services/gpu/shaders/clusterMarker/io.wesl b/src/services/gpu/shaders/structureMarker/io.wesl similarity index 94% rename from src/services/gpu/shaders/clusterMarker/io.wesl rename to src/services/gpu/shaders/structureMarker/io.wesl index 1e290791e..cfab8b554 100644 --- a/src/services/gpu/shaders/clusterMarker/io.wesl +++ b/src/services/gpu/shaders/structureMarker/io.wesl @@ -1,4 +1,4 @@ -// clusterMarker/io.wesl — shared struct definitions for the halo + ring pipelines. +// structureMarker/io.wesl — shared struct definitions for the halo + ring pipelines. // // Both pipelines (halo, ring) read the same CameraUniforms prefix and // the same per-category SourceUniforms. Co-locating the structs here @@ -20,7 +20,7 @@ // // The 80-byte CameraUniforms prefix carries viewProj + viewportPx + // two pads. No renderer-specific tail at v1. The CPU-side write site -// in clusterMarkerRenderer.ts allocates 80 bytes and writes the same +// in structureMarkerRenderer.ts allocates 80 bytes and writes the same // shape as markerLineRenderer.ts. // // ## Why no per-vertex attribute struct @@ -40,8 +40,8 @@ struct Uniforms { // ── per-instance attributes ─────────────────────────────────────── // -// One instance per cluster marker. The CPU writer in -// clusterMarkerRenderer.ts packs each instance into 48 bytes +// One instance per structure marker. The CPU writer in +// structureMarkerRenderer.ts packs each instance into 48 bytes // (12 f32 slots) laid out as: // // [0..15] positionAndRadius vec4 (centreWorld.xyz, physicalRadiusMpc) diff --git a/src/services/gpu/shaders/clusterMarker/ring.wesl b/src/services/gpu/shaders/structureMarker/ring.wesl similarity index 96% rename from src/services/gpu/shaders/clusterMarker/ring.wesl rename to src/services/gpu/shaders/structureMarker/ring.wesl index dac7bca9b..0e448575e 100644 --- a/src/services/gpu/shaders/clusterMarker/ring.wesl +++ b/src/services/gpu/shaders/structureMarker/ring.wesl @@ -1,6 +1,6 @@ -// clusterMarker/ring.wesl — screen-anti-aliased ring at world radius. +// structureMarker/ring.wesl — screen-anti-aliased ring at world radius. // -// One instance per POI. Vertex stage: identical to halo.wesl's +// One instance per structure marker. Vertex stage: identical to halo.wesl's // world-sized billboard expansion — same shared 'expandBillboardWorld' // helper from lib/billboard.wesl (screen-aligned, no shear under // orbit, see that file's "Screen-aligned vs view-aligned" subsection @@ -30,9 +30,9 @@ // We smuggle 'sizePx' through the VsOut.camDistMpc slot rather than // adding a dedicated varying — see the slot comment below. -import package::clusterMarker::io::Uniforms; -import package::clusterMarker::io::VsIn; -import package::clusterMarker::io::VsOut; +import package::structureMarker::io::Uniforms; +import package::structureMarker::io::VsIn; +import package::structureMarker::io::VsOut; import package::lib::camera::worldToClip; import package::lib::billboard::quadCorner; import package::lib::billboard::expandBillboardWorld; diff --git a/src/services/gpu/shaders/clusterMarker/ringPick.wesl b/src/services/gpu/shaders/structureMarker/ringPick.wesl similarity index 88% rename from src/services/gpu/shaders/clusterMarker/ringPick.wesl rename to src/services/gpu/shaders/structureMarker/ringPick.wesl index d0f66f5c0..0eaa57491 100644 --- a/src/services/gpu/shaders/clusterMarker/ringPick.wesl +++ b/src/services/gpu/shaders/structureMarker/ringPick.wesl @@ -1,9 +1,9 @@ -// clusterMarker/ringPick.wesl — offscreen r32uint picking fragment for -// cluster / supercluster / void POI rings. +// structureMarker/ringPick.wesl — offscreen r32uint picking fragment for +// structure rings (cluster / supercluster / void / group). // -// Sister to clusterMarker/ring.wesl (the visible-ring fragment). Both +// Sister to structureMarker/ring.wesl (the visible-ring fragment). Both // consume the same VsOut produced by the shared vertex stage in -// ring.wesl; this one writes the packed POI identity into the pick +// ring.wesl; this one writes the packed structure identity into the pick // texture instead of the visible-ring colour. // // ## Why a separate pick fragment @@ -33,8 +33,8 @@ // // Same '(sourceCode << 27) | (poiIndex + PICK_SENTINEL_OFFSET)' packing // the galaxy path uses. The per-category SourceUniforms block carries -// the source code (5/6/7 for cluster/supercluster/void — see sources.ts -// and lib/selectionEncoding.wesl). 'poiIndex' is the +// the source code (5/6/7/15 for cluster/supercluster/void/group — see +// sources.ts and lib/selectionEncoding.wesl). 'poiIndex' is the // @builtin(instance_index) of this draw, populated in the vertex stage // (ring.wesl) and forwarded through VsOut.poiIndex — flat-interpolated // so the integer arrives unchanged across all six vertices of the @@ -42,19 +42,19 @@ // // ## Why import VsOut from io.wesl rather than from ring.wesl // -// The struct itself lives in clusterMarker/io.wesl (the shared-struct +// The struct itself lives in structureMarker/io.wesl (the shared-struct // module both halo + ring import from). ring.wesl declares the // @vertex fn that produces it but does NOT re-export the struct; the // canonical path is the io module. -import package::clusterMarker::io::VsOut; +import package::structureMarker::io::VsOut; import package::lib::sourceUniforms::SourceUniforms; import package::lib::selectionEncoding::PICK_SENTINEL_OFFSET; // Re-declare the per-source binding here so this module is self- // contained. WESL has no global state — '@group(N) @binding(M)' is // module-local and cannot be exported. The same layout numbers are -// declared in clusterMarker/ring.wesl (visible-ring path); WGSL is +// declared in structureMarker/ring.wesl (visible-ring path); WGSL is // fine with multiple modules declaring the same binding so long as // the layout matches, and the SourceUniforms struct is imported from // the single lib authority so the byte layout cannot drift across diff --git a/src/services/loading/fetchers/clusterCatalogFetcher.ts b/src/services/loading/fetchers/structureCatalogFetcher.ts similarity index 61% rename from src/services/loading/fetchers/clusterCatalogFetcher.ts rename to src/services/loading/fetchers/structureCatalogFetcher.ts index bed620c43..1e2163b75 100644 --- a/src/services/loading/fetchers/clusterCatalogFetcher.ts +++ b/src/services/loading/fetchers/structureCatalogFetcher.ts @@ -1,16 +1,16 @@ /** - * clusterCatalogFetcher — fetches the cluster/supercluster coverage layer and + * structureCatalogFetcher — fetches the cluster/supercluster coverage layer and * returns a `{ catalog, meta }` payload. * * The layer ships as two index-parallel artefacts: - * - `clusters.ccat` — the numeric `ClusterCatalog` binary (positions, + * - `structures.ccat` — the numeric `StructureCatalog` binary (positions, * radii, significance, category). - * - `clusters_meta.json` — the string sidecar (id, names, abell, description) + * - `structures_meta.json` — the string sidecar (id, names, abell, description) * keyed by the same localIdx. * * This fetcher pulls BOTH and pairs them so the later merge has names + * descriptions to hang off each record. The two are built in lock-step by - * `tools/clusters/buildClusters.ts`, so a `count !== meta.length` mismatch + * `tools/structures/buildStructures.ts`, so a `count !== meta.length` mismatch * means a stale artefact slipped through — we fail loud rather than silently * decode a half-mismatched layer. * @@ -30,30 +30,30 @@ * honors the abort signal. */ import type { Fetcher } from '../../../@types/loading/Fetcher'; -import type { ClusterCatalogReq } from '../../../@types/loading/ClusterCatalogReq'; +import type { StructureCatalogReq } from '../../../@types/loading/StructureCatalogReq'; import type { - ClusterCatalogPayload, - ClusterMetaEntry, -} from '../../../@types/loading/ClusterCatalogPayload'; -import { decodeClusterCatalog } from '../../../data/clusterCatalogFormat'; + StructureCatalogPayload, + StructureMetaEntry, +} from '../../../@types/loading/StructureCatalogPayload'; +import { decodeStructureCatalog } from '../../../data/structureCatalogFormat'; import { HttpError, dataUrl } from '../fetchWithProgress'; /** - * Parse `clusters_meta.json` content. Throws on a non-array root. Public so it + * Parse `structures_meta.json` content. Throws on a non-array root. Public so it * can be unit-tested without hitting the network. */ -export function parseClusterMeta(rawJson: string): ClusterMetaEntry[] { +export function parseStructureMeta(rawJson: string): StructureMetaEntry[] { const parsed = JSON.parse(rawJson); if (!Array.isArray(parsed)) { - throw new Error('clusters_meta.json: root must be an array'); + throw new Error('structures_meta.json: root must be an array'); } - return parsed as ClusterMetaEntry[]; + return parsed as StructureMetaEntry[]; } -const CCAT_FILE = 'clusters.ccat'; -const META_FILE = 'clusters_meta.json'; +const CCAT_FILE = 'structures.ccat'; +const META_FILE = 'structures_meta.json'; -export const clusterCatalogFetcher: Fetcher = async ( +export const structureCatalogFetcher: Fetcher = async ( _req, signal, ) => { @@ -70,17 +70,17 @@ export const clusterCatalogFetcher: Fetcher = ( +export const createStructureCatalogSlot: SlotFactory = ( state, _cb, ) => { const slot = createAssetSlot({ - name: 'cluster-catalog', - fetch: clusterCatalogFetcher, + name: 'structure-catalog', + fetch: structureCatalogFetcher, }); slot.subscribe((s) => { if (s.kind === 'ready') { diff --git a/src/services/url/poiUrl.ts b/src/services/url/poiUrl.ts index b40717ae8..b07c3d578 100644 --- a/src/services/url/poiUrl.ts +++ b/src/services/url/poiUrl.ts @@ -14,7 +14,7 @@ * * The id is the literal `StructureRecord.id` (e.g. `virgo-m87`, * `hercules-sc`, `bootes-void`). POI ids are curated and stable across - * rebuilds (they live in `data/cluster_anchors.seed.json`), so encoding + * rebuilds (they live in `data/structure_anchors.seed.json`), so encoding * them directly is safe — unlike galaxies, there's no priority ladder to * navigate (no `famous > pgc > sdss > pos` cascade). * diff --git a/src/utils/cluster/structureMemberCount.ts b/src/utils/structure/structureMemberCount.ts similarity index 88% rename from src/utils/cluster/structureMemberCount.ts rename to src/utils/structure/structureMemberCount.ts index 536565122..a19e93036 100644 --- a/src/utils/cluster/structureMemberCount.ts +++ b/src/utils/structure/structureMemberCount.ts @@ -2,7 +2,7 @@ * structureMemberCount — how many currently-rendered galaxies fall inside * a cluster / supercluster / void's membership sphere. * - * This is the InfoCard-facing consumer the `clusterMembership` cone-search + * This is the InfoCard-facing consumer the `structureMembership` cone-search * was written for. It assembles the catalog set the way the renderer * draws it — visible survey sources only, Synthetic excluded — so the * number agrees with both the on-screen points and the focus-mode fade @@ -22,7 +22,7 @@ * ### Membership radius * * Uses `apparentRadiusMpc ?? physicalRadiusMpc` — the same cone the - * GPU focus fade tests against (see `clusterFocusSubsystem`). Counting + * GPU focus fade tests against (see `structureFocusSubsystem`). Counting * against the named/visual extent rather than the tighter core radius is * what makes "N galaxies" match the set the user sees stay lit on focus. * @@ -35,8 +35,8 @@ * catalogued galaxies). */ -import { clusterMembership } from './clusterMembership'; -import type { CatalogWithSource } from './clusterMembership'; +import { structureMembership } from './structureMembership'; +import type { CatalogWithSource } from './structureMembership'; import { SURVEY_SOURCES, Source } from '../../data/sources'; import { maskHas } from '../sourceMask'; import type { GalaxyCatalog } from '../../@types/data/GalaxyCatalog'; @@ -61,5 +61,5 @@ export function structureMemberCount( // Nothing loaded/visible to search — "not computable yet", not zero. if (catalogs.length === 0) return null; - return clusterMembership(catalogs, poi.worldPos, radiusMpc).count; + return structureMembership(catalogs, poi.worldPos, radiusMpc).count; } diff --git a/src/utils/cluster/clusterMembership.ts b/src/utils/structure/structureMembership.ts similarity index 94% rename from src/utils/cluster/clusterMembership.ts rename to src/utils/structure/structureMembership.ts index 4ac337fe5..c67cb8663 100644 --- a/src/utils/cluster/clusterMembership.ts +++ b/src/utils/structure/structureMembership.ts @@ -1,5 +1,5 @@ /** - * clusterMembership — pure cone-search over loaded galaxy catalogs. + * structureMembership — pure cone-search over loaded galaxy catalogs. * * Given a set of in-memory catalogs, a 3D center, and a radius in Mpc, * returns the packed (sourceCode << 27 | localIdx) identities of every @@ -51,12 +51,12 @@ export type CatalogWithSource = { }; /** - * The return value of {@link clusterMembership}. `packedIds` carries + * The return value of {@link structureMembership}. `packedIds` carries * the matched galaxies in catalog-array-order, then local-index order; * `count` is its length, exposed redundantly so callers (e.g. the * InfoCard "N member galaxies" text) don't have to read `.length`. */ -export type ClusterMembershipResult = { +export type StructureMembershipResult = { readonly count: number; readonly packedIds: readonly number[]; }; @@ -75,11 +75,11 @@ export type ClusterMembershipResult = { * `Object.freeze` if they want defensive immutability; we don't * freeze it here to keep the hot path allocation-free. */ -export function clusterMembership( +export function structureMembership( catalogs: readonly CatalogWithSource[], centerMpc: Vec3, radiusMpc: number, -): ClusterMembershipResult { +): StructureMembershipResult { const cx = centerMpc[0]; const cy = centerMpc[1]; const cz = centerMpc[2]; diff --git a/tests/@types/engineState.test.ts b/tests/@types/engineState.test.ts index fb28088d3..358852e56 100644 --- a/tests/@types/engineState.test.ts +++ b/tests/@types/engineState.test.ts @@ -48,7 +48,7 @@ import { createSelectionSubsystem } from '../../src/services/engine/subsystems/s import { createBiasCorrectionSubsystem } from '../../src/services/engine/subsystems/biasCorrectionSubsystem'; import { createYouAreHereSubsystem } from '../../src/services/engine/subsystems/youAreHereSubsystem'; import { createLabelDirectorSubsystem } from '../../src/services/engine/subsystems/labelDirectorSubsystem'; -import { createClusterFocusSubsystem } from '../../src/services/engine/subsystems/clusterFocusSubsystem'; +import { createStructureFocusSubsystem } from '../../src/services/engine/subsystems/structureFocusSubsystem'; import { createFadeRegistry } from '../../src/services/animation/fadeRegistry'; import { createDisabledGpuTimingService } from '../../src/services/gpu/timing/gpuTimingService'; import type { EngineCallbacks } from '../../src/@types/engine/EngineCallbacks'; @@ -158,7 +158,7 @@ describe('EngineState type', () => { labelRenderer: null, markerLineRenderer: null, selectionRingRenderer: null, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, texturedDiskRenderer: null, proceduralDiskRenderer: null, milkyWayRenderer: null, @@ -196,7 +196,7 @@ describe('EngineState type', () => { }), youAreHere: createYouAreHereSubsystem(), labelDirector: createLabelDirectorSubsystem(), - clusterFocus: createClusterFocusSubsystem(), + structureFocus: createStructureFocusSubsystem(), clickResolver: null, inputBindings: null, scheduler: createRenderScheduler({ onFrame: () => {}, rafImpl: noopRaf, cafImpl: noopCaf }), @@ -208,7 +208,7 @@ describe('EngineState type', () => { points: new Map(), filaments: null, famousMeta: null, - clusterCatalog: null, + structureCatalog: null, pgcAlias: null, cf4Density: null, mcpm: null, @@ -353,7 +353,7 @@ describe('EngineState type', () => { labelRenderer: null, markerLineRenderer: null, selectionRingRenderer: null, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, texturedDiskRenderer: null, proceduralDiskRenderer: null, milkyWayRenderer: null, @@ -391,7 +391,7 @@ describe('EngineState type', () => { }), youAreHere: createYouAreHereSubsystem(), labelDirector: createLabelDirectorSubsystem(), - clusterFocus: createClusterFocusSubsystem(), + structureFocus: createStructureFocusSubsystem(), clickResolver: null, inputBindings: null, scheduler: createRenderScheduler({ onFrame: () => {}, rafImpl: noopRaf, cafImpl: noopCaf }), @@ -403,7 +403,7 @@ describe('EngineState type', () => { points: new Map(), filaments: null, famousMeta: null, - clusterCatalog: null, + structureCatalog: null, pgcAlias: null, cf4Density: null, mcpm: null, diff --git a/tests/@types/loading/AssetKey.types.test.ts b/tests/@types/loading/AssetKey.types.test.ts index 502923848..818c3da33 100644 --- a/tests/@types/loading/AssetKey.types.test.ts +++ b/tests/@types/loading/AssetKey.types.test.ts @@ -3,7 +3,7 @@ * * Confirms that every variant of `AssetKey` is accepted: a concrete numeric * `Source` value (covering `SourceType`), and each of the auxiliary string - * keys (`'clusterCatalog'`, `'famousMeta'`, `'pgcAlias'`, `'filaments'`, + * keys (`'structureCatalog'`, `'famousMeta'`, `'pgcAlias'`, `'filaments'`, * `'cf4Density'`, `'mcpm'`). * * These are purely compile-time assertions. If `AssetKey` drifts from its @@ -21,9 +21,9 @@ describe('AssetKey assignability', () => { expect(k).toBe(Source.SDSS); }); - it("accepts 'clusterCatalog'", () => { - const k: AssetKey = 'clusterCatalog'; - expect(k).toBe('clusterCatalog'); + it("accepts 'structureCatalog'", () => { + const k: AssetKey = 'structureCatalog'; + expect(k).toBe('structureCatalog'); }); it("accepts 'famousMeta'", () => { diff --git a/tests/@types/loading/DemandCtx.types.test.ts b/tests/@types/loading/DemandCtx.types.test.ts index 7cb89c35a..0a6198559 100644 --- a/tests/@types/loading/DemandCtx.types.test.ts +++ b/tests/@types/loading/DemandCtx.types.test.ts @@ -5,7 +5,7 @@ * - A literal object with all fields satisfies `DemandCtx`. * - `request('paletteOpened')` typechecks (i.e. `'paletteOpened'` is a * valid `RequestKey` and the return type is `boolean`). - * - `slotState('clusterCatalog')` typechecks with the `LoadState['kind']` + * - `slotState('structureCatalog')` typechecks with the `LoadState['kind']` * return type. * - `volumeField('mcpm')` typechecks (returns the params or undefined). * @@ -57,7 +57,7 @@ describe('DemandCtx assignability', () => { }); it('slotState accepts an AssetKey string and returns a LoadState kind', () => { - const kind = ctx.slotState('clusterCatalog'); + const kind = ctx.slotState('structureCatalog'); // The return type is LoadState['kind'] — a union of string literals. expect(typeof kind).toBe('string'); }); diff --git a/tests/data/buildStaticAnchorStructures.test.ts b/tests/data/buildStaticAnchorStructures.test.ts index ea2b72fc3..667e4a740 100644 --- a/tests/data/buildStaticAnchorStructures.test.ts +++ b/tests/data/buildStaticAnchorStructures.test.ts @@ -17,13 +17,13 @@ import { describe, it, expect, vi } from 'vitest'; import { buildStaticAnchorStructures } from '../../src/data/buildStaticAnchorStructures'; -import clusterSeedJson from '../../data/cluster_anchors.seed.json'; +import structureSeedJson from '../../data/structure_anchors.seed.json'; import { raDecDistToEqCart } from '../../src/utils/math/raDecDistToEqCart'; describe('buildStaticAnchorStructures', () => { it('emits one structure per seed entry across all four categories', () => { const pois = buildStaticAnchorStructures(); - expect(pois.length).toBe(clusterSeedJson.length); + expect(pois.length).toBe(structureSeedJson.length); }); it('produces URL-safe ids by prefixing the category to the seed id field', () => { @@ -62,7 +62,7 @@ describe('buildStaticAnchorStructures', () => { // Assert against the seed's own value (not a hardcoded string) so the // test stays green when the curated blurbs are rewritten — it verifies // the carry-through wiring, not the prose. - const seedVirgo = (clusterSeedJson as readonly { id: string; description?: string }[]).find( + const seedVirgo = (structureSeedJson as readonly { id: string; description?: string }[]).find( (e) => e.id === 'virgo-m87', )!; const virgo = pois.find((p) => p.id === 'cluster-virgo-m87')!; @@ -141,14 +141,13 @@ describe('buildStaticAnchorStructures — group seed entry mapping', () => { // at the top of this file) and picks up the mocked seed — `doMock` alone // does not invalidate an already-loaded module. vi.resetModules(); - vi.doMock('../../data/cluster_anchors.seed.json', () => ({ + vi.doMock('../../data/structure_anchors.seed.json', () => ({ default: [groupFixture], })); // Dynamic import after resetModules + doMock so this load sees the mock. - const { buildStaticAnchorStructures: buildWithGroupSeed } = await import( - '../../src/data/buildStaticAnchorStructures' - ); + const { buildStaticAnchorStructures: buildWithGroupSeed } = + await import('../../src/data/buildStaticAnchorStructures'); const pois = buildWithGroupSeed(); expect(pois.length).toBe(1); @@ -161,7 +160,7 @@ describe('buildStaticAnchorStructures — group seed entry mapping', () => { // asserting this verifies the carry-through wiring, not just the discriminant. expect(poi.worldPos).toEqual(raDecDistToEqCart(groupFixture)); - vi.doUnmock('../../data/cluster_anchors.seed.json'); + vi.doUnmock('../../data/structure_anchors.seed.json'); vi.resetModules(); }); }); diff --git a/tests/data/selectionEncoding.test.ts b/tests/data/selectionEncoding.test.ts index 074404658..39417fa9d 100644 --- a/tests/data/selectionEncoding.test.ts +++ b/tests/data/selectionEncoding.test.ts @@ -133,7 +133,7 @@ describe('selectionEncoding TS↔WESL parity', () => { ['PICK_SENTINEL_OFFSET', PICK_SENTINEL_OFFSET], // POI category source codes — mirror of TS Source.Cluster / // Source.Supercluster / Source.Void / Source.Group. These appear - // at the WESL side so the future cluster-marker pick fragment can + // at the WESL side so the future structure-marker pick fragment can // refer to them by name instead of inlining a magic 5u/6u/7u/15u // literal. ['SOURCE_CODE_CLUSTER', Source.Cluster], diff --git a/tests/data/sources.test.ts b/tests/data/sources.test.ts index e5e8c997b..19e192596 100644 --- a/tests/data/sources.test.ts +++ b/tests/data/sources.test.ts @@ -77,7 +77,7 @@ describe('Source enum — POI codes (cluster/supercluster/void)', () => { it('keeps POI codes OUT of SURVEY_SOURCES (POIs are not survey sources)', () => { // The points-pipeline visibility bitmask iterates SURVEY_SOURCES. POIs - // render through their own renderer (future clusterMarkerRenderer) + // render through their own renderer (future structureMarkerRenderer) // with its own per-category visibility logic, so listing them here // would muddy the meaning of "this bitmask filters survey galaxies." expect(SURVEY_SOURCES).not.toContain(Source.Cluster); diff --git a/tests/data/clusterAnchors.test.ts b/tests/data/structureAnchors.test.ts similarity index 91% rename from tests/data/clusterAnchors.test.ts rename to tests/data/structureAnchors.test.ts index cdaef26ce..bec4fe0ab 100644 --- a/tests/data/clusterAnchors.test.ts +++ b/tests/data/structureAnchors.test.ts @@ -1,8 +1,8 @@ /** * Tests for `raDecDistToEqCart` and the cluster seed content. * - * The seed data lives in `data/cluster_anchors.seed.json`, parsed by - * `tools/parsers/parseClusterSeed.ts`; the coordinate helper lives at + * The seed data lives in `data/structure_anchors.seed.json`, parsed by + * `tools/parsers/parseStructureSeed.ts`; the coordinate helper lives at * `src/utils/math/raDecDistToEqCart.ts`. * * The id invariants that matter for deep-link stability are covered in @@ -14,11 +14,11 @@ import { describe, it, expect } from 'vitest'; import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { raDecDistToEqCart } from '../../src/utils/math/raDecDistToEqCart'; -import { parseClusterSeed } from '../../tools/parsers/parseClusterSeed'; -import type { ClusterSeedEntry } from '../../tools/parsers/parseClusterSeed'; +import { parseStructureSeed } from '../../tools/parsers/parseStructureSeed'; +import type { StructureSeedEntry } from '../../tools/parsers/parseStructureSeed'; -const SEED_PATH = resolve(__dirname, '../../data/cluster_anchors.seed.json'); -const allEntries = parseClusterSeed(readFileSync(SEED_PATH, 'utf-8')); +const SEED_PATH = resolve(__dirname, '../../data/structure_anchors.seed.json'); +const allEntries = parseStructureSeed(readFileSync(SEED_PATH, 'utf-8')); const CLUSTER_ENTRIES = allEntries.filter((e) => e.category === 'cluster'); const SUPERCLUSTER_ENTRIES = allEntries.filter((e) => e.category === 'supercluster'); @@ -142,7 +142,7 @@ describe('cluster seed — void entries', () => { describe('cluster seed — physicalRadiusMpc population', () => { it('uses the literature-grounded radii from the spec', () => { - const byPrimaryName = (list: readonly ClusterSeedEntry[], n: string) => + const byPrimaryName = (list: readonly StructureSeedEntry[], n: string) => list.find((a) => a.names[0]?.startsWith(n)); expect(byPrimaryName(CLUSTER_ENTRIES, 'Virgo')?.physicalRadiusMpc).toBe(2.2); diff --git a/tests/data/clusterCatalogFormat.test.ts b/tests/data/structureCatalogFormat.test.ts similarity index 77% rename from tests/data/clusterCatalogFormat.test.ts rename to tests/data/structureCatalogFormat.test.ts index 6220a3622..9b72ef9a9 100644 --- a/tests/data/clusterCatalogFormat.test.ts +++ b/tests/data/structureCatalogFormat.test.ts @@ -1,5 +1,5 @@ /** - * Format-level tests for the v1 cluster-catalog binary (CCAT). + * Format-level tests for the v1 structure-catalog binary (CCAT). * * Six contracts under test: * @@ -10,10 +10,10 @@ * 3. A buffer with the wrong magic is rejected with an error containing * 'CCAT' so the caller knows which format was expected. * 4. A buffer with an unsupported version is rejected with a message that - * contains 'build-clusters' so the caller knows how to regenerate. + * contains 'build-structures' so the caller knows how to regenerate. * 5. A truncated buffer (byteLength < header + count*28) is rejected with * a message containing 'truncated' rather than silently decoding zeros. - * 6. emptyClusterCatalog() returns a count-0 catalog with zero-length arrays. + * 6. emptyStructureCatalog() returns a count-0 catalog with zero-length arrays. * * Float values in round-trip tests use exactly representable f32 literals * (integers, powers of two, half-integers) so the Float32 truncation in @@ -21,14 +21,14 @@ */ import { describe, it, expect } from 'vitest'; import { - encodeClusterCatalog, - decodeClusterCatalog, - emptyClusterCatalog, -} from '../../src/data/clusterCatalogFormat'; -import type { ClusterCatalog } from '../../src/@types/data/ClusterCatalog'; + encodeStructureCatalog, + decodeStructureCatalog, + emptyStructureCatalog, +} from '../../src/data/structureCatalogFormat'; +import type { StructureCatalog } from '../../src/@types/data/StructureCatalog'; /** Build a two-record test catalog with known field values. */ -function makeCatalog(): ClusterCatalog { +function makeCatalog(): StructureCatalog { return { count: 2, // Record 0: a cluster; record 1: a supercluster @@ -48,8 +48,8 @@ function makeCatalog(): ClusterCatalog { describe('encode/decode cluster catalog v1 (CCAT)', () => { it('round-trips positions, radii, significance, and category for a 2-record catalog', () => { const cat = makeCatalog(); - const buf = encodeClusterCatalog(cat); - const out = decodeClusterCatalog(buf); + const buf = encodeStructureCatalog(cat); + const out = decodeStructureCatalog(buf); expect(out.count).toBe(2); @@ -74,14 +74,14 @@ describe('encode/decode cluster catalog v1 (CCAT)', () => { }); it('encoded file size is 16 + count*28', () => { - const buf = encodeClusterCatalog(makeCatalog()); + const buf = encodeStructureCatalog(makeCatalog()); // 16-byte header + 2 records × 28 bytes expect(buf.byteLength).toBe(16 + 2 * 28); }); it('encoded file size for count=0 is exactly 16 bytes', () => { - const empty = emptyClusterCatalog(); - const buf = encodeClusterCatalog(empty); + const empty = emptyStructureCatalog(); + const buf = encodeStructureCatalog(empty); expect(buf.byteLength).toBe(16); }); @@ -89,7 +89,7 @@ describe('encode/decode cluster catalog v1 (CCAT)', () => { // Direct byte inspection so we catch a stride regression independently // of the decode path. const cat = makeCatalog(); - const buf = encodeClusterCatalog(cat); + const buf = encodeStructureCatalog(cat); const bytes = new Uint8Array(buf); // Record 0: category=0 at header(16) + rec(0)*28 + offset(24) = 40 @@ -115,29 +115,29 @@ describe('encode/decode cluster catalog v1 (CCAT)', () => { dv.setUint32(8, 0, true); // count 0 dv.setUint32(12, 0, true); // reserved - expect(() => decodeClusterCatalog(buf)).toThrow(/CCAT/); + expect(() => decodeStructureCatalog(buf)).toThrow(/CCAT/); }); - it('rejects wrong version with an error containing "build-clusters"', () => { + it('rejects wrong version with an error containing "build-structures"', () => { // Encode a valid catalog, then patch the version field to 2 - const buf = encodeClusterCatalog(makeCatalog()); + const buf = encodeStructureCatalog(makeCatalog()); new DataView(buf).setUint32(4, 2, true); - expect(() => decodeClusterCatalog(buf)).toThrow(/build-clusters/); + expect(() => decodeStructureCatalog(buf)).toThrow(/build-structures/); }); it('decode rejects a truncated buffer', () => { // Encode a valid 2-record catalog, then lop off the last 4 bytes so the // second record is incomplete. Decode must throw rather than silently // reading zeros from beyond the buffer end. - const buf = encodeClusterCatalog(makeCatalog()); + const buf = encodeStructureCatalog(makeCatalog()); const truncated = buf.slice(0, buf.byteLength - 4); - expect(() => decodeClusterCatalog(truncated)).toThrow(/truncated/); + expect(() => decodeStructureCatalog(truncated)).toThrow(/truncated/); }); - it('encodeClusterCatalog rejects mismatched array lengths', () => { + it('encodeStructureCatalog rejects mismatched array lengths', () => { // count=2 but physicalRadiusMpc has only 1 element — encoder must throw. - const bad: ClusterCatalog = { + const bad: StructureCatalog = { count: 2, positions: new Float32Array([1, 2, 3, 4, 5, 6]), physicalRadiusMpc: new Float32Array([1]), // wrong: should be length 2 @@ -145,11 +145,11 @@ describe('encode/decode cluster catalog v1 (CCAT)', () => { significance: new Float32Array([5e14, 64]), category: new Uint8Array([0, 1]), }; - expect(() => encodeClusterCatalog(bad)).toThrow('physicalRadiusMpc length mismatch'); + expect(() => encodeStructureCatalog(bad)).toThrow('physicalRadiusMpc length mismatch'); }); - it('emptyClusterCatalog has count 0 and zero-length typed arrays', () => { - const empty = emptyClusterCatalog(); + it('emptyStructureCatalog has count 0 and zero-length typed arrays', () => { + const empty = emptyStructureCatalog(); expect(empty.count).toBe(0); expect(empty.positions.length).toBe(0); expect(empty.physicalRadiusMpc.length).toBe(0); diff --git a/tests/services/engine/frame/passes/passes.test.ts b/tests/services/engine/frame/passes/passes.test.ts index a2ac932a0..1829c0e6d 100644 --- a/tests/services/engine/frame/passes/passes.test.ts +++ b/tests/services/engine/frame/passes/passes.test.ts @@ -167,7 +167,7 @@ describe('HDR_PASSES registry', () => { // escape tone-map curve compression and dodge the OVER-blend // coherency issue on tile-based GPUs. The horizon shell draws after // the volume upsample (so cosmic-web densities composite over it) - // and before cluster-markers (so marker rings pop on top). Flow sits + // and before structure-markers (so marker rings pop on top). Flow sits // with the structure layers, after filaments. expect(HDR_PASSES).toHaveLength(9); expect(HDR_PASSES.map((p) => p.name)).toEqual([ @@ -179,7 +179,7 @@ describe('HDR_PASSES registry', () => { 'flow', 'volume-upsample', 'horizon-shell', - 'cluster-markers', + 'structure-markers', ]); }); }); @@ -196,8 +196,8 @@ describe('TIMED_SLOT_NAMES registry', () => { 'ui-overlay', 'pick', ]); - // cluster-markers is present purely by virtue of being in HDR_PASSES. - expect(TIMED_SLOT_NAMES).toContain('cluster-markers'); + // structure-markers is present purely by virtue of being in HDR_PASSES. + expect(TIMED_SLOT_NAMES).toContain('structure-markers'); }); it('has unique slot names (no index-pair collisions downstream)', () => { diff --git a/tests/services/engine/frame/renderFrame.test.ts b/tests/services/engine/frame/renderFrame.test.ts index d143b8cb0..d9ca89a10 100644 --- a/tests/services/engine/frame/renderFrame.test.ts +++ b/tests/services/engine/frame/renderFrame.test.ts @@ -335,7 +335,7 @@ function makeInput( selectionRingRenderer: null, scalarVolumeRenderer: null, flowFieldRenderer: null, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, focusUniform: { bindGroup: {}, write: () => {}, destroy: () => {} }, }, // encodeFlowCompute (pre-HDR) reads these; flow is default-off so the diff --git a/tests/services/engine/frame/renderFrame.timing.test.ts b/tests/services/engine/frame/renderFrame.timing.test.ts index c1f797b45..f5e98cce6 100644 --- a/tests/services/engine/frame/renderFrame.timing.test.ts +++ b/tests/services/engine/frame/renderFrame.timing.test.ts @@ -246,7 +246,7 @@ function makeMinimalInputWithTiming(timingService: GpuTimingService): { selectionRingRenderer: null, scalarVolumeRenderer: null, flowFieldRenderer: null, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, focusUniform: { bindGroup: {}, write: () => {}, destroy: () => {} }, }, // encodeFlowCompute (pre-HDR) reads these; default-off → gate returns. diff --git a/tests/services/engine/helpers/collectPickTargets.test.ts b/tests/services/engine/helpers/collectPickTargets.test.ts index 285e185ca..938bc6ae8 100644 --- a/tests/services/engine/helpers/collectPickTargets.test.ts +++ b/tests/services/engine/helpers/collectPickTargets.test.ts @@ -2,7 +2,7 @@ * collectPickTargets — the unified "what's pickable this frame" gate. * * The regression that motivated the helper: clusters draw into the pick - * texture via clusterMarkerRenderer.pickRing but are NOT galaxy surveys, + * texture via structureMarkerRenderer.pickRing but are NOT galaxy surveys, * so a gate that bailed on "no visible galaxy sources" made clusters * unpickable (and the pick-debug overlay black) whenever every galaxy * survey was toggled off. The `hasAny` flag must stay true on a @@ -12,7 +12,7 @@ import { describe, it, expect } from 'vitest'; import { collectPickTargets } from '../../../../src/services/engine/helpers/collectPickTargets'; import type { PointRenderer } from '../../../../src/@types/rendering/PointRenderer'; -import type { ClusterMarkerRenderer } from '../../../../src/@types/rendering/ClusterMarkerRenderer'; +import type { StructureMarkerRenderer } from '../../../../src/@types/rendering/StructureMarkerRenderer'; import type { PickSourceDraw } from '../../../../src/@types/rendering/PickSourceDraw'; import type { SourceType } from '../../../../src/@types/data/SourceType'; import { Source } from '../../../../src/data/sources'; @@ -37,8 +37,8 @@ function makeRenderer(sources: readonly SourceType[]): PointRenderer { } as unknown as PointRenderer; } -function makeClusterRenderer(markerCount: number): ClusterMarkerRenderer { - return { markerCount: () => markerCount } as unknown as ClusterMarkerRenderer; +function makeStructureMarkerRenderer(markerCount: number): StructureMarkerRenderer { + return { markerCount: () => markerCount } as unknown as StructureMarkerRenderer; } // pickMask bit for a source code. @@ -70,7 +70,7 @@ describe('collectPickTargets', () => { const { visibleSources, hasAny } = collectPickTargets( renderer, mask(), // every survey toggled off - makeClusterRenderer(42), // 42 cluster rings queued + makeStructureMarkerRenderer(42), // 42 structure rings queued ); expect(visibleSources).toHaveLength(0); expect(hasAny).toBe(true); @@ -78,13 +78,17 @@ describe('collectPickTargets', () => { it('hasAny is false when the cluster renderer exists but has zero markers (category hidden / all faded)', () => { const renderer = makeRenderer([Source.SDSS]); - const { hasAny } = collectPickTargets(renderer, mask(), makeClusterRenderer(0)); + const { hasAny } = collectPickTargets(renderer, mask(), makeStructureMarkerRenderer(0)); expect(hasAny).toBe(false); }); it('hasAny is true when both galaxy surveys and cluster markers are present', () => { const renderer = makeRenderer([Source.SDSS]); - const { hasAny } = collectPickTargets(renderer, mask(Source.SDSS), makeClusterRenderer(5)); + const { hasAny } = collectPickTargets( + renderer, + mask(Source.SDSS), + makeStructureMarkerRenderer(5), + ); expect(hasAny).toBe(true); }); }); diff --git a/tests/services/engine/integrationMarkerWire.test.ts b/tests/services/engine/integrationMarkerWire.test.ts index 2ce75c241..5467616d0 100644 --- a/tests/services/engine/integrationMarkerWire.test.ts +++ b/tests/services/engine/integrationMarkerWire.test.ts @@ -1,5 +1,5 @@ /** - * Integration test — produceStructureMarkers → clusterMarkerRenderer round-trip. + * Integration test — produceStructureMarkers → structureMarkerRenderer round-trip. * * The point of this test is NOT to exercise either side in isolation (their * dedicated unit suites already do that), but to assert the contract BETWEEN @@ -8,14 +8,14 @@ * producer chose to emit. * * Why this matters: produceStructureMarkers and setMarkers were authored - * against the same ClusterMarkerDescriptor type, but TypeScript only catches + * against the same StructureMarkerDescriptor type, but TypeScript only catches * shape mismatches at the boundary if both call sites are wired in the same * translation unit. This test is the smallest possible end-to-end smoke check * that catches drift — e.g. a renamed field on the descriptor type would * compile in both modules' unit tests but still mismatch at runtime. * * The "null device" pattern is the standard renderer test idiom in this repo: - * createClusterMarkerRenderer's GPU allocations are all guarded by `if + * createStructureMarkerRenderer's GPU allocations are all guarded by `if * (device)`, so the renderer is safe to construct with `null` cast to * GPUDevice — setMarkers/markerCount run their CPU-side bookkeeping without * touching the GPU. @@ -24,16 +24,16 @@ import { describe, it, expect } from 'vitest'; import { produceStructureMarkers } from '../../../src/services/engine/presentation/produceStructureMarkers'; import { createEngineData } from '../../../src/services/engine/data/createEngineData'; -import { createClusterMarkerRenderer } from '../../../src/services/gpu/renderers/clusterMarkerRenderer'; +import { createStructureMarkerRenderer } from '../../../src/services/gpu/renderers/structureMarkerRenderer'; import { createFadeRegistry } from '../../../src/services/animation/fadeRegistry'; import type { StructureRecord } from '../../../src/@types/engine/data/StructureRecord'; import type { EngineState } from '../../../src/@types/engine/state/EngineState'; import type { ReadyFrameContext } from '../../../src/@types/engine/frame/ReadyFrameContext'; import type { FadeUniformsBgl } from '../../../src/@types/rendering/FadeUniformsBgl'; -describe('produceStructureMarkers → clusterMarkerRenderer.setMarkers', () => { +describe('produceStructureMarkers → structureMarkerRenderer.setMarkers', () => { it('the renderer reports the same marker count the producer emitted', () => { - const renderer = createClusterMarkerRenderer( + const renderer = createStructureMarkerRenderer( { device: null as unknown as GPUDevice, context: null as unknown as GPUCanvasContext, diff --git a/tests/services/engine/phases/initGpu.destroyReachability.test.ts b/tests/services/engine/phases/initGpu.destroyReachability.test.ts index 9143b82ef..5ec95bf49 100644 --- a/tests/services/engine/phases/initGpu.destroyReachability.test.ts +++ b/tests/services/engine/phases/initGpu.destroyReachability.test.ts @@ -135,8 +135,8 @@ vi.mock('../../../../src/services/gpu/renderers/selectionRingRenderer', () => ({ createSelectionRingRenderer: vi.fn(() => makeStub('selectionRingRenderer')), })); -vi.mock('../../../../src/services/gpu/renderers/clusterMarkerRenderer', () => ({ - createClusterMarkerRenderer: vi.fn(() => makeStub('clusterMarkerRenderer')), +vi.mock('../../../../src/services/gpu/renderers/structureMarkerRenderer', () => ({ + createStructureMarkerRenderer: vi.fn(() => makeStub('structureMarkerRenderer')), })); vi.mock('../../../../src/services/gpu/renderers/scalarVolumeRenderer', () => ({ @@ -194,7 +194,7 @@ function makeState(): EngineState { labelRenderer: null, markerLineRenderer: null, selectionRingRenderer: null, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, texturedDiskRenderer: null, proceduralDiskRenderer: null, milkyWayRenderer: null, diff --git a/tests/services/engine/phases/clusterCatalogToStructures.test.ts b/tests/services/engine/phases/structureCatalogToStructures.test.ts similarity index 85% rename from tests/services/engine/phases/clusterCatalogToStructures.test.ts rename to tests/services/engine/phases/structureCatalogToStructures.test.ts index f1219d73c..70c0bdab9 100644 --- a/tests/services/engine/phases/clusterCatalogToStructures.test.ts +++ b/tests/services/engine/phases/structureCatalogToStructures.test.ts @@ -1,5 +1,5 @@ /** - * clusterCatalogToStructures — tests for the bulk cluster/supercluster + * structureCatalogToStructures — tests for the bulk cluster/supercluster * POI producer. * * The producer turns the decoded `.ccat` + meta sidecar into the @@ -18,14 +18,14 @@ * featured `${category}-${seed.id}` anchors and are recognisably * non-deep-linkable. * - * Fixtures are hand-built `ClusterCatalogPayload`s — the producer is a + * Fixtures are hand-built `StructureCatalogPayload`s — the producer is a * pure function over typed arrays, so no engine boot is needed. */ import { describe, it, expect } from 'vitest'; -import { clusterCatalogToStructures } from '../../../../src/services/engine/phases/clusterCatalogToStructures'; -import type { ClusterCatalogPayload } from '../../../../src/@types/loading/ClusterCatalogPayload'; -import type { ClusterMetaEntry } from '../../../../src/@types/loading/ClusterCatalogPayload'; +import { structureCatalogToStructures } from '../../../../src/services/engine/phases/structureCatalogToStructures'; +import type { StructureCatalogPayload } from '../../../../src/@types/loading/StructureCatalogPayload'; +import type { StructureMetaEntry } from '../../../../src/@types/loading/StructureCatalogPayload'; /** * Build a payload from per-record specs. `significance` is the RAW @@ -39,9 +39,9 @@ function makePayload( apparentRadiusMpc: number; significance: number; category: number; - meta: ClusterMetaEntry; + meta: StructureMetaEntry; }>, -): ClusterCatalogPayload { +): StructureCatalogPayload { const count = records.length; const positions = new Float32Array(count * 3); const physicalRadiusMpc = new Float32Array(count); @@ -63,7 +63,7 @@ function makePayload( }; } -const meta = (id: string, abell: string | null = null, description = ''): ClusterMetaEntry => ({ +const meta = (id: string, abell: string | null = null, description = ''): StructureMetaEntry => ({ id, names: [id.toUpperCase()], abell, @@ -74,7 +74,7 @@ const meta = (id: string, abell: string | null = null, description = ''): Cluste // superclusters (byte 1) of differing N_m. The cluster M500 values are // orders of magnitude larger than the SC member counts, which is the // whole point of normalising the two categories independently. -function mixedPayload(): ClusterCatalogPayload { +function mixedPayload(): StructureCatalogPayload { return makePayload([ { pos: [1, 2, 3], @@ -111,20 +111,20 @@ function mixedPayload(): ClusterCatalogPayload { ]); } -describe('clusterCatalogToStructures', () => { +describe('structureCatalogToStructures', () => { it('maps category bytes to cluster/supercluster', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); expect(pois.filter((p) => p.category === 'cluster')).toHaveLength(2); expect(pois.filter((p) => p.category === 'supercluster')).toHaveLength(2); }); it('marks every POI not featured', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); expect(pois.every((p) => p.featured === false)).toBe(true); }); it('carries worldPos + radii through from the catalog', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); const low = pois.find((p) => p.id.includes('low-cluster'))!; expect(low.worldPos).toEqual([1, 2, 3]); expect(low.category === 'cluster' && low.physicalRadiusMpc).toBe(1.5); @@ -132,7 +132,7 @@ describe('clusterCatalogToStructures', () => { }); it('normalizes significance per-category into [0,1] on independent scales', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); const sig = (idFrag: string) => { const p = pois.find((q) => q.id.includes(idFrag))!; // significance lives on the extended-structure arms. @@ -160,14 +160,14 @@ describe('clusterCatalogToStructures', () => { }); it('ids are prefixed bulk and never collide with featured slugs', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); for (const p of pois) { expect(p.id).toMatch(/^(cluster|supercluster)-bulk-/); } }); it('carries the abell designation from meta onto the cluster arm only', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); const high = pois.find((p) => p.id.includes('high-cluster'))!; expect(high.category).toBe('cluster'); expect(high.category === 'cluster' && high.abell).toBe('A2670'); @@ -187,7 +187,7 @@ describe('clusterCatalogToStructures', () => { meta: meta('described-cluster', 'A1', 'X-ray cluster · M500 = 5.0×10¹⁴ M☉ · z = 0.040'), }, ]); - const poi = clusterCatalogToStructures(payload)[0]!; + const poi = structureCatalogToStructures(payload)[0]!; expect(poi.description).toBe('X-ray cluster · M500 = 5.0×10¹⁴ M☉ · z = 0.040'); }); @@ -202,12 +202,12 @@ describe('clusterCatalogToStructures', () => { meta: meta('no-abell-cluster', null), }, ]); - const pois = clusterCatalogToStructures(payload); + const pois = structureCatalogToStructures(payload); expect('abell' in pois[0]!).toBe(false); }); it('names the POI from meta.names[0]', () => { - const pois = clusterCatalogToStructures(mixedPayload()); + const pois = structureCatalogToStructures(mixedPayload()); const high = pois.find((p) => p.id.includes('high-cluster'))!; expect(high.name).toBe('HIGH-CLUSTER'); }); @@ -231,7 +231,7 @@ describe('clusterCatalogToStructures', () => { meta: meta('reserved'), }, ]); - const pois = clusterCatalogToStructures(payload); + const pois = structureCatalogToStructures(payload); expect(pois).toHaveLength(1); expect(pois[0]!.id).toContain('ok-cluster'); }); @@ -248,13 +248,13 @@ describe('clusterCatalogToStructures', () => { meta: meta('solo-cluster'), }, ]); - const pois = clusterCatalogToStructures(payload); + const pois = structureCatalogToStructures(payload); const p = pois[0]!; expect(p.category === 'cluster' && p.significance).toBeCloseTo(1); }); it('returns an empty list for an empty catalog', () => { const payload = makePayload([]); - expect(clusterCatalogToStructures(payload)).toEqual([]); + expect(structureCatalogToStructures(payload)).toEqual([]); }); }); diff --git a/tests/services/engine/phases/wireSlots.test.ts b/tests/services/engine/phases/wireSlots.test.ts index 9143b009a..9a2ee19fd 100644 --- a/tests/services/engine/phases/wireSlots.test.ts +++ b/tests/services/engine/phases/wireSlots.test.ts @@ -26,7 +26,7 @@ * subsystem is assigned onto `state.subsystems.*`, every overlay / * volume-master / label-layer fade handle is registered at its frame-1 * opacity, and the structures-visibility predicate threads through to the - * demand loop (clusterCatalog loads at the visible default, skips when all + * demand loop (structureCatalog loads at the visible default, skips when all * structure categories are hidden). * * Mocking strategy: real `AssetSlot` instances are kept (pure CPU state @@ -82,12 +82,12 @@ vi.mock('../../../../src/services/loading/fetchers/famousMetaFetcher', () => ({ famousMetaFetcher: vi.fn(async () => ({ meta: [] })), })); -// The cluster-catalog slot fires `.load({})` at boot; mock its fetcher so +// The structure-catalog slot fires `.load({})` at boot; mock its fetcher so // the test doesn't network. An empty catalog is enough by default — the // merge test overrides this mock with a populated payload so wireStructureProjection // builds bulk records from the slot's ready value. -vi.mock('../../../../src/services/loading/fetchers/clusterCatalogFetcher', () => ({ - clusterCatalogFetcher: vi.fn(async () => ({ +vi.mock('../../../../src/services/loading/fetchers/structureCatalogFetcher', () => ({ + structureCatalogFetcher: vi.fn(async () => ({ catalog: { count: 0, positions: new Float32Array(0), @@ -203,7 +203,7 @@ vi.mock('../../../../src/services/engine/subsystems/loadProgressAggregator', () // Imported AFTER the mocks so wireSlots picks them up. import { wireSlots } from '../../../../src/services/engine/phases/wireSlots'; import { famousMetaFetcher } from '../../../../src/services/loading/fetchers/famousMetaFetcher'; -import { clusterCatalogFetcher } from '../../../../src/services/loading/fetchers/clusterCatalogFetcher'; +import { structureCatalogFetcher } from '../../../../src/services/loading/fetchers/structureCatalogFetcher'; import { mcpmFetcher } from '../../../../src/services/loading/fetchers/mcpmFetcher'; import { filamentFetcher } from '../../../../src/services/loading/fetchers/filamentFetcher'; import { cf4DensityFetcher } from '../../../../src/services/loading/fetchers/cf4DensityFetcher'; @@ -325,9 +325,9 @@ function makeState( milkyWay: { enabled: true }, filaments: { enabled: false, intensity: 1.0 }, volumes: { masterEnabled: true, fields: seedVolumeFields() }, - // Structure categories all visible by default ⇒ clusterCatalog demanded. + // Structure categories all visible by default ⇒ structureCatalog demanded. // Overridable so a test can hide every category and pin the bug-fix - // (clusterCatalog must NOT load when nothing structural is visible). + // (structureCatalog must NOT load when nothing structural is visible). markerCategoryVisibility: overrides.markerCategoryVisibility ?? allVisible, labelCategoryVisibility: overrides.labelCategoryVisibility ?? allVisible, }, @@ -395,7 +395,7 @@ function makeState( points: points as Map, filaments: null, famousMeta: null, - clusterCatalog: null, + structureCatalog: null, pgcAlias: null, cf4Density: null, mcpm: null, @@ -530,7 +530,7 @@ describe('wireSlots', () => { expect(opacityFor({ kind: 'labelLayer', layer: 'scaleBar' })).toBe(1); }); - it('demand loop loads the default boot sidecar set (mcpm + clusterCatalog + famousMeta) and not the off-by-default ones', async () => { + it('demand loop loads the default boot sidecar set (mcpm + structureCatalog + famousMeta) and not the off-by-default ones', async () => { // Boot parity: the old imperative boot loop loaded MCPM (default-on volume) // + the cluster catalog (structures visible) + famous-meta but left // filaments (off), CF-4 density (off) and the lazy PGC alias idle. After @@ -539,7 +539,7 @@ describe('wireSlots', () => { // observable through its (mocked) fetcher; clear them first since the // module-scoped mocks persist across tests. vi.mocked(mcpmFetcher).mockClear(); - vi.mocked(clusterCatalogFetcher).mockClear(); + vi.mocked(structureCatalogFetcher).mockClear(); vi.mocked(famousMetaFetcher).mockClear(); vi.mocked(filamentFetcher).mockClear(); vi.mocked(cf4DensityFetcher).mockClear(); @@ -552,7 +552,7 @@ describe('wireSlots', () => { // Default-on / structures-visible / famous-loading ⇒ fetched. expect(mcpmFetcher).toHaveBeenCalled(); - expect(clusterCatalogFetcher).toHaveBeenCalled(); + expect(structureCatalogFetcher).toHaveBeenCalled(); expect(famousMetaFetcher).toHaveBeenCalled(); // Default-off / lazy ⇒ never fetched at boot. expect(filamentFetcher).not.toHaveBeenCalled(); @@ -560,14 +560,14 @@ describe('wireSlots', () => { expect(pgcAliasFetcher).not.toHaveBeenCalled(); }); - it('does not load clusterCatalog when every structure category is hidden (bug-fix integration pin)', async () => { + it('does not load structureCatalog when every structure category is hidden (bug-fix integration pin)', async () => { // demandTable.test.ts pins the cluster predicate in isolation; this pins // that wireSlots actually threads the visibility records THROUGH to the // demand loop end-to-end. Old code loaded the .ccat unconditionally; the // fix gates it on any structure category being visible. With both marker // and label visibility all-false the predicate is false, so the boot - // demand pass must skip clusterCatalog entirely. - vi.mocked(clusterCatalogFetcher).mockClear(); + // demand pass must skip structureCatalog entirely. + vi.mocked(structureCatalogFetcher).mockClear(); const allHidden = { cluster: false, supercluster: false, void: false, famousGalaxy: false }; const state = makeState({ @@ -579,7 +579,7 @@ describe('wireSlots', () => { await wireSlots(state, deps); - expect(clusterCatalogFetcher).not.toHaveBeenCalled(); + expect(structureCatalogFetcher).not.toHaveBeenCalled(); }); it('fires `ready` status with a running total each time a survey arrives', async () => { @@ -692,7 +692,7 @@ describe('wireSlots', () => { expect(names.has('famous-points')).toBe(true); expect(names.has('filaments')).toBe(true); expect(names.has('famous-meta')).toBe(true); - expect(names.has('cluster-catalog')).toBe(true); + expect(names.has('structure-catalog')).toBe(true); expect(names.has('pgc-aliases')).toBe(true); }); @@ -712,14 +712,14 @@ describe('wireSlots', () => { }); it('lands bulk clusters + superclusters in the structure store, keeping anchors', async () => { - // The cluster-catalog slot's boot load resolves with one cluster + one + // The structure-catalog slot's boot load resolves with one cluster + one // supercluster; wireStructureProjection builds bulk records (via the real - // clusterCatalogToStructures) and writes them to the structure store's + // structureCatalogToStructures) and writes them to the structure store's // 'bulk' group, alongside the static-anchor group from boot. delete (globalThis as { location?: unknown }).location; (globalThis as { location: { search: string } }).location = { search: '' }; - vi.mocked(clusterCatalogFetcher).mockResolvedValueOnce({ + vi.mocked(structureCatalogFetcher).mockResolvedValueOnce({ catalog: { count: 2, positions: new Float32Array([1, 2, 3, 4, 5, 6]), @@ -738,7 +738,7 @@ describe('wireSlots', () => { const state = makeState({ points: bootPointSlots() }); const deps = makeDeps(); await wireSlots(state, deps); - // Let the async cluster-catalog fetch + its subscriber settle. + // Let the async structure-catalog fetch + its subscriber settle. await new Promise((r) => setTimeout(r, 0)); // Bulk records land in the structure store. @@ -757,7 +757,7 @@ describe('wireSlots', () => { delete (globalThis as { location?: unknown }).location; (globalThis as { location: { search: string } }).location = { search: '' }; - vi.mocked(clusterCatalogFetcher).mockResolvedValueOnce({ + vi.mocked(structureCatalogFetcher).mockResolvedValueOnce({ catalog: { count: 2, positions: new Float32Array([1, 2, 3, 4, 5, 6]), @@ -773,7 +773,7 @@ describe('wireSlots', () => { } as never); // Idle survey fakes keep the synthetic gate waiting so demand runs once — - // a double-run would re-trigger the (non-idempotent) cluster-catalog load + // a double-run would re-trigger the (non-idempotent) structure-catalog load // and race the mock once-value against its empty default. const state = makeState({ points: bootPointSlots() }); const deps = makeDeps(); diff --git a/tests/services/engine/subsystems/clusterFocusSubsystem.test.ts b/tests/services/engine/subsystems/structureFocusSubsystem.test.ts similarity index 87% rename from tests/services/engine/subsystems/clusterFocusSubsystem.test.ts rename to tests/services/engine/subsystems/structureFocusSubsystem.test.ts index c58284555..70de93f53 100644 --- a/tests/services/engine/subsystems/clusterFocusSubsystem.test.ts +++ b/tests/services/engine/subsystems/structureFocusSubsystem.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { createClusterFocusSubsystem } from '../../../../src/services/engine/subsystems/clusterFocusSubsystem'; +import { createStructureFocusSubsystem } from '../../../../src/services/engine/subsystems/structureFocusSubsystem'; import type { StructureRecord } from '../../../../src/@types/engine/data/StructureRecord'; function makeCluster(overrides: Record = {}): StructureRecord { @@ -18,15 +18,15 @@ function makeVoid(overrides: Record = {}): StructureRecord { return makeCluster({ id: 'bootes', name: 'Boötes Void', category: 'void', ...overrides }); } -describe('clusterFocusSubsystem', () => { +describe('structureFocusSubsystem', () => { it('starts inactive with blend=0', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); expect(sub.produceFocusUniforms(0).blend).toBe(0); expect(sub.isAwake(0)).toBe(false); }); it('update with a cluster POI fades blend 0→1 with correct center/radii', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); sub.update(makeCluster({ worldPos: [3, 4, 5], physicalRadiusMpc: 7 }), 0); const mid = sub.produceFocusUniforms(200); expect(mid.blend).toBeGreaterThan(0); @@ -40,7 +40,7 @@ describe('clusterFocusSubsystem', () => { }); it('emits apparent (fade outer edge) and physical (core) radii independently', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); sub.update(makeCluster({ physicalRadiusMpc: 2, apparentRadiusMpc: 5 }), 0); const settled = sub.produceFocusUniforms(500); expect(settled.apparentRadiusMpc).toBe(5); @@ -51,7 +51,7 @@ describe('clusterFocusSubsystem', () => { // Voids share the cluster rule: galaxies inside the void's radius are // members (stay bright), everything else fades. The uniform carries // no per-category bit — just center + the two radii + blend. - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); sub.update(makeVoid({ worldPos: [1, 2, 3], physicalRadiusMpc: 9 }), 0); const settled = sub.produceFocusUniforms(500); expect(settled.blend).toBe(1); @@ -61,7 +61,7 @@ describe('clusterFocusSubsystem', () => { }); it('update(null) after a cluster fades blend 1→0 (and stays settling under per-frame calls)', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); sub.update(makeCluster(), 0); sub.produceFocusUniforms(500); // settle at 1 sub.update(null, 500); @@ -74,7 +74,7 @@ describe('clusterFocusSubsystem', () => { }); it('update with the same POI id is idempotent across frames (no re-fade restart)', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); const poi = makeCluster(); sub.update(poi, 0); sub.produceFocusUniforms(100); // mid fade-in @@ -85,7 +85,7 @@ describe('clusterFocusSubsystem', () => { }); it('replacing the focused POI does not pass through blend 0', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); sub.update(makeCluster({ id: 'virgo', worldPos: [10, 0, 0] }), 0); sub.produceFocusUniforms(500); // settle at 1 sub.update(makeCluster({ id: 'coma', worldPos: [-10, 0, 0] }), 600); @@ -96,7 +96,7 @@ describe('clusterFocusSubsystem', () => { }); it('isAwake is true mid-fade and false at rest', () => { - const sub = createClusterFocusSubsystem(0); + const sub = createStructureFocusSubsystem(0); expect(sub.isAwake(0)).toBe(false); sub.update(makeCluster(), 0); expect(sub.isAwake(200)).toBe(true); diff --git a/tests/services/engine/wiring/assetWiring.test.ts b/tests/services/engine/wiring/assetWiring.test.ts index 4245b492f..df6a4387d 100644 --- a/tests/services/engine/wiring/assetWiring.test.ts +++ b/tests/services/engine/wiring/assetWiring.test.ts @@ -9,7 +9,7 @@ * * Two of the predicates are bug-fix pins (see the module docstring on * `assetWiring.ts`): `filaments` follows `settings.filaments.enabled`, and - * `clusterCatalog` follows structure-category visibility (the plan's stale + * `structureCatalog` follows structure-category visibility (the plan's stale * `structures.enabled` flag does not exist). */ @@ -70,7 +70,7 @@ describe('ASSET_WIRING membership', () => { 'mcpm', 'cf4Density', 'flow', - 'clusterCatalog', + 'structureCatalog', 'pgcAlias', ]; expect(new Set(keys)).toEqual(new Set(expected)); @@ -158,8 +158,8 @@ describe('ASSET_WIRING demand predicates', () => { expect(flow.demand(makeCtx({ settings: { flow: { enabled: false } } }))).toBe(false); }); - it('clusterCatalog demand follows structure-category visibility (bug-fix pin)', () => { - const cluster = rowFor('clusterCatalog'); + it('structureCatalog demand follows structure-category visibility (bug-fix pin)', () => { + const cluster = rowFor('structureCatalog'); const allHidden = { markerCategoryVisibility: { cluster: false, @@ -243,8 +243,8 @@ describe('ASSET_WIRING req builders', () => { expect(rowFor('mcpm').req('large')).toEqual({ tier: 'large' }); }); - it('clusterCatalog req is the empty request', () => { - expect(rowFor('clusterCatalog').req('medium')).toEqual({}); + it('structureCatalog req is the empty request', () => { + expect(rowFor('structureCatalog').req('medium')).toEqual({}); }); it('void-request sidecars (cf4Density, pgcAlias) return undefined', () => { diff --git a/tests/services/engine/wiring/demandTable.test.ts b/tests/services/engine/wiring/demandTable.test.ts index 0b4d8b6ef..767edfdb1 100644 --- a/tests/services/engine/wiring/demandTable.test.ts +++ b/tests/services/engine/wiring/demandTable.test.ts @@ -160,7 +160,7 @@ type PointSlotOverrides = Partial>; type NamedSlotOverrides = Partial<{ famousMeta: StubSlot; filaments: StubSlot; - clusterCatalog: StubSlot; + structureCatalog: StubSlot; pgcAlias: StubSlot; cf4Density: StubSlot; mcpm: StubSlot; @@ -224,7 +224,7 @@ function makeState(opts: MakeStateOptions = {}): EngineState { points, filaments: (namedSlots.filaments ?? stubSlot()) as AssetSlot as never, famousMeta: (namedSlots.famousMeta ?? stubSlot()) as AssetSlot as never, - clusterCatalog: (namedSlots.clusterCatalog ?? stubSlot()) as AssetSlot< + structureCatalog: (namedSlots.structureCatalog ?? stubSlot()) as AssetSlot< unknown, unknown > as never, @@ -259,7 +259,7 @@ function firedKeys(state: EngineState): Set { const namedKeys = [ 'famousMeta', 'filaments', - 'clusterCatalog', + 'structureCatalog', 'pgcAlias', 'cf4Density', 'mcpm', @@ -283,13 +283,13 @@ describe('reevaluateDemand demand-table regression', () => { * Boot defaults: SDSS/2MRS/GLADE/Famous/Milliquas all visible (every survey * ships on in SOURCE_REGISTRY). Famous slot is modelled as 'loading' (it was * just triggered by its own demand row before famousMeta's row evaluates), so - * famousMeta is also demanded. clusterCatalog loads because every structure + * famousMeta is also demanded. structureCatalog loads because every structure * category is visible by default. mcpm IS demanded: the predicate checks * `ctx.volumeField('mcpm')?.enabled`, which the construction seed lands as * true (registry visible:true). cf4Density is NOT (seeded enabled:false). * filaments: off. pgcAlias: no request. Synthetic: surveys not errored. */ - it('boot defaults: SDSS + 2MRS + GLADE + Famous + Milliquas + famousMeta + clusterCatalog + mcpm', () => { + it('boot defaults: SDSS + 2MRS + GLADE + Famous + Milliquas + famousMeta + structureCatalog + mcpm', () => { // Famous starts idle: its point row loads it (idle-guard passes), flipping // the stub to 'loading', so the later famousMeta row sees Famous non-idle // and demands. This is the honest two-phase boot model. @@ -305,7 +305,7 @@ describe('reevaluateDemand demand-table regression', () => { Source.FamousGalaxy, Source.Milliquas, 'famousMeta', - 'clusterCatalog', + 'structureCatalog', 'mcpm', ]), ); @@ -332,7 +332,7 @@ describe('reevaluateDemand demand-table regression', () => { Source.FamousGalaxy, Source.Milliquas, 'famousMeta', - 'clusterCatalog', + 'structureCatalog', 'mcpm', 'filaments', ]), @@ -342,14 +342,14 @@ describe('reevaluateDemand demand-table regression', () => { /** * Structures all hidden: every category set to false in BOTH * markerCategoryVisibility and labelCategoryVisibility. - * Bug-fix pin: clusterCatalog must NOT appear. This verifies the + * Bug-fix pin: structureCatalog must NOT appear. This verifies the * consolidated predicate rather than the stale 'structures.enabled' flag. * * Famous starts idle and is in the drawMask, so its point row loads it and * famousMeta follows (the two-phase boot). The pin under test is the cluster * predicate, asserted independently below. */ - it('structures all hidden: no clusterCatalog (bug-fix pin)', () => { + it('structures all hidden: no structureCatalog (bug-fix pin)', () => { const settings: SettingsLeaves = { ...BOOT_SETTINGS, markerCategoryVisibility: { @@ -369,8 +369,8 @@ describe('reevaluateDemand demand-table regression', () => { const fired = firedKeys(state); - // clusterCatalog must be absent. - expect(fired.has('clusterCatalog')).toBe(false); + // structureCatalog must be absent. + expect(fired.has('structureCatalog')).toBe(false); // The three visible surveys are still demanded. expect(fired.has(Source.SDSS)).toBe(true); expect(fired.has(Source.TwoMRS)).toBe(true); @@ -397,7 +397,7 @@ describe('reevaluateDemand demand-table regression', () => { Source.FamousGalaxy, Source.Milliquas, 'famousMeta', - 'clusterCatalog', + 'structureCatalog', 'mcpm', 'pgcAlias', ]), @@ -415,7 +415,7 @@ describe('reevaluateDemand demand-table regression', () => { * contrast, are NOT re-loaded: the idle-guard skips non-idle slots, which is * the desired no-retry-storm behaviour (a re-eval must not abort + re-fetch * failed surveys). famousMeta still demands because Famous slot !== 'idle'; - * clusterCatalog is still demanded (categories visible). + * structureCatalog is still demanded (categories visible). */ it('synthetic fallback armed: Synthetic loads, errored surveys are not retried', () => { const pointSlots: PointSlotOverrides = { @@ -436,8 +436,8 @@ describe('reevaluateDemand demand-table regression', () => { expect(fired.has(Source.Synthetic)).toBe(true); // famousMeta is demanded (Famous slot !== 'idle'). expect(fired.has('famousMeta')).toBe(true); - // clusterCatalog still demanded (structure visibility unchanged). - expect(fired.has('clusterCatalog')).toBe(true); + // structureCatalog still demanded (structure visibility unchanged). + expect(fired.has('structureCatalog')).toBe(true); // The errored survey point rows are demanded (still visible) but NOT idle, // so the idle-guard leaves them alone — no retry storm on re-evaluation. expect(fired.has(Source.SDSS)).toBe(false); @@ -473,7 +473,7 @@ describe('reevaluateDemand demand-table regression', () => { Source.FamousGalaxy, Source.Milliquas, 'famousMeta', - 'clusterCatalog', + 'structureCatalog', 'mcpm', 'cf4Density', ]), @@ -493,7 +493,7 @@ describe('reevaluateDemand demand-table regression', () => { it('famous-only visible: one pass loads Famous + famousMeta together', () => { const settings: SettingsLeaves = { ...BOOT_SETTINGS, - // Hide every structure category so clusterCatalog stays out of the set + // Hide every structure category so structureCatalog stays out of the set // and the assertion is purely the Famous companion join. markerCategoryVisibility: { cluster: false, diff --git a/tests/services/engine/wiring/installLoadProgress.test.ts b/tests/services/engine/wiring/installLoadProgress.test.ts index 2584cce08..11aef1a1e 100644 --- a/tests/services/engine/wiring/installLoadProgress.test.ts +++ b/tests/services/engine/wiring/installLoadProgress.test.ts @@ -56,7 +56,7 @@ function makeState(): EngineState { points, filaments: stubSlot('filaments'), famousMeta: stubSlot('famous-meta'), - clusterCatalog: stubSlot('cluster-catalog'), + structureCatalog: stubSlot('structure-catalog'), pgcAlias: stubSlot('pgc-aliases'), cf4Density: stubSlot('cf4Density'), mcpm: stubSlot('mcpm'), @@ -98,7 +98,7 @@ describe('installLoadProgress', () => { expect(names.has('2mrs-points')).toBe(true); expect(names.has('filaments')).toBe(true); expect(names.has('famous-meta')).toBe(true); - expect(names.has('cluster-catalog')).toBe(true); + expect(names.has('structure-catalog')).toBe(true); expect(names.has('pgc-aliases')).toBe(true); expect(names.has('cf4Density')).toBe(true); expect(names.has('mcpm')).toBe(true); diff --git a/tests/services/engine/wiring/installSlots.test.ts b/tests/services/engine/wiring/installSlots.test.ts index 2542b716a..93c904579 100644 --- a/tests/services/engine/wiring/installSlots.test.ts +++ b/tests/services/engine/wiring/installSlots.test.ts @@ -38,7 +38,7 @@ function makeState(): EngineState { points, filaments: null, famousMeta: null, - clusterCatalog: null, + structureCatalog: null, pgcAlias: null, cf4Density: null, mcpm: null, @@ -51,14 +51,14 @@ describe('installSlots', () => { const state = makeState(); const filaments = stubSlot('filaments'); const famousMeta = stubSlot('famous-meta'); - const clusterCatalog = stubSlot('cluster-catalog'); + const structureCatalog = stubSlot('structure-catalog'); const pgcAlias = stubSlot('pgc-aliases'); const cf4Density = stubSlot('cf4Density'); const mcpm = stubSlot('mcpm'); const slots = new Map>([ ['filaments', filaments], ['famousMeta', famousMeta], - ['clusterCatalog', clusterCatalog], + ['structureCatalog', structureCatalog], ['pgcAlias', pgcAlias], ['cf4Density', cf4Density], ['mcpm', mcpm], @@ -68,7 +68,7 @@ describe('installSlots', () => { expect(state.assetSlots.filaments).toBe(filaments); expect(state.assetSlots.famousMeta).toBe(famousMeta); - expect(state.assetSlots.clusterCatalog).toBe(clusterCatalog); + expect(state.assetSlots.structureCatalog).toBe(structureCatalog); expect(state.assetSlots.pgcAlias).toBe(pgcAlias); expect(state.assetSlots.cf4Density).toBe(cf4Density); expect(state.assetSlots.mcpm).toBe(mcpm); diff --git a/tests/services/engine/wiring/wireStructureProjection.test.ts b/tests/services/engine/wiring/wireStructureProjection.test.ts index 0cb77f887..9f412cabf 100644 --- a/tests/services/engine/wiring/wireStructureProjection.test.ts +++ b/tests/services/engine/wiring/wireStructureProjection.test.ts @@ -7,7 +7,7 @@ * 1. Static anchors publish synchronously into the structure store — no * async arrival needed; `byCategory('cluster')` is non-empty after the * call so the Structures panel has counts from frame 1. - * 2. The bulk cluster group lands in the store when the cluster-catalog + * 2. The bulk cluster group lands in the store when the structure-catalog * slot fires, without clobbering the anchors group. * 3. `onStructureCountsChange` fires after any group change with fresh * per-category counts. @@ -19,7 +19,7 @@ * ### Mocking strategy * * `buildStaticAnchorStructures` is mocked to a deterministic minimal list so tests - * don't depend on the curated JSON. `clusterCatalogToStructures` is mocked to + * don't depend on the curated JSON. `structureCatalogToStructures` is mocked to * one record per meta entry. The structure store is a real `createEngineData` * instance so `setGroup` / `byCategory` behave exactly as production. The * cluster slot is a light fake with a `fire` helper. @@ -31,7 +31,7 @@ import type { EngineState } from '../../../../src/@types/engine/state/EngineStat import type { EngineCallbacks } from '../../../../src/@types/engine/EngineCallbacks'; import type { LoadState } from '../../../../src/@types/loading/LoadState'; import type { StructureRecord } from '../../../../src/@types/engine/data/StructureRecord'; -import type { ClusterCatalogPayload } from '../../../../src/@types/loading/ClusterCatalogPayload'; +import type { StructureCatalogPayload } from '../../../../src/@types/loading/StructureCatalogPayload'; // ── Module mocks ─────────────────────────────────────────────────────── @@ -73,10 +73,10 @@ vi.mock('../../../../src/data/buildStaticAnchorStructures', () => ({ ]), })); -// clusterCatalogToStructures: one record per entry in payload.meta. -vi.mock('../../../../src/services/engine/phases/clusterCatalogToStructures', () => ({ - clusterCatalogToStructures: vi.fn( - (payload: ClusterCatalogPayload): StructureRecord[] => +// structureCatalogToStructures: one record per entry in payload.meta. +vi.mock('../../../../src/services/engine/phases/structureCatalogToStructures', () => ({ + structureCatalogToStructures: vi.fn( + (payload: StructureCatalogPayload): StructureRecord[] => payload.meta.map((m) => ({ id: `cluster-bulk-${m.id}`, name: m.names[0], @@ -119,14 +119,17 @@ function readyState(value: V): LoadState { // ── State builder ────────────────────────────────────────────────────── -function makeState(): { state: EngineState; clusterCatalogSlot: FakeSlot } { - const clusterCatalogSlot = makeSlot(); +function makeState(): { + state: EngineState; + structureCatalogSlot: FakeSlot; +} { + const structureCatalogSlot = makeSlot(); const state = { data: createEngineData(), assetSlots: { - clusterCatalog: { - name: 'cluster-catalog', - subscribe: clusterCatalogSlot.subscribe, + structureCatalog: { + name: 'structure-catalog', + subscribe: structureCatalogSlot.subscribe, load: vi.fn(), state: () => ({ kind: 'idle' }), current: () => null, @@ -135,7 +138,7 @@ function makeState(): { state: EngineState; clusterCatalogSlot: FakeSlot } { @@ -144,7 +147,7 @@ function makeCb(): { cb: EngineCallbacks; countsSpy: ReturnType } return { cb, countsSpy }; } -const clusterPayload: ClusterCatalogPayload = { +const clusterPayload: StructureCatalogPayload = { catalog: { count: 1, positions: new Float32Array([1, 2, 3]), @@ -176,11 +179,11 @@ describe('wireStructureProjection', () => { }); it('lands the bulk cluster group when the slot fires, keeping the anchors', () => { - const { state, clusterCatalogSlot } = makeState(); + const { state, structureCatalogSlot } = makeState(); const { cb } = makeCb(); wireStructureProjection(state, cb); - clusterCatalogSlot.fire(readyState(clusterPayload)); + structureCatalogSlot.fire(readyState(clusterPayload)); expect(state.data.structures.byId('cluster-bulk-coma')?.id).toBe('cluster-bulk-coma'); // Anchors survive the bulk write (separate group). @@ -188,15 +191,15 @@ describe('wireStructureProjection', () => { }); it('clears the bulk group and re-emits counts on a slot error', () => { - const { state, clusterCatalogSlot } = makeState(); + const { state, structureCatalogSlot } = makeState(); const { cb, countsSpy } = makeCb(); wireStructureProjection(state, cb); - clusterCatalogSlot.fire(readyState(clusterPayload)); + structureCatalogSlot.fire(readyState(clusterPayload)); expect(state.data.structures.byId('cluster-bulk-coma')).not.toBeNull(); countsSpy.mockClear(); - clusterCatalogSlot.fire({ + structureCatalogSlot.fire({ kind: 'error', req: {}, error: new Error('fetch failed'), @@ -207,7 +210,7 @@ describe('wireStructureProjection', () => { }); it('emits onStructureCountsChange with per-category counts after a group change', () => { - const { state, clusterCatalogSlot } = makeState(); + const { state, structureCatalogSlot } = makeState(); const { cb, countsSpy } = makeCb(); wireStructureProjection(state, cb); @@ -222,7 +225,7 @@ describe('wireStructureProjection', () => { // Settings panel renders its toggle with no count. expect(bootCounts.group).toBe(1); - clusterCatalogSlot.fire(readyState(clusterPayload)); + structureCatalogSlot.fire(readyState(clusterPayload)); expect(countsSpy).toHaveBeenCalledTimes(2); const afterCluster = countsSpy.mock.calls[1]![0] as Record; diff --git a/tests/services/gpu/renderers/pickRenderer.poi.test.ts b/tests/services/gpu/renderers/pickRenderer.poi.test.ts index b43e0d021..f05bcb7ff 100644 --- a/tests/services/gpu/renderers/pickRenderer.poi.test.ts +++ b/tests/services/gpu/renderers/pickRenderer.poi.test.ts @@ -1,12 +1,12 @@ /** * pickRenderer.poi.test — type-level contract that `createPickRenderer` - * keeps `clusterMarkerRenderer` as its OPTIONAL tail positional argument + * keeps `structureMarkerRenderer` as its OPTIONAL tail positional argument * (the 7th, index 6, after the required `focusBgl` at index 4 and the * required shared `focusBindGroup` at index 5). * * Why type-only rather than a GPU integration test? The pick pass needs * a live `GPUDevice` plus constructed `PointRenderer` + - * `ClusterMarkerRenderer` to exercise end-to-end — beyond what Vitest can + * `StructureMarkerRenderer` to exercise end-to-end — beyond what Vitest can * stand up in Node. What we can lock down is the signature shape: dropping * the optional marker, making it required, or reordering positional args * breaks the type assertions below at type-check time (vitest runs tsc). @@ -16,7 +16,7 @@ import { describe, it, expect } from 'vitest'; import { createPickRenderer } from '../../../../src/services/gpu/renderers/pickRenderer'; describe('createPickRenderer POI integration', () => { - it('keeps clusterMarkerRenderer optional as the 7th positional', () => { + it('keeps structureMarkerRenderer optional as the 7th positional', () => { // Compile-time check: the 7th parameter must exist and must be // assignable from `undefined` (i.e. declared with `?`). If a // future edit removes the param or makes it required, the diff --git a/tests/services/gpu/renderers/clusterMarkerRenderer.pick.test.ts b/tests/services/gpu/renderers/structureMarkerRenderer.pick.test.ts similarity index 68% rename from tests/services/gpu/renderers/clusterMarkerRenderer.pick.test.ts rename to tests/services/gpu/renderers/structureMarkerRenderer.pick.test.ts index 20901bca6..4cd96cecf 100644 --- a/tests/services/gpu/renderers/clusterMarkerRenderer.pick.test.ts +++ b/tests/services/gpu/renderers/structureMarkerRenderer.pick.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import type { ClusterMarkerRenderer } from '../../../../src/@types/rendering/ClusterMarkerRenderer'; -import type { ClusterMarkerDescriptor } from '../../../../src/@types/rendering/ClusterMarkerDescriptor'; +import type { StructureMarkerRenderer } from '../../../../src/@types/rendering/StructureMarkerRenderer'; +import type { StructureMarkerDescriptor } from '../../../../src/@types/rendering/StructureMarkerDescriptor'; // Type-level assertion that the renderer's public surface declares the // `pickRing` method introduced in plan 3. The GPU side cannot be unit- @@ -9,25 +9,25 @@ import type { ClusterMarkerDescriptor } from '../../../../src/@types/rendering/C // // If the type loses `pickRing` (or the signature drifts), this file fails // to type-check and `npm test` fails before any expect() runs. -describe('ClusterMarkerRenderer pick API', () => { +describe('StructureMarkerRenderer pick API', () => { it('declares a pickRing method on the type', () => { // Indexed-access on the type itself: if `pickRing` is missing, - // `ClusterMarkerRenderer['pickRing']` is a compile-time error and + // `StructureMarkerRenderer['pickRing']` is a compile-time error and // the file refuses to typecheck. Capture the type at runtime via // a typeof binding so we can assert at least one shape constraint // (a function accepting a GPURenderPassEncoder). - type PickRingFn = ClusterMarkerRenderer['pickRing']; + type PickRingFn = StructureMarkerRenderer['pickRing']; const fn: PickRingFn = (_pass) => { void _pass; }; expect(fn).toBeTypeOf('function'); }); - it('ClusterMarkerDescriptor accepts category group (type gate)', () => { + it('StructureMarkerDescriptor accepts category group (type gate)', () => { // Compile-time guard: ensures `'group'` is a valid PoiCategory on - // ClusterMarkerDescriptor so group descriptors can reach pickRing. + // StructureMarkerDescriptor so group descriptors can reach pickRing. // If the category union loses 'group' this assignment is a type error. - const d: ClusterMarkerDescriptor = { + const d: StructureMarkerDescriptor = { id: 'test-group-pick-1', category: 'group', worldPos: [1, 2, 3], diff --git a/tests/services/gpu/renderers/clusterMarkerRenderer.test.ts b/tests/services/gpu/renderers/structureMarkerRenderer.test.ts similarity index 85% rename from tests/services/gpu/renderers/clusterMarkerRenderer.test.ts rename to tests/services/gpu/renderers/structureMarkerRenderer.test.ts index a8bce6aca..412bd25ab 100644 --- a/tests/services/gpu/renderers/clusterMarkerRenderer.test.ts +++ b/tests/services/gpu/renderers/structureMarkerRenderer.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; -import { createClusterMarkerRenderer } from '../../../../src/services/gpu/renderers/clusterMarkerRenderer'; -import type { ClusterMarkerDescriptor } from '../../../../src/@types/rendering/ClusterMarkerDescriptor'; +import { createStructureMarkerRenderer } from '../../../../src/services/gpu/renderers/structureMarkerRenderer'; +import type { StructureMarkerDescriptor } from '../../../../src/@types/rendering/StructureMarkerDescriptor'; import type { FadeUniformsBgl } from '../../../../src/@types/rendering/FadeUniformsBgl'; // Null-device pattern, mirrors markerLineRenderer.test.ts. @@ -11,7 +11,7 @@ const newRenderer = (initialCapacity?: number) => { format: 'rgba16float' as GPUTextureFormat, canvas: null as unknown as HTMLCanvasElement, }; - return createClusterMarkerRenderer( + return createStructureMarkerRenderer( ctx, 'rgba16float', null as unknown as FadeUniformsBgl, @@ -19,7 +19,7 @@ const newRenderer = (initialCapacity?: number) => { ); }; -const cluster = (id: number): ClusterMarkerDescriptor => ({ +const cluster = (id: number): StructureMarkerDescriptor => ({ // `id` is CPU-side metadata used by the selection / pick paths; // the renderer ignores it when packing the instance buffer, but // the type requires it. Synthesize a stable per-fixture id. @@ -32,7 +32,7 @@ const cluster = (id: number): ClusterMarkerDescriptor => ({ }); // `void` is a JS reserved word; use void_ to avoid a syntax error. -const void_ = (id: number): ClusterMarkerDescriptor => ({ +const void_ = (id: number): StructureMarkerDescriptor => ({ id: `test-void-${id}`, category: 'void', worldPos: [id, 0, 0], @@ -41,7 +41,7 @@ const void_ = (id: number): ClusterMarkerDescriptor => ({ ringColor: [0, 0.9, 0.9, 1], }); -const group = (id: number): ClusterMarkerDescriptor => ({ +const group = (id: number): StructureMarkerDescriptor => ({ id: `test-group-${id}`, category: 'group', worldPos: [id, 0, 0], @@ -50,7 +50,7 @@ const group = (id: number): ClusterMarkerDescriptor => ({ ringColor: [0.5, 0.9, 0.6, 1], }); -describe('ClusterMarkerRenderer (CPU state)', () => { +describe('StructureMarkerRenderer (CPU state)', () => { it('starts with zero markers', () => { const r = newRenderer(); expect(r.markerCount()).toBe(0); @@ -81,7 +81,7 @@ describe('ClusterMarkerRenderer (CPU state)', () => { it('label is stable', () => { const r = newRenderer(); - expect(r.label).toBe('clusterMarkerRenderer'); + expect(r.label).toBe('structureMarkerRenderer'); }); it('counts group descriptors alongside cluster / void', () => { diff --git a/tests/services/gpu/shaders/clusterMarker/ringPick.test.ts b/tests/services/gpu/shaders/structureMarker/ringPick.test.ts similarity index 96% rename from tests/services/gpu/shaders/clusterMarker/ringPick.test.ts rename to tests/services/gpu/shaders/structureMarker/ringPick.test.ts index bc97e7dad..ffdf8215b 100644 --- a/tests/services/gpu/shaders/clusterMarker/ringPick.test.ts +++ b/tests/services/gpu/shaders/structureMarker/ringPick.test.ts @@ -1,5 +1,5 @@ /** - * Parity test for clusterMarker/ringPick.wesl. + * Parity test for structureMarker/ringPick.wesl. * * ### Why we test the shader as text * @@ -20,7 +20,7 @@ import { describe, it, expect } from 'vitest'; // eslint-disable-next-line import/no-unresolved -- ?raw is a Vite query suffix -import ringPickCode from '../../../../../src/services/gpu/shaders/clusterMarker/ringPick.wesl?raw'; +import ringPickCode from '../../../../../src/services/gpu/shaders/structureMarker/ringPick.wesl?raw'; import { PICK_SENTINEL_OFFSET } from '../../../../../src/data/selectionEncoding'; describe('ringPick.wesl', () => { diff --git a/tests/services/loading/fetchers/clusterCatalogFetcher.test.ts b/tests/services/loading/fetchers/structureCatalogFetcher.test.ts similarity index 73% rename from tests/services/loading/fetchers/clusterCatalogFetcher.test.ts rename to tests/services/loading/fetchers/structureCatalogFetcher.test.ts index 6847f43b0..45caa8eca 100644 --- a/tests/services/loading/fetchers/clusterCatalogFetcher.test.ts +++ b/tests/services/loading/fetchers/structureCatalogFetcher.test.ts @@ -1,15 +1,15 @@ import { describe, expect, it } from 'vitest'; import { - clusterCatalogFetcher, - parseClusterMeta, -} from '../../../../src/services/loading/fetchers/clusterCatalogFetcher'; -import { encodeClusterCatalog } from '../../../../src/data/clusterCatalogFormat'; -import type { ClusterCatalog } from '../../../../src/@types/data/ClusterCatalog'; -import type { ClusterMetaEntry } from '../../../../src/@types/loading/ClusterCatalogPayload'; + structureCatalogFetcher, + parseStructureMeta, +} from '../../../../src/services/loading/fetchers/structureCatalogFetcher'; +import { encodeStructureCatalog } from '../../../../src/data/structureCatalogFormat'; +import type { StructureCatalog } from '../../../../src/@types/data/StructureCatalog'; +import type { StructureMetaEntry } from '../../../../src/@types/loading/StructureCatalogPayload'; import { useFetchMock } from '../../../setup/fetchMock'; /** Build a tiny well-formed catalog of `count` records for fixtures. */ -const makeCatalog = (count: number): ClusterCatalog => ({ +const makeCatalog = (count: number): StructureCatalog => ({ count, positions: new Float32Array(count * 3).map((_, i) => i), physicalRadiusMpc: new Float32Array(count).fill(1), @@ -18,7 +18,7 @@ const makeCatalog = (count: number): ClusterCatalog => ({ category: new Uint8Array(count), }); -const makeMeta = (count: number): ClusterMetaEntry[] => +const makeMeta = (count: number): StructureMetaEntry[] => Array.from({ length: count }, (_, i) => ({ id: `c${i}`, names: [`Cluster ${i}`], @@ -44,9 +44,9 @@ const routeByUrl = ( }); }; -describe('parseClusterMeta', () => { +describe('parseStructureMeta', () => { it('parses a valid array', () => { - const parsed = parseClusterMeta( + const parsed = parseStructureMeta( '[{"id":"a","names":["A"],"abell":null,"description":""}]', ); expect(parsed).toHaveLength(1); @@ -54,18 +54,18 @@ describe('parseClusterMeta', () => { }); it('rejects a non-array root', () => { - expect(() => parseClusterMeta('{}')).toThrow(); + expect(() => parseStructureMeta('{}')).toThrow(); }); }); -describe('clusterCatalogFetcher', () => { +describe('structureCatalogFetcher', () => { const fetch = useFetchMock(); it('decodes the ccat and pairs it with meta', async () => { - const ccat = encodeClusterCatalog(makeCatalog(2)); + const ccat = encodeStructureCatalog(makeCatalog(2)); routeByUrl(fetch.mock, ccat, JSON.stringify(makeMeta(2))); - const payload = await clusterCatalogFetcher( + const payload = await structureCatalogFetcher( {}, new AbortController().signal, () => {}, @@ -87,12 +87,12 @@ describe('clusterCatalogFetcher', () => { }); await expect( - clusterCatalogFetcher({}, new AbortController().signal, () => {}), + structureCatalogFetcher({}, new AbortController().signal, () => {}), ).rejects.toThrow(); }); it('throws HttpError on a 404 from the meta fetch', async () => { - const ccat = encodeClusterCatalog(makeCatalog(2)); + const ccat = encodeStructureCatalog(makeCatalog(2)); fetch.mock.mockImplementation((url: string) => { if (String(url).endsWith('.ccat')) { return Promise.resolve(new Response(ccat, { status: 200 })); @@ -101,21 +101,21 @@ describe('clusterCatalogFetcher', () => { }); await expect( - clusterCatalogFetcher({}, new AbortController().signal, () => {}), + structureCatalogFetcher({}, new AbortController().signal, () => {}), ).rejects.toThrow(); }); it('throws on a count/meta-length mismatch', async () => { - const ccat = encodeClusterCatalog(makeCatalog(2)); + const ccat = encodeStructureCatalog(makeCatalog(2)); routeByUrl(fetch.mock, ccat, JSON.stringify(makeMeta(1))); await expect( - clusterCatalogFetcher({}, new AbortController().signal, () => {}), + structureCatalogFetcher({}, new AbortController().signal, () => {}), ).rejects.toThrow(/mismatch/i); }); it('passes the abort signal to both fetches', async () => { - const ccat = encodeClusterCatalog(makeCatalog(1)); + const ccat = encodeStructureCatalog(makeCatalog(1)); fetch.mock.mockImplementation((_url: string, init?: RequestInit) => { if (init?.signal?.aborted) { return Promise.reject(new DOMException('aborted', 'AbortError')); @@ -129,7 +129,7 @@ describe('clusterCatalogFetcher', () => { const controller = new AbortController(); controller.abort(); await expect( - clusterCatalogFetcher({}, controller.signal, () => {}), + structureCatalogFetcher({}, controller.signal, () => {}), ).rejects.toThrow(); }); }); diff --git a/tests/services/loading/slots/clusterCatalogSlot.test.ts b/tests/services/loading/slots/structureCatalogSlot.test.ts similarity index 78% rename from tests/services/loading/slots/clusterCatalogSlot.test.ts rename to tests/services/loading/slots/structureCatalogSlot.test.ts index 0bd47b36a..f11571214 100644 --- a/tests/services/loading/slots/clusterCatalogSlot.test.ts +++ b/tests/services/loading/slots/structureCatalogSlot.test.ts @@ -1,5 +1,5 @@ /** - * clusterCatalogSlot — verifies the slot wakes the renderer on a successful + * structureCatalogSlot — verifies the slot wakes the renderer on a successful * load and degrades gracefully on fetch failure. * * The slot has no GPU commit and owns no state — `wireStructureProjection` @@ -9,7 +9,7 @@ * deterministic ready/error transition without touching the network. */ import { describe, expect, it, vi } from 'vitest'; -import type { ClusterCatalogPayload } from '../../../../src/@types/loading/ClusterCatalogPayload'; +import type { StructureCatalogPayload } from '../../../../src/@types/loading/StructureCatalogPayload'; import type { EngineState } from '../../../../src/@types/engine/state/EngineState'; import type { EngineCallbacks } from '../../../../src/@types/engine/EngineCallbacks'; import { HttpError } from '../../../../src/services/loading/fetchWithProgress'; @@ -17,13 +17,13 @@ import { HttpError } from '../../../../src/services/loading/fetchWithProgress'; // Hoisted mock target — `vi.mock` runs before imports, so the fetcher // reference has to live in a hoisted block the factory closure can see. const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn() })); -vi.mock('../../../../src/services/loading/fetchers/clusterCatalogFetcher', () => ({ - clusterCatalogFetcher: mockFetch, +vi.mock('../../../../src/services/loading/fetchers/structureCatalogFetcher', () => ({ + structureCatalogFetcher: mockFetch, })); -import { createClusterCatalogSlot } from '../../../../src/services/loading/slots/clusterCatalogSlot'; +import { createStructureCatalogSlot } from '../../../../src/services/loading/slots/structureCatalogSlot'; -function fakePayload(): ClusterCatalogPayload { +function fakePayload(): StructureCatalogPayload { return { catalog: { count: 1, @@ -51,13 +51,13 @@ function fakeState(): { state: EngineState; requestRender: ReturnType { +describe('createStructureCatalogSlot', () => { it('wakes the renderer on ready', async () => { const payload = fakePayload(); mockFetch.mockResolvedValue(payload); const { state, requestRender } = fakeState(); - const slot = createClusterCatalogSlot(state, noopCb); + const slot = createStructureCatalogSlot(state, noopCb); slot.load({}); await vi.waitFor(() => expect(slot.state().kind).toBe('ready')); @@ -66,18 +66,18 @@ describe('createClusterCatalogSlot', () => { expect(requestRender).toHaveBeenCalled(); // Construction purity: the factory RETURNS the slot and does NOT // self-install it — `installSlots` (the orchestrator) owns the write. - expect(slot.name).toBe('cluster-catalog'); - expect(state.assetSlots.clusterCatalog).toBeUndefined(); + expect(slot.name).toBe('structure-catalog'); + expect(state.assetSlots.structureCatalog).toBeUndefined(); }); it('warns on error', async () => { // A 404 is a permanent failure under defaultRetryPolicy → give-up // immediately (no slow backoff), so the slot reaches 'error' at once. - mockFetch.mockRejectedValue(new HttpError(404, 'clusters.ccat')); + mockFetch.mockRejectedValue(new HttpError(404, 'structures.ccat')); const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const { state } = fakeState(); - const slot = createClusterCatalogSlot(state, noopCb); + const slot = createStructureCatalogSlot(state, noopCb); slot.load({}); await vi.waitFor(() => expect(slot.state().kind).toBe('error')); diff --git a/tests/tools/deploy/syncR2.test.ts b/tests/tools/deploy/syncR2.test.ts index ef1dbe804..abfce4013 100644 --- a/tests/tools/deploy/syncR2.test.ts +++ b/tests/tools/deploy/syncR2.test.ts @@ -16,9 +16,9 @@ import { describe, expect, it } from 'vitest'; import { ALLOW, etagMatches } from '../../../tools/deploy/syncR2'; describe('syncR2 ALLOW', () => { - it('accepts clusters.ccat and clusters_meta.json', () => { - expect(ALLOW('clusters.ccat')).toBe(true); - expect(ALLOW('clusters_meta.json')).toBe(true); + it('accepts structures.ccat and structures_meta.json', () => { + expect(ALLOW('structures.ccat')).toBe(true); + expect(ALLOW('structures_meta.json')).toBe(true); }); it('still rejects glade.bin / sdss.bin', () => { diff --git a/tests/tools/parsers/parseClusterSeed.test.ts b/tests/tools/parsers/parseStructureSeed.test.ts similarity index 55% rename from tests/tools/parsers/parseClusterSeed.test.ts rename to tests/tools/parsers/parseStructureSeed.test.ts index babf15d13..f809eebda 100644 --- a/tests/tools/parsers/parseClusterSeed.test.ts +++ b/tests/tools/parsers/parseStructureSeed.test.ts @@ -1,13 +1,13 @@ import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { - parseClusterSeed, - validateClusterSeedEntry, - type ClusterSeedEntry, -} from '../../../tools/parsers/parseClusterSeed'; + parseStructureSeed, + validateStructureSeedEntry, + type StructureSeedEntry, +} from '../../../tools/parsers/parseStructureSeed'; import { rawDataPath } from '../../../tools/utils/io/rawDataRegistry'; -function baseEntry(overrides: Partial = {}): ClusterSeedEntry { +function baseEntry(overrides: Partial = {}): StructureSeedEntry { return { id: 'coma', names: ['Coma Cluster', 'A1656'], @@ -22,10 +22,10 @@ function baseEntry(overrides: Partial = {}): ClusterSeedEntry }; } -describe('parseClusterSeed', () => { +describe('parseStructureSeed', () => { it('accepts the bundled seed file', () => { - const raw = readFileSync(rawDataPath('clusters.seed'), 'utf8'); - const entries = parseClusterSeed(raw); + const raw = readFileSync(rawDataPath('structures.seed'), 'utf8'); + const entries = parseStructureSeed(raw); expect(entries.length).toBeGreaterThanOrEqual(25); const validCategories = new Set(['cluster', 'supercluster', 'void', 'group']); for (const e of entries) { @@ -35,97 +35,97 @@ describe('parseClusterSeed', () => { it('rejects out-of-range raHours', () => { const bad = [baseEntry({ id: 'bad', raHours: 24 })]; - expect(() => parseClusterSeed(JSON.stringify(bad))).toThrow(/bad.*raHours|raHours.*bad/i); + expect(() => parseStructureSeed(JSON.stringify(bad))).toThrow(/bad.*raHours|raHours.*bad/i); }); it('rejects duplicate ids', () => { const dups = [baseEntry({ id: 'coma' }), baseEntry({ id: 'coma' })]; - expect(() => parseClusterSeed(JSON.stringify(dups))).toThrow(/duplicate id/i); + expect(() => parseStructureSeed(JSON.stringify(dups))).toThrow(/duplicate id/i); }); it('rejects non-positive distMpc', () => { const bad = [baseEntry({ id: 'bad', distMpc: 0 })]; - expect(() => parseClusterSeed(JSON.stringify(bad))).toThrow(/bad.*distMpc|distMpc.*bad/i); + expect(() => parseStructureSeed(JSON.stringify(bad))).toThrow(/bad.*distMpc|distMpc.*bad/i); }); - it('validateClusterSeedEntry rejects unknown category', () => { - const e = baseEntry({ category: 'supergroup' as ClusterSeedEntry['category'] }); - expect(() => validateClusterSeedEntry(e)).toThrow(/category/); + it('validateStructureSeedEntry rejects unknown category', () => { + const e = baseEntry({ category: 'supergroup' as StructureSeedEntry['category'] }); + expect(() => validateStructureSeedEntry(e)).toThrow(/category/); }); it('accepts category group and round-trips it', () => { const e = baseEntry({ id: 'local-group', category: 'group' }); - const result = validateClusterSeedEntry(e); + const result = validateStructureSeedEntry(e); expect(result.category).toBe('group'); expect(result.id).toBe('local-group'); - // Also verify parseClusterSeed round-trips a group entry end-to-end. - const entries = parseClusterSeed(JSON.stringify([e])); + // Also verify parseStructureSeed round-trips a group entry end-to-end. + const entries = parseStructureSeed(JSON.stringify([e])); expect(entries).toHaveLength(1); expect(entries[0]?.category).toBe('group'); }); it('rejects raHours below 0', () => { const e = baseEntry({ id: 'neg-ra', raHours: -1 }); - expect(() => validateClusterSeedEntry(e)).toThrow(/neg-ra.*raHours|raHours.*neg-ra/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/neg-ra.*raHours|raHours.*neg-ra/i); }); it('rejects decDeg out of [-90, 90]', () => { const e = baseEntry({ id: 'bad-dec', decDeg: 91 }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-dec.*decDeg|decDeg.*bad-dec/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-dec.*decDeg|decDeg.*bad-dec/i); }); it('rejects non-positive physicalRadiusMpc', () => { const e = baseEntry({ id: 'bad-phys', physicalRadiusMpc: -1 }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-phys.*physicalRadiusMpc|physicalRadiusMpc.*bad-phys/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-phys.*physicalRadiusMpc|physicalRadiusMpc.*bad-phys/i); }); it('rejects non-positive apparentRadiusMpc', () => { const e = baseEntry({ id: 'bad-app', apparentRadiusMpc: 0 }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-app.*apparentRadiusMpc|apparentRadiusMpc.*bad-app/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-app.*apparentRadiusMpc|apparentRadiusMpc.*bad-app/i); }); it('rejects empty id', () => { const e = baseEntry({ id: '' }); - expect(() => validateClusterSeedEntry(e)).toThrow(/id/); + expect(() => validateStructureSeedEntry(e)).toThrow(/id/); }); it('rejects empty names array', () => { const e = baseEntry({ names: [] }); - expect(() => validateClusterSeedEntry(e)).toThrow(/coma.*names|names.*coma/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/coma.*names|names.*coma/i); }); it('accepts optional abell field', () => { const e = baseEntry({ abell: 'A1656' }); - expect(validateClusterSeedEntry(e).abell).toBe('A1656'); + expect(validateStructureSeedEntry(e).abell).toBe('A1656'); }); it('accepts optional commonName field', () => { const e = baseEntry({ commonName: 'The Coma Cluster' }); - expect(validateClusterSeedEntry(e).commonName).toBe('The Coma Cluster'); + expect(validateStructureSeedEntry(e).commonName).toBe('The Coma Cluster'); }); it('rejects root that is not an array', () => { - expect(() => parseClusterSeed('{}')).toThrow(/array/i); + expect(() => parseStructureSeed('{}')).toThrow(/array/i); }); it('rejects an empty description', () => { const e = baseEntry({ id: 'bad-desc', description: ' ' }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-desc.*description|description.*bad-desc/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-desc.*description|description.*bad-desc/i); }); it('rejects a non-string abell', () => { const e = baseEntry({ id: 'bad-abell', abell: 99 as unknown as string }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-abell.*abell|abell.*bad-abell/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-abell.*abell|abell.*bad-abell/i); }); it('rejects a non-string commonName', () => { const e = baseEntry({ id: 'bad-cn', commonName: null as unknown as string }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-cn.*commonName|commonName.*bad-cn/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-cn.*commonName|commonName.*bad-cn/i); }); it('rejects an empty commonName', () => { const e = baseEntry({ id: 'bad-cn2', commonName: '' }); - expect(() => validateClusterSeedEntry(e)).toThrow(/bad-cn2.*commonName|commonName.*bad-cn2/i); + expect(() => validateStructureSeedEntry(e)).toThrow(/bad-cn2.*commonName|commonName.*bad-cn2/i); }); }); diff --git a/tests/tools/clusters/buildClusters.test.ts b/tests/tools/structures/buildStructures.test.ts similarity index 95% rename from tests/tools/clusters/buildClusters.test.ts rename to tests/tools/structures/buildStructures.test.ts index 00881cb88..e8b51f216 100644 --- a/tests/tools/clusters/buildClusters.test.ts +++ b/tests/tools/structures/buildStructures.test.ts @@ -1,11 +1,11 @@ /** - * Tests for `buildClusters.ts` exported pure functions. + * Tests for `buildStructures.ts` exported pure functions. * * All disk-touching logic lives in `main`; the two exported functions * (`extractAbell`, `buildClusterEntries`) operate on in-memory data so * unit tests here never touch the filesystem or the real MCXC/MSCC tables. * - * Fixture strategy: hand-crafted McxcRow/MsccRow/ClusterSeedEntry objects + * Fixture strategy: hand-crafted McxcRow/MsccRow/StructureSeedEntry objects * supply the minimum fields each test cares about. Only the fields under * test need valid values — others are set to innocuous defaults. This keeps * each test small and the failure messages unambiguous. @@ -16,11 +16,11 @@ import { resolve } from 'node:path'; import { extractAbell, buildClusterEntries, -} from '../../../tools/clusters/buildClusters'; -import { parseClusterSeed } from '../../../tools/parsers/parseClusterSeed'; +} from '../../../tools/structures/buildStructures'; +import { parseStructureSeed } from '../../../tools/parsers/parseStructureSeed'; import type { McxcRow } from '../../../tools/parsers/parseMcxc'; import type { MsccRow } from '../../../tools/parsers/parseMscc'; -import type { ClusterSeedEntry } from '../../../tools/parsers/parseClusterSeed'; +import type { StructureSeedEntry } from '../../../tools/parsers/parseStructureSeed'; import { H0_KM_S_MPC } from '../../../src/utils/math/constants'; // ── Shared fixtures ────────────────────────────────────────────────────────── @@ -67,7 +67,7 @@ function makeMsccRow(overrides: Partial = {}): MsccRow { } /** An empty featured seed (no curated anchors → no dedup suppression). */ -const NO_SEED: readonly ClusterSeedEntry[] = []; +const NO_SEED: readonly StructureSeedEntry[] = []; // ── extractAbell ───────────────────────────────────────────────────────────── @@ -206,8 +206,8 @@ describe('buildClusterEntries drops a bulk entry near a featured seed anchor', ( // Read Coma's real seed coordinates. Using the real file means the test // stays honest — if the seed changes the test fails rather than silently // diverging from reality. - const seedJson = readFileSync(resolve('data/cluster_anchors.seed.json'), 'utf8'); - const seed = parseClusterSeed(seedJson); + const seedJson = readFileSync(resolve('data/structure_anchors.seed.json'), 'utf8'); + const seed = parseStructureSeed(seedJson); const coma = seed.find((e) => e.id === 'coma-a1656')!; expect(coma).toBeDefined(); diff --git a/tests/utils/cluster/structureMemberCount.test.ts b/tests/utils/structure/structureMemberCount.test.ts similarity index 96% rename from tests/utils/cluster/structureMemberCount.test.ts rename to tests/utils/structure/structureMemberCount.test.ts index e76303f42..22e46dc9b 100644 --- a/tests/utils/cluster/structureMemberCount.test.ts +++ b/tests/utils/structure/structureMemberCount.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { structureMemberCount } from '../../../src/utils/cluster/structureMemberCount'; +import { structureMemberCount } from '../../../src/utils/structure/structureMemberCount'; import { Source } from '../../../src/data/sources'; import { ALL_VISIBLE_MASK, maskWith, maskWithout } from '../../../src/utils/sourceMask'; import type { GalaxyCatalog } from '../../../src/@types/data/GalaxyCatalog'; @@ -9,7 +9,7 @@ import type { StructureRecord } from '../../../src/@types/engine/data/StructureR /** * Minimal GalaxyCatalog from (x,y,z) tuples — only `positions`/`count` are * read by the cone search; the rest are zero-filled to satisfy the shape. - * Mirrors the helper in clusterMembership.test.ts. + * Mirrors the helper in structureMembership.test.ts. */ function makeCatalog(positions: ReadonlyArray): GalaxyCatalog { const count = positions.length; diff --git a/tests/utils/cluster/clusterMembership.test.ts b/tests/utils/structure/structureMembership.test.ts similarity index 82% rename from tests/utils/cluster/clusterMembership.test.ts rename to tests/utils/structure/structureMembership.test.ts index 805ba0574..3274591e7 100644 --- a/tests/utils/cluster/clusterMembership.test.ts +++ b/tests/utils/structure/structureMembership.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { clusterMembership } from '../../../src/utils/cluster/clusterMembership'; +import { structureMembership } from '../../../src/utils/structure/structureMembership'; import { Source } from '../../../src/data/sources'; import { packSelection } from '../../../src/data/selectionEncoding'; import type { GalaxyCatalog } from '../../../src/@types/data/GalaxyCatalog'; /** * Build a minimal GalaxyCatalog from a list of (x,y,z) tuples. - * Only the `positions` + `count` fields are read by clusterMembership; + * Only the `positions` + `count` fields are read by structureMembership; * the other Float32Array slots are filled with zeros via `new * Float32Array(count)` so the type's required-field shape is satisfied * without polluting the test fixture with irrelevant data. @@ -32,14 +32,14 @@ function makeCatalog(positions: ReadonlyArray }; } -describe('clusterMembership — pure cone search', () => { +describe('structureMembership — pure cone search', () => { it('classifies one inside, one boundary, one outside galaxy', () => { const catalog = makeCatalog([ [5, 0, 0], [10, 0, 0], [20, 0, 0], ]); - const result = clusterMembership( + const result = structureMembership( [{ source: Source.SDSS, catalog }], [0, 0, 0], 10, @@ -50,7 +50,7 @@ describe('clusterMembership — pure cone search', () => { it('uses strict less-than (galaxy on boundary excluded)', () => { const catalog = makeCatalog([[0, 0, 10]]); - const result = clusterMembership( + const result = structureMembership( [{ source: Source.TwoMRS, catalog }], [0, 0, 0], 10, @@ -70,7 +70,7 @@ describe('clusterMembership — pure cone search', () => { [0, 2, 0], // inside [0, 100, 0], // outside ]); - const result = clusterMembership( + const result = structureMembership( [ { source: Source.SDSS, catalog: sdss }, { source: Source.TwoMRS, catalog: twomrs }, @@ -88,14 +88,14 @@ describe('clusterMembership — pure cone search', () => { }); it('returns {count: 0, packedIds: []} for empty catalogs', () => { - const result = clusterMembership([], [0, 0, 0], 10); + const result = structureMembership([], [0, 0, 0], 10); expect(result.count).toBe(0); expect(result.packedIds).toEqual([]); }); it('returns {count: 0, packedIds: []} when every input catalog is empty', () => { const empty = makeCatalog([]); - const result = clusterMembership( + const result = structureMembership( [ { source: Source.SDSS, catalog: empty }, { source: Source.TwoMRS, catalog: empty }, @@ -113,16 +113,16 @@ describe('clusterMembership — pure cone search', () => { [2, 0, 0], [3, 0, 0], ]); - const r1 = clusterMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); - const r2 = clusterMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); + const r1 = structureMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); + const r2 = structureMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); expect(r1.count).toBe(r2.count); expect(r1.packedIds).toEqual(r2.packedIds); }); it('does not internally cache (each call returns a fresh array)', () => { const catalog = makeCatalog([[1, 0, 0]]); - const r1 = clusterMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); - const r2 = clusterMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); + const r1 = structureMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); + const r2 = structureMembership([{ source: Source.SDSS, catalog }], [0, 0, 0], 5); expect(r1.packedIds).not.toBe(r2.packedIds); // distinct array references expect(r1.packedIds).toEqual(r2.packedIds); // but equal contents }); diff --git a/tests/visual/renderFrameSplitBaseline.test.ts b/tests/visual/renderFrameSplitBaseline.test.ts index 2673df87f..4c721328e 100644 --- a/tests/visual/renderFrameSplitBaseline.test.ts +++ b/tests/visual/renderFrameSplitBaseline.test.ts @@ -364,7 +364,7 @@ describe('renderFrame visual baseline', () => { // the recorded single-vs-split sequence is unchanged. flowFieldRenderer: null, volumeUpsample, - clusterMarkerRenderer: null, + structureMarkerRenderer: null, // Shared focus uniform — no-op write (doesn't touch the recorded // encoder); its bind group is bound identically in both the // single and split paths, so the sequence stays stable. diff --git a/tools/curation/writeMetaSidecar.ts b/tools/curation/writeMetaSidecar.ts index 2d737ae8e..67e5dc08d 100644 --- a/tools/curation/writeMetaSidecar.ts +++ b/tools/curation/writeMetaSidecar.ts @@ -2,7 +2,7 @@ * `writeMetaSidecar` — write a per-localIdx id→strings sidecar JSON file. * * Why a shared helper instead of inline `writeFileSync`? Two build scripts - * (`buildFamous`, `buildClusters`) emit the same artefact shape: an ordered + * (`buildFamous`, `buildStructures`) emit the same artefact shape: an ordered * JSON array where position == localIdx in the matching `.bin`. Centralising * the write step means one place controls the formatting (2-space pretty-print) * and both callers stay in sync without each hard-coding the `JSON.stringify` @@ -25,7 +25,7 @@ export type MetaSidecarEntry = { names: string[]; description: string; [key: string]: unknown; // domain-specific extras (famous type/commonName, - // cluster blurb) pass through untouched + // cluster blurb) pass through untouched }; /** diff --git a/tools/deploy/syncR2.ts b/tools/deploy/syncR2.ts index d54b9824a..04b28ae87 100644 --- a/tools/deploy/syncR2.ts +++ b/tools/deploy/syncR2.ts @@ -137,10 +137,10 @@ export const ALLOW = (name: string): boolean => // emitted by `npm run build-flow-field`. Tier-agnostic, like filaments.bin. name === 'flowfield.scfd' || // Cluster/supercluster coverage artefacts (MCXC + MSCC + featured seed) - // emitted by `npm run build-clusters`: the packed point catalog and its + // emitted by `npm run build-structures`: the packed point catalog and its // per-structure metadata sidecar. Tier-agnostic, like famous.bin. - name === 'clusters.ccat' || - name === 'clusters_meta.json'; + name === 'structures.ccat' || + name === 'structures_meta.json'; /** * Decide whether a local file is byte-identical to the object already in R2, diff --git a/tools/fetch/fetchClusterCatalogs.ts b/tools/fetch/fetchStructureCatalogs.ts similarity index 96% rename from tools/fetch/fetchClusterCatalogs.ts rename to tools/fetch/fetchStructureCatalogs.ts index 208fb9292..6a22fc62f 100644 --- a/tools/fetch/fetchClusterCatalogs.ts +++ b/tools/fetch/fetchStructureCatalogs.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node /** - * fetchClusterCatalogs — download MCXC and MSCC from CDS VizieR FTP to + * fetchStructureCatalogs — download MCXC and MSCC from CDS VizieR FTP to * data/raw/{mcxc,mscc}/. * * Both catalogs are plain uncompressed fixed-width ASCII; the fetch is a @@ -76,7 +76,7 @@ async function fetchCatalog(opts: { }): Promise { const { label, tableUrl, readmeUrl, tablePath, readmePath, sha256Path } = opts; - process.stderr.write(`\nfetchClusterCatalogs: ${label}\n`); + process.stderr.write(`\nfetchStructureCatalogs: ${label}\n`); // ReadMe first — tiny (~5 KB) and the parser needs it for column-offset verification. const readmeResult = await downloadWithResume(readmeUrl, readmePath); @@ -133,7 +133,7 @@ async function main(): Promise { sha256Path: MSCC_SHA256_PATH, }); - process.stderr.write('\nfetchClusterCatalogs: done\n'); + process.stderr.write('\nfetchStructureCatalogs: done\n'); } const invokedDirectly = process.argv[1] === fileURLToPath(import.meta.url); diff --git a/tools/parsers/parseMcxc.ts b/tools/parsers/parseMcxc.ts index 140e7ddfe..e963d5579 100644 --- a/tools/parsers/parseMcxc.ts +++ b/tools/parsers/parseMcxc.ts @@ -36,7 +36,7 @@ import { nonCommentLines, slot } from './common.js'; const MIN_LINE_LEN = 204; /** - * A single parsed MCXC cluster row, carrying the fields the cluster-coverage + * A single parsed MCXC cluster row, carrying the fields the structure-coverage * pipeline consumes. String fields are trimmed; blank columns become `''`. */ export type McxcRow = { diff --git a/tools/parsers/parseMscc.ts b/tools/parsers/parseMscc.ts index 618c113e2..87b7abe53 100644 --- a/tools/parsers/parseMscc.ts +++ b/tools/parsers/parseMscc.ts @@ -11,7 +11,7 @@ * this parser handles `mscc.dat` only. * * Why MSCC in this pipeline? Together with the MCXC X-ray cluster catalogue, - * it supplies the ~375-structure cluster-coverage feature with large-scale + * it supplies the ~375-structure structure-coverage feature with large-scale * supercluster envelopes. MCXC gives physically calibrated R500 radii for * individual X-ray clusters; MSCC gives the Friend-of-Friends extent (`dmax`) * for supercluster complexes, letting the renderer draw an outer coverage halo @@ -40,7 +40,7 @@ * no sexagesimal fallback to parse — the decimal form is the only option. * * The SCLs field (bytes 6–21, A16) is the Einasto et al. 2001 cross-reference; - * it is not consumed by the cluster-coverage pipeline and is intentionally + * it is not consumed by the structure-coverage pipeline and is intentionally * skipped here to keep the output type minimal. * * The trailing `memCl` field (bytes 53–324, A272) is a comma-separated list of @@ -64,7 +64,7 @@ * `dmax` is stored in raw h70^-1 Mpc as published. The conversion to physical * Mpc (divide by h70 = 0.7) and the halving to a radius (dmax is a diameter: * the maximum *pair* separation, not the centroid-to-edge distance) both happen - * in the `buildClusters` pipeline step (Task 10), not here. The parser is a + * in the `buildStructures` pipeline step (Task 10), not here. The parser is a * faithful column reader; it does not interpret units. */ @@ -75,7 +75,7 @@ const MIN_LINE_LEN = 51; /** * A single parsed MSCC supercluster row, carrying the fields consumed by the - * cluster-coverage pipeline. Numeric fields are trimmed and `parseFloat`'d; + * structure-coverage pipeline. Numeric fields are trimmed and `parseFloat`'d; * no unit conversions are applied. */ export type MsccRow = { @@ -93,7 +93,7 @@ export type MsccRow = { * Maximum separation of a member-cluster pair, in **raw h70^-1 Mpc** * (as published). Do NOT interpret as a radius — `dmax` is the diameter * of the tightest enclosing sphere. The h70→Mpc conversion and the - * halving to a centroid radius live in `buildClusters` (Task 10). + * halving to a centroid radius live in `buildStructures` (Task 10). */ dmaxMpc: number; }; diff --git a/tools/parsers/parseClusterSeed.ts b/tools/parsers/parseStructureSeed.ts similarity index 72% rename from tools/parsers/parseClusterSeed.ts rename to tools/parsers/parseStructureSeed.ts index 966c6df6d..04a98d80a 100644 --- a/tools/parsers/parseClusterSeed.ts +++ b/tools/parsers/parseStructureSeed.ts @@ -1,5 +1,5 @@ /** - * parseClusterSeed — parse + validate `data/cluster_anchors.seed.json`. + * parseStructureSeed — parse + validate `data/structure_anchors.seed.json`. * * The seed file is the single source of truth for which galaxy clusters, * superclusters, voids, and nearby galaxy groups appear as featured labelled @@ -22,7 +22,7 @@ const VALID_CATEGORIES = ['cluster', 'supercluster', 'void', 'group'] as const; /** - * One featured structure from `cluster_anchors.seed.json`. + * One featured structure from `structure_anchors.seed.json`. * * Coordinates follow the `SkyCoord` convention: RA in hours [0, 24), * Dec in degrees [-90, 90], distances in Mpc. @@ -35,7 +35,7 @@ const VALID_CATEGORIES = ['cluster', 'supercluster', 'void', 'group'] as const; * superclusters and voids it matches physR. Drives ring sizing and * cone-search membership. */ -export type ClusterSeedEntry = { +export type StructureSeedEntry = { /** URL-safe lower-kebab id, unique within the file (no category prefix). */ id: string; /** Ordered names; primary first. The first name drives the POI label. */ @@ -70,45 +70,45 @@ export type ClusterSeedEntry = { * Validate a single entry. Throws with a message naming the offending id * on any malformed field. Returns the entry unchanged so callers can chain. */ -export function validateClusterSeedEntry(e: ClusterSeedEntry): ClusterSeedEntry { +export function validateStructureSeedEntry(e: StructureSeedEntry): StructureSeedEntry { if (typeof e.id !== 'string' || e.id.length === 0) { - throw new Error(`cluster seed: missing id on entry ${JSON.stringify(e).slice(0, 60)}`); + throw new Error(`structure seed: missing id on entry ${JSON.stringify(e).slice(0, 60)}`); } if (!Array.isArray(e.names) || e.names.length === 0) { - throw new Error(`cluster seed: ${e.id} has empty names array`); + throw new Error(`structure seed: ${e.id} has empty names array`); } if (!VALID_CATEGORIES.includes(e.category as (typeof VALID_CATEGORIES)[number])) { throw new Error( - `cluster seed: ${e.id} has unknown category ${JSON.stringify(e.category)} (expected 'cluster' | 'supercluster' | 'void' | 'group')`, + `structure seed: ${e.id} has unknown category ${JSON.stringify(e.category)} (expected 'cluster' | 'supercluster' | 'void' | 'group')`, ); } if (!Number.isFinite(e.raHours) || e.raHours < 0 || e.raHours >= 24) { - throw new Error(`cluster seed: ${e.id} has out-of-range raHours ${e.raHours} (expected [0, 24))`); + throw new Error(`structure seed: ${e.id} has out-of-range raHours ${e.raHours} (expected [0, 24))`); } if (!Number.isFinite(e.decDeg) || e.decDeg < -90 || e.decDeg > 90) { - throw new Error(`cluster seed: ${e.id} has out-of-range decDeg ${e.decDeg} (expected [-90, 90])`); + throw new Error(`structure seed: ${e.id} has out-of-range decDeg ${e.decDeg} (expected [-90, 90])`); } if (!Number.isFinite(e.distMpc) || e.distMpc <= 0) { - throw new Error(`cluster seed: ${e.id} has non-positive distMpc ${e.distMpc}`); + throw new Error(`structure seed: ${e.id} has non-positive distMpc ${e.distMpc}`); } if (!Number.isFinite(e.physicalRadiusMpc) || e.physicalRadiusMpc <= 0) { throw new Error( - `cluster seed: ${e.id} has non-positive physicalRadiusMpc ${e.physicalRadiusMpc}`, + `structure seed: ${e.id} has non-positive physicalRadiusMpc ${e.physicalRadiusMpc}`, ); } if (!Number.isFinite(e.apparentRadiusMpc) || e.apparentRadiusMpc <= 0) { throw new Error( - `cluster seed: ${e.id} has non-positive apparentRadiusMpc ${e.apparentRadiusMpc}`, + `structure seed: ${e.id} has non-positive apparentRadiusMpc ${e.apparentRadiusMpc}`, ); } if (typeof e.description !== 'string' || e.description.trim().length === 0) { - throw new Error(`cluster seed: ${e.id} missing description`); + throw new Error(`structure seed: ${e.id} missing description`); } if (e.commonName !== undefined && (typeof e.commonName !== 'string' || e.commonName.length === 0)) { - throw new Error(`cluster seed: ${e.id} has invalid commonName (must be a non-empty string)`); + throw new Error(`structure seed: ${e.id} has invalid commonName (must be a non-empty string)`); } if (e.abell !== undefined && (typeof e.abell !== 'string' || e.abell.length === 0)) { - throw new Error(`cluster seed: ${e.id} has invalid abell (must be a non-empty string)`); + throw new Error(`structure seed: ${e.id} has invalid abell (must be a non-empty string)`); } return e; } @@ -117,17 +117,17 @@ export function validateClusterSeedEntry(e: ClusterSeedEntry): ClusterSeedEntry * Parse and validate the entire seed JSON. Throws on any per-entry problem * and on duplicate ids across the file. */ -export function parseClusterSeed(rawJson: string): ClusterSeedEntry[] { +export function parseStructureSeed(rawJson: string): StructureSeedEntry[] { const parsed = JSON.parse(rawJson); if (!Array.isArray(parsed)) { - throw new Error('cluster seed: root must be an array'); + throw new Error('structure seed: root must be an array'); } const seen = new Set(); - const out: ClusterSeedEntry[] = []; + const out: StructureSeedEntry[] = []; for (const e of parsed) { - const validated = validateClusterSeedEntry(e as ClusterSeedEntry); + const validated = validateStructureSeedEntry(e as StructureSeedEntry); if (seen.has(validated.id)) { - throw new Error(`cluster seed: duplicate id "${validated.id}"`); + throw new Error(`structure seed: duplicate id "${validated.id}"`); } seen.add(validated.id); out.push(validated); diff --git a/tools/clusters/buildClusters.ts b/tools/structures/buildStructures.ts similarity index 90% rename from tools/clusters/buildClusters.ts rename to tools/structures/buildStructures.ts index 138b6ed80..8eff6aa54 100644 --- a/tools/clusters/buildClusters.ts +++ b/tools/structures/buildStructures.ts @@ -1,23 +1,23 @@ #!/usr/bin/env node /** - * buildClusters — assemble the cluster/supercluster coverage layer. + * buildStructures — assemble the featured-structure coverage layer. * * Reads: * - `data/raw/mcxc/mcxc.dat` (MCXC X-ray cluster catalog) * - `data/raw/mscc/mscc.dat` (MSCC supercluster catalog) - * - `data/cluster_anchors.seed.json` (featured curated anchors) + * - `data/structure_anchors.seed.json` (featured curated anchors) * * Writes: - * - `public/data/clusters.ccat` (ClusterCatalog binary, renderer input) - * - `public/data/clusters_meta.json` (per-localIdx id/names/abell/description) + * - `public/data/structures.ccat` (StructureCatalog binary, renderer input) + * - `public/data/structures_meta.json` (per-localIdx id/names/abell/description) * * The two artefacts are index-parallel: record i in the .ccat corresponds * to entry i in the meta JSON, allowing the runtime to look up human-readable * metadata by the localIdx the pick-renderer returns. * - * Run order: after `npm run build-tiers` (the cluster build is independent of + * Run order: after `npm run build-tiers` (the structure build is independent of * the galaxy .bin files but shares the same `public/data/` output directory). - * The npm script is `build-clusters`. + * The npm script is `build-structures`. * * ## Filtering strategy * @@ -39,16 +39,16 @@ import { fileURLToPath } from 'node:url'; import { parseMcxc, type McxcRow } from '../parsers/parseMcxc.js'; import { parseMscc, type MsccRow } from '../parsers/parseMscc.js'; -import { parseClusterSeed, type ClusterSeedEntry } from '../parsers/parseClusterSeed.js'; -import { encodeClusterCatalog } from '../../src/data/clusterCatalogFormat.js'; +import { parseStructureSeed, type StructureSeedEntry } from '../parsers/parseStructureSeed.js'; +import { encodeStructureCatalog } from '../../src/data/structureCatalogFormat.js'; import { rawDataPath } from '../utils/io/rawDataRegistry.js'; import { writeMetaSidecar } from '../curation/writeMetaSidecar.js'; import { dedupeByProximity } from '../curation/dedupeByProximity.js'; import { raDecDistToEqCart } from '../../src/utils/math/raDecDistToEqCart.js'; import { redshiftToDistanceMpc } from '../../src/utils/math/redshiftToDistanceMpc.js'; import { H0_KM_S_MPC } from '../../src/utils/math/constants.js'; -import type { ClusterCatalog } from '../../src/@types/data/ClusterCatalog.js'; -import type { ClusterCategoryByte } from '../../src/@types/data/ClusterCatalog.js'; +import type { StructureCatalog } from '../../src/@types/data/StructureCatalog.js'; +import type { StructureCategoryByte } from '../../src/@types/data/StructureCatalog.js'; import type { Vec3 } from '../../src/@types/math/Vec3.js'; // ── Tunable threshold constants ─────────────────────────────────────────────── @@ -132,7 +132,7 @@ const H70 = H0_KM_S_MPC / 70; * metadata alongside the numeric fields so callers can build the meta JSON * from the same objects, guaranteeing index alignment. */ -export type ClusterBuildEntry = { +export type StructureBuildEntry = { /** URL-safe slug of names[0] — becomes the localIdx lookup key at runtime. */ id: string; /** Equatorial-Cartesian world position in Mpc. */ @@ -144,7 +144,7 @@ export type ClusterBuildEntry = { /** Raw mass proxy: M500 (10^14 M☉) for clusters, Nm for superclusters. */ significance: number; /** 0 = cluster (MCXC), 1 = supercluster (MSCC). */ - category: ClusterCategoryByte; + category: StructureCategoryByte; /** Display names; names[0] is the primary label shown in the UI. */ names: string[]; /** Normalized Abell/ACO designation, e.g. 'A2670' or 'S0805', or null. */ @@ -242,7 +242,7 @@ export function extractAbell(oName: string, aName: string): string | null { } /** - * Build the intermediate `ClusterBuildEntry[]` from raw MCXC rows, raw MSCC + * Build the intermediate `StructureBuildEntry[]` from raw MCXC rows, raw MSCC * rows, and a curated featured-anchor seed. * * Steps: @@ -255,10 +255,10 @@ export function extractAbell(oName: string, aName: string): string | null { export function buildClusterEntries( mcxc: readonly McxcRow[], mscc: readonly MsccRow[], - featuredSeed: readonly ClusterSeedEntry[], -): ClusterBuildEntry[] { + featuredSeed: readonly StructureSeedEntry[], +): StructureBuildEntry[] { // ── Step 1: MCXC → cluster entries ──────────────────────────────────────── - const clusterEntries: ClusterBuildEntry[] = []; + const clusterEntries: StructureBuildEntry[] = []; for (const row of mcxc) { if (row.z > Z_MAX || row.m500 < MCXC_M500_MIN) continue; @@ -295,7 +295,7 @@ export function buildClusterEntries( } // ── Step 2: MSCC → supercluster entries ─────────────────────────────────── - const scEntries: ClusterBuildEntry[] = []; + const scEntries: StructureBuildEntry[] = []; for (const row of mscc) { if (row.z > Z_MAX || row.nm < MSCC_NM_MIN) continue; @@ -334,14 +334,14 @@ export function buildClusterEntries( // ── Meta sidecar type ───────────────────────────────────────────────────────── -type ClusterMetaEntry = { +type StructureMetaEntry = { id: string; names: string[]; abell: string | null; description: string; }; -function toMeta(e: ClusterBuildEntry): ClusterMetaEntry { +function toMeta(e: StructureBuildEntry): StructureMetaEntry { return { id: e.id, names: e.names, abell: e.abell, description: e.description }; } @@ -362,8 +362,8 @@ async function main(): Promise { process.stderr.write(` parsed ${msccRows.length} MSCC rows\n`); process.stderr.write('parsing cluster seed…\n'); - const seedRaw = readFileSync(rawDataPath('clusters.seed'), 'utf8'); - const seed = parseClusterSeed(seedRaw); + const seedRaw = readFileSync(rawDataPath('structures.seed'), 'utf8'); + const seed = parseStructureSeed(seedRaw); process.stderr.write(` loaded ${seed.length} featured seed entries\n`); // ── Build entries ────────────────────────────────────────────────────────── @@ -394,7 +394,7 @@ async function main(): Promise { category[i] = e.category; } - const catalog: ClusterCatalog = { + const catalog: StructureCatalog = { count, positions, physicalRadiusMpc, @@ -403,13 +403,13 @@ async function main(): Promise { category, }; - const buf = encodeClusterCatalog(catalog); - writeFileSync(resolve(outDir, 'clusters.ccat'), Buffer.from(buf)); - process.stderr.write(`wrote clusters.ccat (${buf.byteLength} bytes, ${count} records)\n`); + const buf = encodeStructureCatalog(catalog); + writeFileSync(resolve(outDir, 'structures.ccat'), Buffer.from(buf)); + process.stderr.write(`wrote structures.ccat (${buf.byteLength} bytes, ${count} records)\n`); // ── Write meta sidecar ───────────────────────────────────────────────────── - writeMetaSidecar(entries.map(toMeta), resolve(outDir, 'clusters_meta.json')); - process.stderr.write('wrote clusters_meta.json\n'); + writeMetaSidecar(entries.map(toMeta), resolve(outDir, 'structures_meta.json')); + process.stderr.write('wrote structures_meta.json\n'); } // Allow the script to be both executed (CLI) and imported (tests). diff --git a/tools/utils/io/rawDataRegistry.ts b/tools/utils/io/rawDataRegistry.ts index 948fac00d..600f2e949 100644 --- a/tools/utils/io/rawDataRegistry.ts +++ b/tools/utils/io/rawDataRegistry.ts @@ -49,8 +49,7 @@ export const RAW_DATA = { path: 'data/raw/2mrs/J_ApJS_199_26_ReadMe', kind: 'file', source: 'committed', - description: - 'VizieR ReadMe for 2MRS — byte-offset specs the table-3 parser relies on.', + description: 'VizieR ReadMe for 2MRS — byte-offset specs the table-3 parser relies on.', }, '2mrs.xsc-pa': { path: 'data/raw/2mrs/2mass_xsc_pa.csv', @@ -131,12 +130,12 @@ export const RAW_DATA = { // ─── Clusters (curated featured structures) ─────────────────────────── - 'clusters.seed': { - path: 'data/cluster_anchors.seed.json', + 'structures.seed': { + path: 'data/structure_anchors.seed.json', kind: 'file', source: 'committed', description: - 'Hand-authored seed list of featured galaxy clusters, superclusters, and voids. Drives the cluster-coverage POI build.', + 'Hand-authored seed list of featured galaxy clusters, superclusters, and voids. Drives the structure-coverage POI build.', }, // ─── Famous (curated catalog) ────────────────────────────────────────── @@ -298,7 +297,7 @@ export const RAW_DATA = { description: 'MCXC Meta-Catalogue X-ray galaxy Clusters — 1743 clusters with RA/Dec, z, L500, M500, R500. Fixed-width ASCII, 323 bytes/row.', upstream: 'https://cdsarc.cds.unistra.fr/ftp/J/A+A/534/A109/mcxc.dat', - fetcher: 'tools/fetch/fetchClusterCatalogs.ts', + fetcher: 'tools/fetch/fetchStructureCatalogs.ts', readme: 'mcxc.readme', }, 'mcxc.readme': { @@ -308,7 +307,7 @@ export const RAW_DATA = { description: 'VizieR ReadMe for J/A+A/534/A109 — byte-offset spec for mcxc.dat. Downloaded alongside the table.', upstream: 'https://cdsarc.cds.unistra.fr/ftp/J/A+A/534/A109/ReadMe', - fetcher: 'tools/fetch/fetchClusterCatalogs.ts', + fetcher: 'tools/fetch/fetchStructureCatalogs.ts', }, 'mcxc.sha256': { path: 'data/raw/mcxc/mcxc.dat.sha256', @@ -316,7 +315,7 @@ export const RAW_DATA = { source: 'committed', description: 'SHA-256 sidecar for mcxc.dat — committed so the parser can detect truncated or stale downloads.', - fetcher: 'tools/fetch/fetchClusterCatalogs.ts', + fetcher: 'tools/fetch/fetchStructureCatalogs.ts', }, // ─── MSCC — Main SuperCluster Catalogue (Chow-Martinez+ 2014) ───────── @@ -328,7 +327,7 @@ export const RAW_DATA = { description: 'MSCC Main SuperCluster Catalogue — 601 superclusters with RA/Dec, z, max separation, member cluster list. Fixed-width ASCII, 324 bytes/row.', upstream: 'https://cdsarc.cds.unistra.fr/ftp/J/MNRAS/445/4073/mscc.dat', - fetcher: 'tools/fetch/fetchClusterCatalogs.ts', + fetcher: 'tools/fetch/fetchStructureCatalogs.ts', readme: 'mscc.readme', }, 'mscc.readme': { @@ -338,7 +337,7 @@ export const RAW_DATA = { description: 'VizieR ReadMe for J/MNRAS/445/4073 — byte-offset spec for mscc.dat. Downloaded alongside the table.', upstream: 'https://cdsarc.cds.unistra.fr/ftp/J/MNRAS/445/4073/ReadMe', - fetcher: 'tools/fetch/fetchClusterCatalogs.ts', + fetcher: 'tools/fetch/fetchStructureCatalogs.ts', }, 'mscc.sha256': { path: 'data/raw/mscc/mscc.dat.sha256', @@ -346,7 +345,7 @@ export const RAW_DATA = { source: 'committed', description: 'SHA-256 sidecar for mscc.dat — committed so the parser can detect truncated or stale downloads.', - fetcher: 'tools/fetch/fetchClusterCatalogs.ts', + fetcher: 'tools/fetch/fetchStructureCatalogs.ts', }, // ─── StarNet++ weights (famous-galaxy curator) ──────────────────────── diff --git a/tools/volumes/auditCf4Anchors.ts b/tools/volumes/auditCf4Anchors.ts index a29a52ab5..fc36ecb78 100644 --- a/tools/volumes/auditCf4Anchors.ts +++ b/tools/volumes/auditCf4Anchors.ts @@ -24,7 +24,7 @@ */ import { readFileSync } from 'node:fs'; import { readNpy } from '../parsers/npyReader'; -import { parseClusterSeed } from '../parsers/parseClusterSeed'; +import { parseStructureSeed } from '../parsers/parseStructureSeed'; import { raDecDistToEqCart } from '../../src/utils/math/raDecDistToEqCart'; import type { Vec3 } from '../../src/@types/math/Vec3'; import { eqToSg, sgToVoxelIndex } from '../utils/math/coordinates'; @@ -94,13 +94,13 @@ function main(): void { // Load the seed and filter to clusters — the audit checks overdensities // at well-known cluster positions, not at supercluster or void centres. - const clusterEntries = parseClusterSeed( - readFileSync(rawDataPath('clusters.seed'), 'utf-8'), + const clusterEntries = parseStructureSeed( + readFileSync(rawDataPath('structures.seed'), 'utf-8'), ).filter((e) => e.category === 'cluster'); // Pre-compute each anchor's continuous voxel index from RA/Dec/distance. // The numpy axis order is what we vary below — the SG coords are fixed. - // ClusterSeedEntry has raHours/decDeg/distMpc (a superset of SkyCoord), + // StructureSeedEntry has raHours/decDeg/distMpc (a superset of SkyCoord), // so each entry passes straight into raDecDistToEqCart. const anchorSgIdx: { name: string; sgIdx: [number, number, number] }[] = clusterEntries.map( (a) => { diff --git a/tools/volumes/verifyCf4Scfd.ts b/tools/volumes/verifyCf4Scfd.ts index 5623fc0c8..3fbafa809 100644 --- a/tools/volumes/verifyCf4Scfd.ts +++ b/tools/volumes/verifyCf4Scfd.ts @@ -19,14 +19,10 @@ */ import { readFileSync } from 'node:fs'; import { decodeScalarField } from '../../src/data/scalarFieldFormat'; -import { parseClusterSeed } from '../parsers/parseClusterSeed'; +import { parseStructureSeed } from '../parsers/parseStructureSeed'; import { raDecDistToEqCart } from '../../src/utils/math/raDecDistToEqCart'; import type { Vec3 } from '../../src/@types/math/Vec3'; -import { - eqToSg, - eqCartToRaDecDist, - voxelToEqCart, -} from '../utils/math/coordinates'; +import { eqToSg, eqCartToRaDecDist, voxelToEqCart } from '../utils/math/coordinates'; import { f16BitsToFloat } from '../utils/math/floatHalf'; import { percentileOf } from '../utils/math/percentile'; import { rawDataPath } from '../utils/io/rawDataRegistry'; @@ -34,7 +30,7 @@ import { rawDataPath } from '../utils/io/rawDataRegistry'; /** * Minimal shape needed by `sampleAtAnchor` — raHours/decDeg/distMpc for the * position conversion + names[0] for the display label. Matches the fields - * present on ClusterSeedEntry. + * present on StructureSeedEntry. */ type NamedAnchor = { names: string[]; @@ -88,7 +84,7 @@ function main(): void { sorted.sort(); // Load seed once; split by category for the three verification passes. - const allSeed = parseClusterSeed(readFileSync(rawDataPath('clusters.seed'), 'utf-8')); + const allSeed = parseStructureSeed(readFileSync(rawDataPath('structures.seed'), 'utf-8')); const CLUSTER_ENTRIES = allSeed.filter((e) => e.category === 'cluster'); const SUPERCLUSTER_ENTRIES = allSeed.filter((e) => e.category === 'supercluster'); const VOID_ENTRIES = allSeed.filter((e) => e.category === 'void');