diff --git a/.github/scripts/ci/build-ci.mts b/.github/scripts/ci/build-ci.mts index fd03808f494..b35a3612a2e 100644 --- a/.github/scripts/ci/build-ci.mts +++ b/.github/scripts/ci/build-ci.mts @@ -52,7 +52,7 @@ export function ciBuildPlan(root: string, mode: CiBuildMode): readonly CiBuildTa // Hashes icon sources with @cssearth/core/node, so it waits for the packages build. node('icons', 'site/build/prepare/prepare-shell-icons.mts', ['packages']), node('navigation', 'packages/bake/cli/prepare-navigation.mts', ['solar'], ['--catalog-only']), - node('world', 'site/build/prepare/prepare-spatial-context.ts', ['navigation'], ['src/objects/sun/source/navigation/universe.json', 'src/objects/sun/prepared/world-context.json']), + node('world', 'site/build/prepare/prepare-spatial-context.ts', ['navigation']), node('moon-labels', 'site/build/prepare/prepare-moon-labels.mts', ['world']), node('world-presentation', 'site/build/prepare/prepare-world-presentation.mts', ['world']), ); diff --git a/docs/performance/world-context-by-system.md b/docs/performance/world-context-by-system.md index 0a29c9d60ec..c3a22360134 100644 --- a/docs/performance/world-context-by-system.md +++ b/docs/performance/world-context-by-system.md @@ -44,8 +44,9 @@ by one rule the bake and the build share ([world-holders.ts](../../packages/obje every object with a file from the root of the tree down, and the rows object entries carry. It names no holder. Node tools, tests, the build and the deploy check read it; no page does. -The bake computes every body in one full context, `src/objects/sun/prepared/world-context.json`, in the Sun's package: the -frame is the Sun's and the Sun's scene is framed from it. Node tools read it; no page does. It is the only world file there. +The bake computes every body in one full context, `src/objects/observable-universe/prepared/world-context.json`, in the root +object's package beside the summary and the index, from that package's `source/navigation/universe.json`. Node tools and +tests read it; no page does. A file is read by every page when a body in it is drawn from anywhere: one that orbits nothing or the Sun and is no plain dot (the Solar System's planets, the Milky Way's featured stars, the galaxies and clusters, a featured star of diff --git a/labs/experiments/native-scroll/context.mts b/labs/experiments/native-scroll/context.mts index 45c9694f4ff..5c3d1b6faec 100644 --- a/labs/experiments/native-scroll/context.mts +++ b/labs/experiments/native-scroll/context.mts @@ -17,7 +17,7 @@ import { prepareNativeOrbitCulling,nativeOrbitCullingCss } from './orbit-culling import { BODY_INDICATOR_DIAMETER } from '@cssearth/renderer/universe/world-context/context-scale.ts'; const root = pathToFileURL(resolve('.') + '/'); -const plan = parsePreparedWorldContext(JSON.parse(await readFile(new URL('src/objects/sun/prepared/world-context.json', root), 'utf8'))); +const plan = parsePreparedWorldContext(JSON.parse(await readFile(new URL('src/objects/observable-universe/prepared/world-context.json', root), 'utf8'))); const descriptor: unknown = JSON.parse(await readFile(new URL('src/objects/milky-way-volume/object.json', root), 'utf8')); const volume = await loadPreparedCssVolume(descriptor, { read: async path => { const bytes = await readFile(new URL(`src/objects/milky-way-volume/${path}`, root)); diff --git a/package.json b/package.json index becd9ba6434..7974263f0c6 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "prepare:titles": "node site/build/prepare/prepare-shell-titles.mts", "prepare:volume": "node packages/bake/cli/prepare-volume.mts", "prepare:world-presentation": "node site/build/prepare/prepare-moon-labels.mts && node site/build/prepare/prepare-world-presentation.mts", - "prepare:world-context": "node site/build/prepare/prepare-spatial-context.ts src/objects/sun/source/navigation/universe.json src/objects/sun/prepared/world-context.json && pnpm prepare:world-presentation", + "prepare:world-context": "node site/build/prepare/prepare-spatial-context.ts && pnpm prepare:world-presentation", "pretest": "pnpm prepare:shell", "server-answers:record": "node .github/scripts/server-answers/record.mts", "server-answers:diff": "node .github/scripts/server-answers/diff.mts", diff --git a/packages/bake/src/world-context/spatial-context.test.ts b/packages/bake/src/world-context/spatial-context.test.ts index e346fc4caa7..7b2d03c9fce 100644 --- a/packages/bake/src/world-context/spatial-context.test.ts +++ b/packages/bake/src/world-context/spatial-context.test.ts @@ -6,7 +6,7 @@ import { M_PER_AU } from '@cssearth/astronomy'; import { parseWorldContextSource, prepareWorldContext } from './spatial-context.ts'; import type { OrbitalState } from './spatial-context.ts'; -const sourcePath = 'src/objects/sun/source/navigation/universe.json'; +const sourcePath = 'src/objects/observable-universe/source/navigation/universe.json'; // Unit cases supply their own body inventory; the application resolves catalogue membership. async function readSource() { return { ...JSON.parse(await readFile(sourcePath, 'utf8')), bodies: [{id: 'test-body', name: 'Test body', color: '#aaaaaa'}] }; diff --git a/packages/renderer/src/sky/prepared-sky-runtime.test.ts b/packages/renderer/src/sky/prepared-sky-runtime.test.ts index 7891e97e2b1..a9044c2a651 100644 --- a/packages/renderer/src/sky/prepared-sky-runtime.test.ts +++ b/packages/renderer/src/sky/prepared-sky-runtime.test.ts @@ -89,7 +89,7 @@ afterEach(() => { unstubAllGlobals(); catalogMount.mock.resetCalls(); }); test('cold bootstrap keeps catalogue and image banks descriptor-only, then reuses their first navigation load', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url); - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const image: PreparedCssImageLayers = { ...volume, id: 'lazy-image', bankViews: volume.stacks.map(stack => ({ axis: stack.axis, normalUnits: stack.axis === 'x' ? [1, 0, 0] : stack.axis === 'y' ? [0, 1, 0] : [0, 0, 1], samplingStepUnits: 1 })) }; @@ -116,7 +116,7 @@ test('cold bootstrap keeps catalogue and image banks descriptor-only, then reuse test('banks declared after the universe is made reach its mounted layers and every later mount, as addSystems does', () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url); - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const frame = volume.frame; const { datasetBillboards } = datasetBanks([{ id: 'hosted-cloud', frame, attachedTo: 'host' }]); @@ -141,7 +141,7 @@ test('banks declared after the universe is made reach its mounted layers and eve test('the galaxy is its bulge slices at every overview scope, with no disc plane and no impostor views', () => { const base = new URL('../../../../src/', import.meta.url); - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const document = new FakeDocument(); const mounted = createPreparedUniverse({ context, volume, pointAppearance: readCanonicalPointField(), sprites: {}, @@ -228,7 +228,7 @@ test('optional sky is validated as part of the existing volume capability and sh }); test('near star cube is fully handed off before solar-system parallax produces duplicate stars', () => { - const source = JSON.parse(readFileSync(new URL('../../../../src/objects/sun/source/navigation/universe.json', import.meta.url), 'utf8')); + const source = JSON.parse(readFileSync(new URL('../../../../src/objects/observable-universe/source/navigation/universe.json', import.meta.url), 'utf8')); const astronomicalUnitM = 149_597_870_700; assert.equal(source.stars.fadeStartDistanceM, 100 * astronomicalUnitM); assert.equal(source.stars.fullDistanceM, 200 * astronomicalUnitM); @@ -238,7 +238,7 @@ test('near star cube is fully handed off before solar-system parallax produces d for (const { withSky } of [{ withSky: true }, { withSky: false }]) test(`nearer than the disc's half-height the NASA band is the sky; from twice that the galaxy volume is (sky=$withSky)`, () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url); - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const sky = { ...fixture(), referenceFrame: volume.frame.referenceFrame, epochJdTt: volume.frame.epochJdTt }; const { sky: _originalSky, ...volumeWithoutSky } = volume; @@ -294,7 +294,7 @@ for (const { withSky } of [{ withSky: true }, { withSky: false }]) test(`nearer test('a galaxy backing that loads while the camera rests is placed before any layer shows', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const backing = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/backing.json', base), 'utf8')); stubGlobal('fetch', async () => new Response(JSON.stringify(backing))); @@ -322,7 +322,7 @@ test('a galaxy backing that loads while the camera rests is placed before any la test('a galaxy backing whose ring image cannot resolve mounts no layer at all', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const backing = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/backing.json', base), 'utf8')); stubGlobal('fetch', async () => new Response(JSON.stringify(backing))); @@ -343,7 +343,7 @@ test('a galaxy backing whose ring image cannot resolve mounts no layer at all', test('shared universe draws only resolved nebulae and never prefetches their datasets with the galaxy', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const frame: PreparedCssVolume['frame'] = { referenceFrame: volume.frame.referenceFrame, epochJdTt: volume.frame.epochJdTt, originM: [context.focus.positionM[0], context.focus.positionM[1], context.focus.positionM[2] + 50 * parsecM], @@ -446,7 +446,7 @@ test('shared universe draws only resolved nebulae and never prefetches their dat test('an unloaded independent bank is fetched by proximity while the galactic fade is zero; a galactic one waits for the galaxy', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const frame: PreparedCssVolume['frame'] = { referenceFrame: volume.frame.referenceFrame, epochJdTt: volume.frame.epochJdTt, originM: [context.focus.positionM[0], context.focus.positionM[1], context.focus.positionM[2] + 50 * parsecM], @@ -496,7 +496,7 @@ test('an unloaded independent bank is fetched by proximity while the galactic fa test('selecting a nebula loads its bank on demand even while it is out of view', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const frame: PreparedCssVolume['frame'] = { referenceFrame: volume.frame.referenceFrame, epochJdTt: volume.frame.epochJdTt, originM: [context.focus.positionM[0], context.focus.positionM[1], context.focus.positionM[2] + 5000 * parsecM], @@ -548,7 +548,7 @@ test('selecting a nebula loads its bank on demand even while it is out of view', test('hidden dataset banks are bounded, active subscriptions pin them, and eviction reloads saved presentation', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); const volume = JSON.parse(readFileSync(new URL('objects/milky-way-volume/prepared/volume.json', base), 'utf8')).data as PreparedCssVolume; const makeBank = (id: string, distancePc: number): PreparedVolumeDatasets => { const frame: PreparedCssVolume['frame'] = { referenceFrame: volume.frame.referenceFrame, epochJdTt: volume.frame.epochJdTt, @@ -621,7 +621,7 @@ function completedPixel(host: FakeElement, image: FakeElement, sky: FakeElement test('authoritative detailed close-up gates background fetch, painting and publication without changing the NASA sky weight', async () => { stubGlobal('HTMLElement', FakeElement); stubGlobal('Element', FakeElement); const base = new URL('../../../../src/', import.meta.url), parsecM = 3.085677581491367e16; - const context = JSON.parse(readFileSync(new URL('objects/sun/prepared/world-context.json', base), 'utf8')); + const context = JSON.parse(readFileSync(new URL('objects/observable-universe/prepared/world-context.json', base), 'utf8')); context.volume.opacityProfile = { model: 'logarithmic-distance', fadeStartDistanceM: 1e19, fullDistanceM: 1e21, nearOpacity: 0, fullOpacity: 1 }; // The galaxy's volume is gated by the distance to the selected body, as for every body: the close-up is mid-fade. context.volume.discHalfHeightM = .5 * parsecM; diff --git a/packages/renderer/src/universe/world-context/world-context-frame.test.ts b/packages/renderer/src/universe/world-context/world-context-frame.test.ts index 548acde0a60..9f9b0c79622 100644 --- a/packages/renderer/src/universe/world-context/world-context-frame.test.ts +++ b/packages/renderer/src/universe/world-context/world-context-frame.test.ts @@ -12,7 +12,7 @@ const defined = (value: unknown): unknown => Array.isArray(value) ? value.map(de : value && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype ? Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined).map(([key, item]) => [key, defined(item)])) : value; -const plan = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../../../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); +const plan = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../../../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); // Tests move the observer in place; the planner itself receives the readonly view. type World = WorldContextView['world']; type TestView = Omit & { world: Omit & { pose: Omit & { positionM: [number, number, number] } } }; diff --git a/packages/renderer/src/universe/world-context/world-context-planner.test.ts b/packages/renderer/src/universe/world-context/world-context-planner.test.ts index 8fec0e704be..f5a0dd96b3e 100644 --- a/packages/renderer/src/universe/world-context/world-context-planner.test.ts +++ b/packages/renderer/src/universe/world-context/world-context-planner.test.ts @@ -11,7 +11,7 @@ import { FEATURED_STAR_TIER, labelImportance } from '../../labels/universe-label import { unpackPreparedBinary } from '@cssearth/objects/node'; const plan = parsePreparedWorldContext(JSON.parse(await readFile( - new URL('../../../../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); + new URL('../../../../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); const freeze = (value: T): T => { // Typed orbit arrays cannot be frozen; the test compares their contents instead. if (value && typeof value === 'object' && !ArrayBuffer.isView(value)) { diff --git a/packages/renderer/src/universe/world-context/world-context-point-source.test.ts b/packages/renderer/src/universe/world-context/world-context-point-source.test.ts index 351a849652f..8925aca5082 100644 --- a/packages/renderer/src/universe/world-context/world-context-point-source.test.ts +++ b/packages/renderer/src/universe/world-context/world-context-point-source.test.ts @@ -58,7 +58,7 @@ test('point source uses the prepared star photometry, nearest atlas color, and s }); test('the prepared Sun landmark grows smoothly from the light-year handoff without changing its physical disc', async () => { - const source = JSON.parse(await readFile(new URL('../../../../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8')); + const source = JSON.parse(await readFile(new URL('../../../../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8')); const solarPlan = parsePreparedWorldContext(source), au = 149597870700, lightYearM = 299792458 * 31557600; const sample = (distanceAu: number, context = solarPlan) => worldContextPointAppearance(context, field, { ...world(distanceAu * au), epochJdTt: context.frame.epochJdTt }, { ...viewport, focalPixels: 1280 * Math.sqrt(3) / 2 }, { selectedDetail: true })!; diff --git a/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts b/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts index d00332e7963..11bbc247f93 100644 --- a/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts +++ b/packages/renderer/src/universe/world-context/world-context-system-selection.test.ts @@ -5,7 +5,7 @@ import { isDeepStrictEqual } from 'node:util'; import { parsePreparedWorldContext } from '@cssearth/objects'; import { createWorldContextPlanner, type WorldContextView } from './world-context-planner.js'; -const plan = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../../../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); +const plan = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../../../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); test('a selected satellite overview retains its selected host locator', () => { const host = plan.bodies.find(body => body.id === 'jupiter')!; diff --git a/packages/telescope-cli/src/sphere/sphere-lane.mts b/packages/telescope-cli/src/sphere/sphere-lane.mts index e909fe561d3..56e0f190854 100644 --- a/packages/telescope-cli/src/sphere/sphere-lane.mts +++ b/packages/telescope-cli/src/sphere/sphere-lane.mts @@ -1,5 +1,5 @@ /** A measurement dataset for an existing standard sphere. No geometry or camera is authored here. */ -import { objectPageCss, parsePreparedObjectRuntime, parsePreparedWorldContext } from '@cssearth/objects'; +import { OBJECT_TREE_ROOT, objectPageCss, parsePreparedObjectRuntime, parsePreparedWorldContext } from '@cssearth/objects'; import { readFile, mkdir } from 'node:fs/promises'; import { resolve } from 'node:path'; @@ -49,7 +49,7 @@ export async function inspectMeasurementSphere(root:string,target:string){ const own = requireArray(requireRecord(properties.page).stylesheets).map(path => requireString(path)).filter(path => !shared.includes(path)); const styles = await Promise.all([...shared, ...own].map(async file => Buffer.from(objectPageCss((await pinned(resolve(root, file))).toString(), id)))); if (!worldFrame) throw new Error('Standard sphere has no prepared physical frame'); - const context = parsePreparedWorldContext(await json(resolve(root, 'src/objects/sun/prepared/world-context.json'))); + const context = parsePreparedWorldContext(await json(resolve(root, 'src/objects', OBJECT_TREE_ROOT, 'prepared/world-context.json'))); return {id,object,inputs,pinned,recipe,original,datasetId,surface,variant,required,styles,worldFrame,context}; } diff --git a/site/build/prepare/paged-star-dot-positions.mts b/site/build/prepare/paged-star-dot-positions.mts index 3a0938177c5..97818efec9c 100644 --- a/site/build/prepare/paged-star-dot-positions.mts +++ b/site/build/prepare/paged-star-dot-positions.mts @@ -18,12 +18,12 @@ import { readFile, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; import { gzipSync } from 'node:zlib'; import { hasErrorCode, isRecord } from '@cssearth/core'; -import { systemHostId, parsePreparedWorldContextPlan } from '@cssearth/objects'; +import { OBJECT_TREE_ROOT, systemHostId, parsePreparedWorldContextPlan } from '@cssearth/objects'; import { readObjectDescriptors } from '@cssearth/objects/node'; const KPC_M = 3.0856775814913673e19; const objects = resolve(import.meta.dirname, '../../../src/objects'); -const worldPath = resolve(objects, 'sun/prepared/world-context.json'); +const worldPath = resolve(objects, OBJECT_TREE_ROOT, 'prepared/world-context.json'); const world = parsePreparedWorldContextPlan(JSON.parse(await readFile(worldPath, 'utf8'))); const bodies = new Map(world.bodies.map(body => [body.id, body])); /** The object a body is inside past any system: its own, or the one of the star it is bound to (each row's `inside`, diff --git a/site/build/prepare/pin-world-files.mts b/site/build/prepare/pin-world-files.mts index a79ac74ec9b..29a3257407f 100644 --- a/site/build/prepare/pin-world-files.mts +++ b/site/build/prepare/pin-world-files.mts @@ -2,7 +2,8 @@ * Pins the world's per-object files into each object's inventory. The world step writes them into many packages: each * object's bodies (`prepared/members.json`), their orbit banks (`prepared/orbits/`), the places of its children's systems * (`prepared/places.json`), its system views (`prepared/views/`) and the dots of the plain stars inside it - * (`prepared/plain-stars.bin`), and the root object's summary and index (`prepared/world.json`, `prepared/world-index.json`). + * (`prepared/plain-stars.bin`), and the root object's summary, index and full context (`prepared/world.json`, + * `prepared/world-index.json`, `prepared/world-context.json`). * `plain-stars-far.bin` was the world's own second bank: its row leaves with the file. Each package's inventory rows * for those files are replaced by what the bake left on disk; every other row stays as it is. * diff --git a/site/build/prepare/prepare-moon-labels.mts b/site/build/prepare/prepare-moon-labels.mts index 7b56b443460..9b13764a15e 100644 --- a/site/build/prepare/prepare-moon-labels.mts +++ b/site/build/prepare/prepare-moon-labels.mts @@ -3,7 +3,7 @@ import { pathToFileURL } from 'node:url'; import { mkdir, readFile, writeFile } from 'node:fs/promises'; import { gzipSync, gunzipSync } from 'node:zlib'; import catalogue from '../../source/moon-catalogues.json' with { type: 'json' }; -import worldInput from '../../../src/objects/sun/prepared/world-context.json' with { type: 'json' }; +import worldInput from '../../../src/objects/observable-universe/prepared/world-context.json' with { type: 'json' }; import { parsePreparedWorldContext } from '@cssearth/objects'; import { hasProperMoonName, prepareBodyMoons } from '../../prepare-body-moons.mts'; import { sourceArray, sourceObject, sourceText } from '@cssearth/objects/sources'; diff --git a/site/build/prepare/prepare-spatial-context.ts b/site/build/prepare/prepare-spatial-context.ts index cb4c40d30db..ba70646d94f 100644 --- a/site/build/prepare/prepare-spatial-context.ts +++ b/site/build/prepare/prepare-spatial-context.ts @@ -31,19 +31,19 @@ export interface SpatialContextPreparationOptions { readonly sourcePath: string; readonly outputPath: string; readonly solarGeometryPath: string; - /** Directory containing object descriptor folders; inferred beside a navigation source when omitted. */ + /** Directory containing object descriptor folders; the checkout's `src/objects` when omitted. */ readonly objectsDirectory?: string; } export type SpatialContextCommandOptions = SpatialContextPreparationOptions; -/** Parse the CLI. No manifest pins the generated context, so there is nothing to refresh after writing it. */ +/** Parse the CLI. The world's source and its full context are the root object's, by rule: the command names no path. + * `--objects=` is for a fixture's objects folder. */ export function parseSpatialContextCommand(args: readonly string[], cwd = process.cwd()): SpatialContextCommandOptions { - const [sourcePath, outputPath, solarGeometryPath = resolve(cwd, 'src/platform/solar-geometry.mts'), ...extra] = args; - if (!sourcePath || !outputPath || extra.length > 0 || args.some(argument => argument.startsWith('--'))) { - throw new TypeError('Usage: prepare-spatial-context [solar-geometry.mts]'); - } - return { sourcePath: resolve(cwd, sourcePath), outputPath: resolve(cwd, outputPath), solarGeometryPath: resolve(cwd, solarGeometryPath) }; + const [objects = '--objects=src/objects', ...extra] = args, option = '--objects='; + if (extra.length > 0 || !objects.startsWith(option) || objects.length === option.length) throw new TypeError('Usage: prepare-spatial-context [--objects=]'); + const objectsDirectory = resolve(cwd, objects.slice(option.length)); + return { sourcePath: worldSourcePath(objectsDirectory), outputPath: worldContextPath(objectsDirectory), solarGeometryPath: resolve(cwd, 'src/platform/solar-geometry.mts'), objectsDirectory }; } /** The sRGB hex of a Planck spectrum at a temperature, through a CIE color-matching table (the route a star without a measured @@ -88,7 +88,7 @@ export async function prepareSpatialContext(options: SpatialContextPreparationOp const { contextColor } = await import('@cssearth/objects'); const { contextAnnotationOpacity } = await import('@cssearth/renderer/navigation/marker-presentation.ts'); const { isJplMissionTarget } = await import(pathToFileURL(resolve(process.cwd(), 'site/build/prepare/jpl-mission-targets.mts')).href) as typeof import('./jpl-mission-targets.mts'); - const objectsRoot = options.objectsDirectory ?? dirname(dirname(dirname(dirname(options.sourcePath)))); + const objectsRoot = options.objectsDirectory ?? resolve(process.cwd(), 'src/objects'); const byId = new Map(registry.map(object => [object.id, object])); // The catalogue step's discovery records, as the prepared catalogue holds them: what the world's visibility reads per body. const discoveries: Record = Object.fromEntries(registry.map(object => [object.id, object.discovery])); @@ -143,7 +143,7 @@ export async function prepareSpatialContext(options: SpatialContextPreparationOp }; if (source.frame.epochJdTt !== geometry.SOLAR_GEOMETRY_EPOCH_JD_TT) throw new TypeError('World context and solar geometry epochs differ.'); const auM = geometry.ASTRONOMICAL_UNIT_KILOMETERS * M_PER_KM; - const objectsDirectory = options.objectsDirectory ?? dirname(dirname(dirname(dirname(options.sourcePath)))); + const objectsDirectory = options.objectsDirectory ?? resolve(process.cwd(), 'src/objects'); const facts: Record = {}, states: Record = {}; for (const body of source.bodies) { const data = (BODIES as Readonly>)[body.id]; @@ -245,14 +245,11 @@ export async function prepareSpatialContext(options: SpatialContextPreparationOp return { id, bank: plainStarDotBank(id, stars, { referenceFrame: prepared.frame.referenceFrame, epochJdTt: prepared.frame.epochJdTt, originM: centre.originM })! }; }); for (const { id, bank } of dotBanks) await writeCatalogueBank({ objectDirectory: resolve(objectsRoot, id), id: bank.id, bank, published: true, inventory: async () => undefined }); - // The Sun's package held the world's own banks before the root's did. - for (const id of [PLAIN_STAR_DOT_BANK, RETIRED_PLAIN_STAR_DOT_BANK]) await rm(resolve(dirname(options.outputPath), `${id}.bin`), { force: true }); // The summary and the build's index are in the root object's package, the object nothing is outside of. The summary names // the objects this bake wrote a dot bank for, so the site asks for no other. await writeIfChanged(worldSummaryPath(objectsRoot), `${JSON.stringify({ ...summary, ...(dotBanks.length ? { dotBanks: dotBanks.map(({ id }) => id) } : {}) })}\n`); // Read by the build and Node tools only: every body's place in the world's order, and the rows object entries carry. await writeIfChanged(worldIndexPath(objectsRoot), `${JSON.stringify(index)}\n`); - for (const old of ['world-stars.json', 'world-context-summary.json', 'world-index.json']) await rm(resolve(dirname(options.outputPath), old), { force: true }); // Each holder is an object: a star's system, or the asteroid dot bank. Its members are its own package's prepared file, // published with it; a holder without a package is refused, named. const holders = new Set(), placed = new Set(); @@ -284,9 +281,12 @@ export async function prepareSpatialContext(options: SpatialContextPreparationOp placed.add(table.id); await writeIfChanged(placesFilePath(objectsRoot, table.id), `${JSON.stringify(table.file)}\n`); } - // A package that holds no members, places or orbit any more loses its file, and the Sun's old folders of them go. + // A package that holds no members, places or orbit any more loses its file. for (const entry of await readdir(objectsRoot, { withFileTypes: true })) { if (!entry.isDirectory()) continue; + // The summary, the index and the full context are the root object's alone: a copy in another package is from before they + // were, and the pins would publish it as that package's. + if (entry.name !== OBJECT_TREE_ROOT) for (const name of ['world.json', 'world-index.json', 'world-context.json']) await rm(resolve(objectsRoot, entry.name, 'prepared', name), { force: true }); if (!holders.has(entry.name)) await rm(memberFilePath(objectsRoot, entry.name), { force: true }); if (!placed.has(entry.name)) await rm(placesFilePath(objectsRoot, entry.name), { force: true }); const directory = resolve(objectsRoot, entry.name, 'prepared', 'orbits'), kept = orbits.get(entry.name); @@ -295,8 +295,6 @@ export async function prepareSpatialContext(options: SpatialContextPreparationOp if (!starsInside.has(entry.name)) await rm(resolve(objectsRoot, entry.name, 'prepared', `${PLAIN_STAR_DOT_BANK}.bin`), { force: true }); await rm(resolve(objectsRoot, entry.name, 'prepared', `${RETIRED_PLAIN_STAR_DOT_BANK}.bin`), { force: true }); } - for (const old of ['world-systems', 'world-orbits']) await rm(resolve(dirname(options.outputPath), old), { recursive: true, force: true }); - await rm(resolve(dirname(options.outputPath), 'world-orbits.bin'), { force: true }); // System framing's camera candidates, one file per host, read when navigation frames that system: each in the package of // the system the host is inside (its parent), which a host with members always is. const owned = new Map>(); @@ -313,8 +311,6 @@ export async function prepareSpatialContext(options: SpatialContextPreparationOp const directory = resolve(objectsRoot, entry.name, 'prepared', 'views'), kept = owned.get(entry.name); for (const name of await readdir(directory).catch(() => [] as string[])) if (!kept?.has(name)) await rm(resolve(directory, name)); } - await rm(resolve(dirname(options.outputPath), 'system-views'), { recursive: true, force: true }); - await rm(resolve(dirname(options.outputPath), 'world-system-views.json'), { force: true }); } /** The star packages that are dots of a galaxy's own bank, by id: the names of each bank's tracked table @@ -334,14 +330,26 @@ async function packagedGalaxyStars(objectsRoot: string): Promise//prepared/world-context.json`); else the output's own folder, so a - * run that writes its context anywhere else (a test's temporary folder) keeps every file it writes beside it. */ + * the output is the root object's prepared file (`//prepared/world-context.json`); else the output's own folder, + * so a run that writes its context anywhere else (a test's temporary folder) keeps every file it writes beside it. */ export function worldFilesRoot(options: Pick): string { if (options.objectsDirectory !== undefined) return options.objectsDirectory; const folder = dirname(options.outputPath); return basename(folder) === 'prepared' ? dirname(dirname(folder)) : folder; } +/** The world's definition: the root object's source, `source/navigation/universe.json` (frame, focus, catalogue membership, + * camera, sky and the distances its layers fade over). */ +export function worldSourcePath(objectsRoot: string): string { + return resolve(objectsRoot, OBJECT_TREE_ROOT, 'source', 'navigation', 'universe.json'); +} + +/** Every body of the world in one file, for the build's tools and tests: the root object's prepared `world-context.json`. + * No page reads it. */ +export function worldContextPath(objectsRoot: string): string { + return resolve(objectsRoot, OBJECT_TREE_ROOT, 'prepared', 'world-context.json'); +} + /** The world file every page reads: the root object's prepared `world.json` (frame, camera and sky facts and the focus). */ export function worldSummaryPath(objectsRoot: string): string { return resolve(objectsRoot, OBJECT_TREE_ROOT, 'prepared', 'world.json'); diff --git a/site/build/prepare/world-files.mts b/site/build/prepare/world-files.mts index a24bca8adae..1a4002ee4c8 100644 --- a/site/build/prepare/world-files.mts +++ b/site/build/prepare/world-files.mts @@ -1,4 +1,4 @@ /** The files the world step writes into many packages' `prepared/` and pins itself (pin-world-files.mts): each object's bodies * (`members.json`), their orbit banks (`orbits/`), the places of its children's systems (`places.json`), its system views - * (`views/`), and the root object's summary, index and plain-star dot banks. */ -export const worldFile = (name: string) => ['members.json', 'places.json', 'plain-stars.bin', 'plain-stars-far.bin', 'world.json', 'world-index.json'].includes(name) || name.startsWith('views/') || name.startsWith('orbits/'); + * (`views/`), the dots of the plain stars inside it, and the root object's summary, index and full context. */ +export const worldFile = (name: string) => ['members.json', 'places.json', 'plain-stars.bin', 'plain-stars-far.bin', 'world.json', 'world-index.json', 'world-context.json'].includes(name) || name.startsWith('views/') || name.startsWith('orbits/'); diff --git a/site/test/moon-labels.test.mts b/site/test/moon-labels.test.mts index f403fc7c6e2..8f9adf0685f 100644 --- a/site/test/moon-labels.test.mts +++ b/site/test/moon-labels.test.mts @@ -28,7 +28,7 @@ test('disabled captions respect foreground labels, planet occlusion and overview }); test('major moon orbits remain enabled and minor moon orbits are suppressed across planets', async () => { - const world = requireRecord(JSON.parse(await readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8')), 'world context'); + const world = requireRecord(JSON.parse(await readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8')), 'world context'); const bodies = requireArray(world.bodies, 'world bodies').map(value => { const body = requireRecord(value, 'world body'); const orbit = body.orbit === undefined ? undefined : { centerBodyId: requireString(requireRecord(body.orbit, 'orbit').centerBodyId, 'orbit centre') }; diff --git a/site/test/prepare-spatial-context.test.mts b/site/test/prepare-spatial-context.test.mts index 6b17cf0a033..572bb6bef21 100644 --- a/site/test/prepare-spatial-context.test.mts +++ b/site/test/prepare-spatial-context.test.mts @@ -13,18 +13,22 @@ import { prepareSceneDistance } from '@cssearth/bake/navigation'; import { parseSpatialContextCommand, prepareSpatialContext, worldFilesRoot } from '../build/prepare/prepare-spatial-context.ts'; const root = process.cwd(); -const sourcePath = resolve(root, 'src/objects/sun/source/navigation/universe.json'); +const sourcePath = resolve(root, 'src/objects/observable-universe/source/navigation/universe.json'); const solarGeometryPath = resolve(root, 'src/platform/solar-geometry.mts'); const contextEntries = (await readCatalog(resolve(import.meta.dirname, '../../src/objects'), prepareSceneDistance)).filter(body => body.context && body.id !== 'sun') .sort((a, b) => (a.context!.order ?? Number.MAX_SAFE_INTEGER) - (b.context!.order ?? Number.MAX_SAFE_INTEGER) || a.id.localeCompare(b.id, 'en')); -test('the command takes its paths and refuses unknown options', () => { - const generated = parseSpatialContextCommand(['source.json', 'prepared.json'], '/repo'); - assert.equal(generated.sourcePath, '/repo/source.json'); - assert.equal(generated.outputPath, '/repo/prepared.json'); +test("the command names no path: the world's source and full context are the root object's, in the objects folder it is given", () => { + const generated = parseSpatialContextCommand([], '/repo'); + assert.equal(generated.sourcePath, '/repo/src/objects/observable-universe/source/navigation/universe.json'); + assert.equal(generated.outputPath, '/repo/src/objects/observable-universe/prepared/world-context.json'); assert.equal(generated.solarGeometryPath, '/repo/src/platform/solar-geometry.mts'); + assert.equal(generated.objectsDirectory, '/repo/src/objects'); + const fixture = parseSpatialContextCommand(['--objects=fixture/objects'], '/repo'); + assert.equal(fixture.outputPath, '/repo/fixture/objects/observable-universe/prepared/world-context.json'); + assert.equal(worldFilesRoot(fixture), '/repo/fixture/objects'); - assert.throws(() => parseSpatialContextCommand(['source.json', 'prepared.json', '--unknown'], '/repo'), /Usage:/); + for (const args of [['source.json', 'prepared.json'], ['--unknown'], ['--objects='], ['--objects=a', '--objects=b']]) assert.throws(() => parseSpatialContextCommand(args, '/repo'), /Usage:/); }); @@ -129,7 +133,7 @@ test('all authored bodies retain parent-relative ephemeris orbits in one physica const source = JSON.parse(await readFile(sourcePath, 'utf8')); // A context written outside an objects folder keeps the world's per-object files beside it, never in a folder above. assert.equal(worldFilesRoot({ outputPath }), directory); - assert.equal(worldFilesRoot({ outputPath: resolve(root, 'src/objects/sun/prepared/world-context.json') }), resolve(root, 'src/objects')); + assert.equal(worldFilesRoot({ outputPath: resolve(root, 'src/objects/observable-universe/prepared/world-context.json') }), resolve(root, 'src/objects')); assert.ok(JSON.parse(await readFile(resolve(directory, 'solar-system/prepared/members.json'), 'utf8')).bodies.id.includes('mercury')); // Catalogue entries first, then the bodies drawn from their astronomy records around a packaged host. assert.deepEqual(result.bodies.filter((body: { unpackaged?: true }) => !body.unpackaged).map((body: { id: string }) => body.id), contextEntries.map(body => body.id)); diff --git a/site/test/prepared-world-context.test.ts b/site/test/prepared-world-context.test.ts index 1172aa8f31f..c48c74c0b37 100644 --- a/site/test/prepared-world-context.test.ts +++ b/site/test/prepared-world-context.test.ts @@ -785,7 +785,7 @@ test('context alignment accepts observed Linux roundoff but rejects detached ori // Every malformed case parses the whole generated universe again, so the test's time grows with the number of bodies. test('accepts the generated Sun context and rejects detached or malformed prepared data', { timeout: 20000 }, async () => { - const source = JSON.parse(await readFile(fileURLToPath(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url)), 'utf8')) as Record; + const source = JSON.parse(await readFile(fileURLToPath(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url)), 'utf8')) as Record; const [{ readCatalog }, { parseNavigationDistance }] = await Promise.all([import('@cssearth/objects/node'), import('@cssearth/objects')]); // Each entry's distance as `prepare:catalog` placed it in the registry. // Every body of the world: the scenes, the packages the host draws (galaxies, clusters, nebulae) and the objects seen from inside. @@ -842,7 +842,7 @@ test('accepts the generated Sun context and rejects detached or malformed prepar }); test('the Earth reference remains painted when its physical marker has faded at outer-system scale', async () => { - const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); + const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); const document = new FakeDocument(), host = document.createElement('section'), before = document.createElement('i'); host.clientWidth = 1280; host.clientHeight = 720; host.append(before); const layer = mountTestContext({ host: host as unknown as HTMLElement, before: before as unknown as Element, @@ -864,7 +864,7 @@ test('the Earth reference remains painted when its physical marker has faded at }); test('prepared planetary systems retain identified moon paths and retire offscreen context annotations', async () => { - const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); + const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); const document = new FakeDocument(), host = document.createElement('section'), before = document.createElement('i'); host.clientWidth = 800; host.clientHeight = 600; host.append(before); const layer = mountTestContext({ host: host as unknown as HTMLElement, before: before as unknown as Element, @@ -962,7 +962,7 @@ for (const orbitRenderer of ['bars', 'strokes'] as const) test(`${orbitRenderer} // Parsed once for both systems below. The code is the same for every host: a planet with many moons and a star with planets. let worldContext: Promise> | undefined; for (const planet of ['saturn', 'trappist-1']) test(`${planet} moon orbits stay complete across selection, hover, flight and zoom`, async () => { - worldContext ??= readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8').then(text => parsePreparedWorldContext(JSON.parse(text))); + worldContext ??= readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8').then(text => parsePreparedWorldContext(JSON.parse(text))); // The system alone is mounted: mounting the whole universe once per system grew with systems times bodies, and ~1,000 exoplanet // hosts (batch 1, 2026-09-29) made this test run for most of an hour. const universe = await worldContext, systemIds = new Set([planet, ...required([universe.focus, ...universe.bodies].find(body => body.id === planet)!.systemView).memberIds]); @@ -1008,7 +1008,7 @@ for (const planet of ['saturn', 'trappist-1']) test(`${planet} moon orbits stay }); test('initial Jupiter system framing makes the four large moons and their labels readable', async () => { - const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); + const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); const document = new FakeDocument(), host = document.createElement('section'), before = document.createElement('i'); host.clientWidth = 1280; host.clientHeight = 720; host.append(before); const viewport = { focalPixels: 1100, framingRadiusPixels: 200, principalOffsetPixels: [0, 0] as const, @@ -2819,7 +2819,7 @@ test('CSSOM transform serialization cannot turn an unchanged publication into an // (2026-10-04), where a 30 s limit passed on one run and failed on the next. test('the orbit banks decode to the orbits of the full prepared file, each vertex within half an Int32 step', { timeout: 120_000 }, async () => { const objects = new URL('../../src/objects/', import.meta.url); - const full = parsePreparedWorldContext(JSON.parse(await readFile(new URL('sun/prepared/world-context.json', objects), 'utf8'))); + const full = parsePreparedWorldContext(JSON.parse(await readFile(new URL('observable-universe/prepared/world-context.json', objects), 'utf8'))); // Every object's file read, as Node reads the world (site/world-context-plan.mts): the summary and the index are the root object's. const index = JSON.parse(await readFile(new URL('observable-universe/prepared/world-index.json', objects), 'utf8')) as { files: string[] }; const summary = await parseCompleteWorldContext(JSON.parse(await readFile(new URL('observable-universe/prepared/world.json', objects), 'utf8')), @@ -2897,7 +2897,7 @@ test('a body circle holds a dot in the body color until its own disc outgrows th }); test('inside the Solar System, moons without a circle stay inside their planet dot and other stars are dimmed', async () => { - const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); + const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); const document = new FakeDocument(), host = document.createElement('section'), before = document.createElement('i'); host.clientWidth = 1280; host.clientHeight = 720; host.append(before); const layer = mountTestContext({ host: host as unknown as HTMLElement, before: before as unknown as Element, @@ -2924,7 +2924,7 @@ test('inside the Solar System, moons without a circle stay inside their planet d // The inertia gate (docs/performance/motion-freezes-membership.md): while the camera coasts, retained DOM changes only // transform and opacity, plus the orbit strokes' paint exception. Production shape: strokes, and a coast reports rotation. async function orbitEarth(options: { coast: boolean }) { - const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/sun/prepared/world-context.json', import.meta.url), 'utf8'))); + const context = parsePreparedWorldContext(JSON.parse(await readFile(new URL('../../src/objects/observable-universe/prepared/world-context.json', import.meta.url), 'utf8'))); const document = new FakeDocument(), host = document.createElement('section'), before = document.createElement('i'); host.clientWidth = 820; host.clientHeight = 1094; host.append(before); const layer = mountTestContext({ host: host as unknown as HTMLElement, before: before as unknown as Element, orbitRenderer: 'strokes', diff --git a/site/test/system-framing.test.mts b/site/test/system-framing.test.mts index 9acd10bd208..1722e7ffa04 100644 --- a/site/test/system-framing.test.mts +++ b/site/test/system-framing.test.mts @@ -6,7 +6,7 @@ import { OBJECTS, SCENE_OBJECTS } from '../objects.mts'; import { seedObjectDirectory } from '../object-directory.mts'; // A page knows the objects it has read; this test holds the whole registry. seedObjectDirectory(OBJECTS); -import contextInput from '../../src/objects/sun/prepared/world-context.json' with { type: 'json' }; +import contextInput from '../../src/objects/observable-universe/prepared/world-context.json' with { type: 'json' }; import { STELLAR_SYSTEMS, SYSTEM_FRAMING_RADII, SYSTEM_VIEWS, SYSTEM_VIEW_HOSTS, loadSystemView, systemFramingRadii, systemFramingRect, systemViewTarget } from '../system-framing.mts'; import { bodyViewAtCamera } from '../zoom-scope.mts'; import { createPreparedWorldNavigation } from '../prepared-world-navigation.mts'; diff --git a/site/test/zoom-scope.test.mts b/site/test/zoom-scope.test.mts index 1f050e3b42b..8512fd9b182 100644 --- a/site/test/zoom-scope.test.mts +++ b/site/test/zoom-scope.test.mts @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { sourceTest } from '@cssearth/objects/node/source-test'; const test = sourceTest(); -import preparedContext from '../../src/objects/sun/prepared/world-context.json' with { type: 'json' }; +import preparedContext from '../../src/objects/observable-universe/prepared/world-context.json' with { type: 'json' }; import { bodyViewAtCamera, zoomFrameDistanceM, zoomScopeAtCamera, viewDistance, type ZoomStep } from '../zoom-scope.mts'; import { GALAXY_SCALE } from '@cssearth/renderer/labels/universe-label-policy.ts'; import { presentWorldCamera } from '@cssearth/renderer/navigation/world-camera.ts'; diff --git a/src/objects/observable-universe/README.md b/src/objects/observable-universe/README.md index db365322a57..6de6eabffb4 100644 --- a/src/objects/observable-universe/README.md +++ b/src/objects/observable-universe/README.md @@ -13,6 +13,10 @@ The card's facts cite their sources in [source/content/object.json](source/conte 1. `node packages/bake/cli/prepare-objects.mts --object=observable-universe` prepares its scene through the standard authored lane. The [recipe](object.json) declares no surface, so the scene is the camera, sky and world frame. When the scene mounts, its frame is moved to the star the view is centred on (the Sun on a cold page). 2. `node site/build/prepare/companion-context.mts observable-universe` saves the list marker from the default dataset's picture. +## The world + +Nothing is outside this object, so its package holds what belongs to the whole world and to no one body. [source/navigation/universe.json](source/navigation/universe.json) defines the world: its frame, the body at its origin, the camera's range and the distances its layers fade over. `pnpm prepare:world-context`, a shared step and not a step of this object's recipe, reads it and writes `prepared/world.json` (what every page reads), `prepared/world-index.json` (the build's table) and `prepared/world-context.json` (every body in one file, for Node tools and tests; no page reads it) here, and every other world file into the package of the object that holds its bodies ([how the world is split](../../../docs/performance/world-context-by-system.md)). + ## Known problems - The framing radius is the nominal solar radius, 695,700 km (IAU 2015 Resolution B3): it is seen from inside, centred on a star. It is a presentation value, not an extent. diff --git a/src/objects/observable-universe/inventory.json b/src/objects/observable-universe/inventory.json index b6b5b093e1d..5a5e593c32e 100644 --- a/src/objects/observable-universe/inventory.json +++ b/src/objects/observable-universe/inventory.json @@ -61,6 +61,12 @@ "bytes": 1968, "sha256": "4cc0456edeeecaa387b11c97c870acb69cd19b27f873bbd58c4e926d08a5e87c" }, + { + "location": "prepared", + "filename": "world-context.json", + "bytes": 58689179, + "sha256": "d66e886591c6af3dbf307d60b653af82fd7544b073cf01bd6ff84c163a22f791" + }, { "location": "prepared", "filename": "world-index.json", diff --git a/src/objects/observable-universe/source/manifest.json b/src/objects/observable-universe/source/manifest.json index 21bbb4b005f..470e0ebec0e 100644 --- a/src/objects/observable-universe/source/manifest.json +++ b/src/objects/observable-universe/source/manifest.json @@ -35,6 +35,13 @@ }, { "path": "preparation/navigation.json" + }, + { + "path": "navigation/universe.json", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored definition of the world every page shares: its reference frame, epoch and orientation, the body at its origin, catalogue membership, the camera's range and the distances its layers fade over. The world step (pnpm prepare:world-context) reads it by rule; it is no recipe source of this object." + } } ] } diff --git a/src/objects/sun/source/navigation/universe.json b/src/objects/observable-universe/source/navigation/universe.json similarity index 100% rename from src/objects/sun/source/navigation/universe.json rename to src/objects/observable-universe/source/navigation/universe.json diff --git a/src/objects/sun/README.md b/src/objects/sun/README.md index 959b6a9029c..14d36aac7a2 100644 --- a/src/objects/sun/README.md +++ b/src/objects/sun/README.md @@ -14,7 +14,7 @@ Source selections, recorded trials and open questions are in the [investigation | Corona · 171 Å | SDO AIA 171 Å CR2311 FITS | Ultraviolet light from the quiet corona and upper transition region. | | Corona · 193 Å | SDO AIA 193 Å CR2311 FITS | A different band sensitive to coronal and hot flare plasma. The arrows switch between the two corona maps. | -The Solar System overview's credits also include the planets, moons, asteroids and comets in the [shared world](source/presentation/solar-system.json). Each body keeps its imagery, measurements and full acknowledgments in its [own object package](../). The [shared orbital preparation](../../../packages/bake/cli/prepare-solar-geometry.mts) combines analytical models with retained Horizons states at the displayed scene epoch; these are not live ephemerides. The [world navigation recipe](source/navigation/universe.json) samples every prepared orbit at 90 vertices. +The Solar System overview's credits also include the planets, moons, asteroids and comets in the [shared world](source/presentation/solar-system.json). Each body keeps its imagery, measurements and full acknowledgments in its [own object package](../). The [shared orbital preparation](../../../packages/bake/cli/prepare-solar-geometry.mts) combines analytical models with retained Horizons states at the displayed scene epoch; these are not live ephemerides. The [world's definition](../observable-universe/source/navigation/universe.json), in the root object's package, samples every prepared orbit at 90 vertices. ## Processing diff --git a/src/objects/sun/inventory.json b/src/objects/sun/inventory.json index f254ce16c2c..7452d7c888b 100644 --- a/src/objects/sun/inventory.json +++ b/src/objects/sun/inventory.json @@ -247,12 +247,6 @@ "bytes": 2080, "sha256": "a1f39fa3c7bdf30cff9213abcecd5f5e93d4045d50bbe53749072676c40725f9" }, - { - "location": "prepared", - "filename": "world-context.json", - "bytes": 58689179, - "sha256": "d66e886591c6af3dbf307d60b653af82fd7544b073cf01bd6ff84c163a22f791" - }, { "location": "prepared", "filename": "world-navigation.json", diff --git a/src/objects/sun/source/manifest.json b/src/objects/sun/source/manifest.json index e0a8d47ded9..88a18b17d02 100644 --- a/src/objects/sun/source/manifest.json +++ b/src/objects/sun/source/manifest.json @@ -1343,22 +1343,6 @@ "reason": "Project-authored source acquisition recipe; published inputs retain their own identities and hashes." } }, - { - "id": "sun-world-context-navigation", - "path": "navigation/universe.json", - "origin": "Project-authored physical navigation context at the pinned solar-geometry epoch", - "credit": "cssEarth project authors", - "license": "Project-authored data", - "acquisition": "Authored numerical source validated against the checked-in solar geometry adapter", - "redistribution": "Project-authored source data", - "consumers": [ - "world-context" - ], - "sourceBinding": { - "kind": "local", - "reason": "Project-authored navigation context with physical records and a pinned epoch; its component sources remain cited in the navigation data." - } - }, { "id": "sun-preparation-raster", "path": "preparation/raster.json",