Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/surface-preparation.md
Original file line number Diff line number Diff line change
Expand Up @@ -417,7 +417,9 @@ its georeferenced sampler at true east longitudes, correlates that with the prep
edge in 2° steps (`measureAtlasLeftEdge`), and refuses the preparation when the best edge is more than 4° from the
declared one and correlates at least 0.2 better. A minimap with framing (`source/presentation/minimap.json`) starts at
the declared edge plus (centre − 180°); the check allows for that, and so must anyone who draws Gazetteer rims on a
minimap by hand. A redraw-only run carries the published feature anchors forward, so it runs the full preparation when
minimap by hand. A planet locked to its star (`cssearth-synchronous-rotation@1`) with no framing record is centred on
longitude 0, the point that faces the star, so its day side is one disc in the middle of the minimap instead of two halves
at its edges (`readMinimapCenter`). A redraw-only run carries the published feature anchors forward, so it runs the full preparation when
the surface map's edge differs from the one the published feature record states.

On 2026-09-28 this found eight bodies drawn half a turn from their frames: Iapetus, Ganymede, Ariel, Miranda, Oberon,
Expand Down
25 changes: 21 additions & 4 deletions packages/bake/src/objects/default-view/dataset-coverage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Vector3 } from '@cssearth/engine';
import { detectMissingCoverage } from '../../raster/index.ts';
import { isRecord } from '@cssearth/core';
import { readPreparedControls } from '@cssearth/objects/node';
import { SYNCHRONOUS_ROTATION_SCHEMA } from '@cssearth/objects';

const DEGREE = Math.PI / 180;
export interface DatasetCoverage { readonly dataset: string; readonly missing: Uint8Array; readonly width: number; readonly height: number;
Expand All @@ -17,16 +18,32 @@ export interface DatasetCoverage { readonly dataset: string; readonly missing: U
const absent = (error: unknown): undefined => { if (isRecord(error) && error.code === 'ENOENT') return undefined; throw error; };
const optionalJson = (path: string): Promise<unknown> => readFile(path, 'utf8').then(text => JSON.parse(text) as unknown, absent);

/** The body longitude a minimap is centred on: its framing record's (`source/presentation/minimap.json`); else 0 for a planet
* locked to its star, whose longitude 0 faces the star (`cssearth-synchronous-rotation@1`), so its day side is one disc in the
* middle and not split across both edges; else none, and the minimap starts where its map does. */
export async function readMinimapCenter(objectDirectory: string): Promise<number | undefined> {
const framing = await optionalJson(resolve(objectDirectory, 'source/presentation/minimap.json'));
const center = isRecord(framing) ? framing.centerLongitudeDegrees : undefined;
if (center !== undefined) {
if (typeof center !== 'number' || !Number.isFinite(center)) throw new TypeError(`${objectDirectory}: minimap framing centerLongitudeDegrees is ${String(center)}, not a number.`);
return center;
}
const descriptor = await optionalJson(resolve(objectDirectory, 'object.json'));
const recipe = isRecord(descriptor) && isRecord(descriptor.properties) ? descriptor.properties.recipe : undefined;
const rotation = isRecord(recipe) && Array.isArray(recipe.sources) ? recipe.sources.find(entry => isRecord(entry) && entry.id === 'rotation') : undefined;
if (!isRecord(rotation) || typeof rotation.path !== 'string') return undefined;
const source = await optionalJson(resolve(objectDirectory, rotation.path));
return isRecord(source) && source.schema === SYNCHRONOUS_ROTATION_SCHEMA ? 0 : undefined;
}

/** The default dataset's coverage, or none for a body without prepared datasets or a minimap of its default dataset. */
export async function readDefaultDatasetCoverage(objectDirectory: string, mapLeftEdgeLongitudeDeg: number): Promise<DatasetCoverage | undefined> {
const [controls, minimaps, framing] = await Promise.all([readPreparedControls(resolve(objectDirectory, 'prepared')).catch(absent),
optionalJson(resolve(objectDirectory, 'prepared/minimaps.json')), optionalJson(resolve(objectDirectory, 'source/presentation/minimap.json'))]);
const [controls, minimaps, center] = await Promise.all([readPreparedControls(resolve(objectDirectory, 'prepared')).catch(absent),
optionalJson(resolve(objectDirectory, 'prepared/minimaps.json')), readMinimapCenter(objectDirectory)]);
const dataset = isRecord(controls) && isRecord(controls.datasets) && typeof controls.datasets.defaultDataset === 'string' ? controls.datasets.defaultDataset : undefined;
const image = dataset && isRecord(minimaps) && Array.isArray(minimaps.images) ? minimaps.images.find(entry => isRecord(entry) && entry.id === dataset) : undefined;
if (!dataset || !isRecord(image)) return undefined;
if (typeof image.path !== 'string') throw new TypeError(`${objectDirectory}: minimap ${dataset} has no path.`);
const center = isRecord(framing) ? framing.centerLongitudeDegrees : undefined;
if (center !== undefined && typeof center !== 'number') throw new TypeError(`${objectDirectory}: minimap framing centerLongitudeDegrees is ${String(center)}, not a number.`);
// prepare-surface-minimaps rolls a framed map so its left edge is map longitude (center - 180).
const offset = center === undefined ? 0 : ((center - 180) % 360 + 360) % 360;
const { data, info } = await sharp(resolve(objectDirectory, 'prepared', image.path)).removeAlpha().raw().toBuffer({ resolveWithObject: true });
Expand Down
25 changes: 25 additions & 0 deletions packages/bake/src/surface-previews/surface-minimaps.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,31 @@ test('a model-only dataset can omit a misleading flat map without opening its im
} finally { await rm(root, { recursive: true, force: true }); }
});

test('a planet locked to its star centres its minimap on longitude 0, the side that faces the star', async () => {
const root = await mkdtemp(resolve(tmpdir(), 'locked-minimap-test-'));
try {
await mkdir(resolve(root, 'prepared'));
await mkdir(resolve(root, 'source/preparation'), { recursive: true });
// The day side, longitudes 270° to 90° E, is white across both edges of the map; the night side is black in the middle.
const day = Buffer.alloc(32 * 16 * 3);
for (let y = 0; y < 16; y++) for (let x = 0; x < 32; x++) if (x < 8 || x >= 24) day.fill(255, (y * 32 + x) * 3, (y * 32 + x + 1) * 3);
await sharp(day, { raw: { width: 32, height: 16, channels: 3 } }).png().toFile(resolve(root, 'day.png'));
await writeFile(resolve(root, 'prepared/surfaces.json'), JSON.stringify({ surfaces: [{ id: 'day', map: { url: '/scenes/planet/day.png' } }] }));
await writeFile(resolve(root, 'object.json'), JSON.stringify({ properties: { recipe: { sources: [{ id: 'rotation', path: 'source/preparation/rotation.json' }] } } }));
const options = { objectDirectory: root, publicDirectory: root, outputDirectory: resolve(root, 'prepared'), solarGeometry };
const row = async () => {
await prepareSurfaceMinimaps(options);
// Read the bytes: libvips caches a decoded file by its name.
const { data } = await sharp(await readFile(resolve(root, 'prepared/minimaps/day.webp'))).greyscale().raw().toBuffer({ resolveWithObject: true });
return [2, 16, 29].map(x => data[8 * 32 + x]! > 128 ? 'day' : 'night');
};
await writeFile(resolve(root, 'source/preparation/rotation.json'), JSON.stringify({ schema: 'cssearth-synchronous-rotation@1' }));
assert.deepEqual(await row(), ['night', 'day', 'night']);
await writeFile(resolve(root, 'source/preparation/rotation.json'), JSON.stringify({ schema: 'cssearth-linear-rotation@1' }));
assert.deepEqual(await row(), ['day', 'night', 'day']);
} finally { await rm(root, { recursive: true, force: true }); }
});

// Solid-body preparation records source metadata here; it is not a file path.
test('a prepared photo map uses its map URL when source is a provenance record', async () => {
const {default:sharp} = await import('sharp');
Expand Down
10 changes: 5 additions & 5 deletions packages/bake/src/surface-previews/surface-minimaps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { isArray, isRecord, requireRecord, requireArray, requireString, requireFiniteNumber, shape, text, number, array, optional } from '@cssearth/core';
import type {ResizeOptions,Sharp} from 'sharp';
import { DECORATIVE_WEBP, applyUnderlay, composeLimbPreview, detectMissingCoverage, readRgba, withAlpha } from '../raster/index.ts';
import { coverageDirection } from '../objects/default-view/index.ts';
import { coverageDirection, readMinimapCenter } from '../objects/default-view/index.ts';
import type {SurfacePreviewDirectories} from './surface-preview-source.ts';
import {optionalPreviewJson as optionalJson,parsePreviewControls,parsePreviewSurface} from './surface-preview-source.ts';
const parseMinimapFraming=shape({centerLongitudeDegrees:optional(number),excludeDatasets:optional(array(text))});
Expand Down Expand Up @@ -110,6 +110,7 @@ export async function prepareSurfaceMinimaps({ objectDirectory, publicDirectory,
throw new TypeError('Choose existing photographic surfaces for minimap refresh.');
const framingValue=await optionalJson(resolve(objectDirectory, 'source/presentation/minimap.json'));
const framing=framingValue && parseMinimapFraming(framingValue);
const center = await readMinimapCenter(objectDirectory);
const images = [];
const excluded = framing?.excludeDatasets ?? [];
if (!isArray(excluded) || excluded.some(id => typeof id !== 'string' ||
Expand Down Expand Up @@ -162,10 +163,9 @@ export async function prepareSurfaceMinimaps({ objectDirectory, publicDirectory,
if (interpreted.missing) exactMissing = { missing: interpreted.missing, width, height };
}
} else pipeline = sharp(input).resize(minimapResize(nearest));
if (framing?.centerLongitudeDegrees !== undefined) {
if (!Number.isFinite(framing.centerLongitudeDegrees)) throw new Error('Invalid minimap framing');
if (center !== undefined) {
const { data, info } = await pipeline.raw().toBuffer({ resolveWithObject: true });
const offset = Math.round(((framing.centerLongitudeDegrees - 180) % 360 + 360) % 360 / 360 * info.width);
const offset = Math.round(((center - 180) % 360 + 360) % 360 / 360 * info.width);
const shifted = Buffer.alloc(data.length);
for (let y = 0; y < info.height; y++) {
const row = y * info.width * info.channels, split = offset * info.channels;
Expand All @@ -174,7 +174,7 @@ export async function prepareSurfaceMinimaps({ objectDirectory, publicDirectory,
}
pipeline = sharp(shifted, { raw: info });
}
const framedOffset = framing?.centerLongitudeDegrees === undefined ? 0 : ((framing.centerLongitudeDegrees - 180) % 360 + 360) % 360;
const framedOffset = center === undefined ? 0 : ((center - 180) % 360 + 360) % 360;
const coverage = await minimapCoverage(pipeline, framedOffset, exactMissing);
const result = await writeMinimap(pipeline, nearest, resolve(outputDirectory, path));
images.push({ id: surface.id, path, width: result.width, height: result.height, ...(coverage ? { coverage } : {}),
Expand Down
6 changes: 3 additions & 3 deletions site/build/prepare/authored/prepare-authored.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { leafImageCandidates, parseGeometryProfile, prepareGeometryScene, widest
import { prepareCssPresentation, type PresentationInputs } from '@cssearth/bake/presentation';
import { prepareCelestialAssets } from '@cssearth/bake/objects/celestial';
import { checkGaiaCepheidModel, parseGaiaCepheidRow, pulsationTrack } from '@cssearth/bake/photometry';
import { assertDefaultViewFacesDataset } from '@cssearth/bake/objects/default-view';
import { assertDefaultViewFacesDataset, readMinimapCenter } from '@cssearth/bake/objects/default-view';
import { prepareObjectContentAssets } from '../../content/prepare.ts';
import { loadGeometryAdapters, presentationHostAdapters } from '@cssearth/bake/objects/host-adapters';
import { prepareRuntimeManifest } from '@cssearth/bake/delivery';
Expand Down Expand Up @@ -110,9 +110,9 @@ export async function assertMapsStartAtSurfaceMapEdge(objectDirectory: string, o
const surfaceMapPath = resolve(objectDirectory, 'source', surfaceMap);
const edge = record(JSON.parse(await readFile(surfaceMapPath, 'utf8')) as unknown, 'surface map').mapLeftEdgeLongitudeDeg;
if (typeof edge !== 'number' || !Number.isFinite(edge)) throw new TypeError(`${descriptor.id}: ${surfaceMapPath} mapLeftEdgeLongitudeDeg is ${String(edge)}, not a number.`);
const framing = await readFile(resolve(objectDirectory, 'source/presentation/minimap.json'), 'utf8').then(text => (JSON.parse(text) as { centerLongitudeDegrees?: unknown }).centerLongitudeDegrees, () => undefined);
const framing = await readMinimapCenter(objectDirectory);
// prepare-surface-minimaps rolls a framed minimap so its left edge is map longitude (center - 180).
const minimapEdge = ((edge + (typeof framing === 'number' ? framing - 180 : 0)) % 360 + 360) % 360;
const minimapEdge = ((edge + (framing === undefined ? 0 : framing - 180)) % 360 + 360) % 360;
const minimaps = record(JSON.parse(await readFile(resolve(outputDirectory, 'minimaps.json'), 'utf8')) as unknown, 'minimaps').images;
for (const value of surfaces) {
const surface = record(value, 'raster surface'), science = surface.science === undefined ? undefined : record(surface.science, `${String(surface.id)} science`);
Expand Down
4 changes: 2 additions & 2 deletions src/objects/gj-3473b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/dayside.webp",
"bytes": 1108,
"sha256": "121454bc72e820c8ab4e6445a74db0c2df3c54610a5c037e2623af26a3d4bbe1"
"bytes": 1168,
"sha256": "21289683b30799bf31377bd5af368f404bcf3267dd99e6f57b198c628c422f27"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/gj-357b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/dayside.webp",
"bytes": 1076,
"sha256": "bc1707db469b9892f0502005d8ac5a4b15dc3ff879194714c80fce6eabe5e9e2"
"bytes": 1164,
"sha256": "b5cc2ab80e132853d97997efd5686c96be3d13a9f0c236082f16bd42b6462c38"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/hat-p-12b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/dayside.webp",
"bytes": 1174,
"sha256": "76d49d4065789cc0c8656b9a07a9798d3ac3a948a844188b2aa93706c7bcde1c"
"bytes": 1184,
"sha256": "6e7409ea8001dd23e89e235fc54ad2bd604bc408e7b3f28fde7813331c2cc594"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/hat-p-7b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1266,
"sha256": "03e05bd8cb0a811042123d568044cefbc0ed3ffc85cf28822012d8480b27f466"
"bytes": 1338,
"sha256": "3791ce7b245ee866ded5036d019bdbb4cd1f50e29a16b4c9062f36ccf33e281c"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/hd-149026b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1358,
"sha256": "34a548b901c33d18d419d8c99f0095717ad2cbdbe79375f296301ed573de1469"
"bytes": 1350,
"sha256": "7ffc10abcd984a437a98010d858c0b9cdf36e6d7023eee6189d5da77a566f75d"
},
{
"location": "prepared",
Expand Down
12 changes: 6 additions & 6 deletions src/objects/hd-189733b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,20 +142,20 @@
{
"location": "prepared",
"filename": "minimaps/illustration.webp",
"bytes": 17928,
"sha256": "2885b9db9314a4fe85a4104ffbb3c3de1b801e39fbb69a6ce929ed6c656ae6de"
"bytes": 17856,
"sha256": "03cd828aa1ffe84be19b81422c6a052d190997194686da7056cb586f3d94f33e"
},
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 2146,
"sha256": "5248695c8cf8439d577431eb835a5cc3302b80826f99907a890fa07cc1fa4139"
"bytes": 1958,
"sha256": "0cd6f4bb32d4a878bd84d5cdfa61e2d50d44f13ae051d3ef46764c2819eb15fa"
},
{
"location": "prepared",
"filename": "minimaps/uncertainty.webp",
"bytes": 2178,
"sha256": "f9545fe15413cd2b34325ffa0995a4e0ebb481fa2ef6a645541e1aab6fad61af"
"bytes": 2070,
"sha256": "978ce1c588d7d9d3bdd55960604e7bef5c1f5a01f5dd73462c7b83b47465c0b9"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/hd-209458b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1360,
"sha256": "39df5a6dcb59b49d4aedcfd2f6b4defe72768656ae46d575102b5bd9d02d0a89"
"bytes": 1326,
"sha256": "3c233846b842a1e64d5c3d9ec2f1e6f8ed06fa4d12fc2bd9d7257abb5b27f709"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/kelt-16b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1306,
"sha256": "40f926c2bf88fed41a402f6056943571228065b6e8f1c93edd1696ff3b08f5b3"
"bytes": 1342,
"sha256": "372c0d0735e5d74ddeca28b1e9ae3398653c22544bd9fe3d8d1926670123b891"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/kelt-9b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1208,
"sha256": "96dca30f722271e03b0aee493c522391c2b5c36fd6cd7314f6c9726a92d0809d"
"bytes": 1244,
"sha256": "77b0622b2a3310b8b4895f1e4ac0f8b2ebc9d1f170b53821c9145b8fc6a70a78"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/kepler-22b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,8 @@
{
"location": "prepared",
"filename": "minimaps/illustration.webp",
"bytes": 13800,
"sha256": "e3131d8b9e7699a00e6701772cbb6687db7e1298b9b2149fc268f6ab18531078"
"bytes": 13726,
"sha256": "8ac99b0aa281764cf082e73beb7d0d19ecf79301e3527e15fc10d96327aa2417"
},
{
"location": "prepared",
Expand Down
8 changes: 4 additions & 4 deletions src/objects/kepler-452b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,14 @@
{
"location": "prepared",
"filename": "minimaps.json",
"bytes": 165,
"sha256": "e50951d46a42e15b5df28729b4b69422b605c5f7766c7f8fa0753f8c9b705250"
"bytes": 214,
"sha256": "e1ee0754c81d22c5fbb043df84bb28c0297a3ca7c02edcf8f2979ea811b47603"
},
{
"location": "prepared",
"filename": "minimaps/illustration.webp",
"bytes": 14248,
"sha256": "f6cd299a698f1217b3dfdd16fa512f624727046f027f5bd6a3904da295277e79"
"bytes": 14280,
"sha256": "a017aa1a1bacf4265c495dfe00653bc78741a50314bc410905caabf9f4fa3cda"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/kepler-7b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,8 @@
{
"location": "prepared",
"filename": "minimaps/illustration.webp",
"bytes": 2042,
"sha256": "87127e57f26c51ef142eefddbafb7eb86c616d62c7f0b42a416ef22824774a6a"
"bytes": 2058,
"sha256": "75230eb8ded78fb327363bdc5a1b1ed46fcfd60e4ea8ffc90fbd98fe161add73"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/lhs-3844b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1550,
"sha256": "0895dd6116ef3fee7da16993037aa06aa9c5acc72e62616539cbeac8dd49420e"
"bytes": 1582,
"sha256": "83eaf380e67693afdc8290a4c21c8d864a63d32fa07f5fe782814792426d3016"
},
{
"location": "prepared",
Expand Down
4 changes: 2 additions & 2 deletions src/objects/ltt-9779b/inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@
{
"location": "prepared",
"filename": "minimaps/temperature.webp",
"bytes": 1612,
"sha256": "83849d10525156fbc66f16af45f2df867ca94148187a9d3b5821d0ea5b8aeb02"
"bytes": 1776,
"sha256": "50f2199f7840ac48737c8aafedf919b8b8f23a33ce2b0163a4bc025b2ef448b1"
},
{
"location": "prepared",
Expand Down
Loading
Loading