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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 2 additions & 1 deletion packages/bake/authoring/damit-asteroids/author.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import { dirname, relative, resolve } from 'node:path';
import sharp from 'sharp';
import { createSourceManifest } from '@cssearth/objects/node';
import { NEUTRAL_CATALOGUE_COLOUR } from '@cssearth/objects';
import { ENTRY_EVIDENCE } from '@cssearth/bake/sources';
import { shapeMaterialRaster, renderRadialSnapshot } from '@cssearth/bake/objects/layers/terrestrial';
import { elementsUrl, vectorsUrl } from '../../../../packages/astronomy/cli/lib/horizons.mts';
Expand Down Expand Up @@ -377,7 +378,7 @@ async function authorBody(body: Body) {
worldFrame: { referenceFrame: 'sun-icrf', epochJdTt: EPOCH_JD, originM, presentationToReference: [1, 0, 0, 0, -1, 0, 0, 0, 1], orbitUpReference: [0, 0, 1],
metersPerUnit: radiusMeters / geometryRadius, bodyRadiusM: radiusMeters },
page: { stylesheets: ['src/renderers/css/styles/templates/small-body.css'], metadata: { url: 'prepared/page.json' } },
catalog: { name, classification: 'asteroid', color: '#aaaaaa', distanceAu: semiMajorAxisAu, description: body.text.card, systemName: 'Solar System', context: {} },
catalog: { name, classification: 'asteroid', color: NEUTRAL_CATALOGUE_COLOUR, distanceAu: semiMajorAxisAu, description: body.text.card, systemName: 'Solar System', context: {} },
},
schema: descriptor.schema, type: descriptor.type,
prepared: { format: 'cssearth-css-object@5', url: 'prepared/object.json' },
Expand Down
6 changes: 4 additions & 2 deletions packages/bake/src/objects/geometry/radial-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ export type SourcePin = s.Infer<typeof sourcePin>;
export const webpEncoding = obj({quality: opt(n), alphaQuality: opt(n), lossless: opt(s.boolean), nearLossless: opt(s.boolean), smartSubsample: opt(s.boolean), effort: opt(n), preset: opt(s.literal('default', 'picture', 'photo', 'drawing', 'icon', 'text'))});
const radiusMapping = s.union(obj({kind: s.literal('linear'), scale: n}), obj({kind: s.literal('piecewise-log'), knots: arr(vector2)}));
export type RadiusMapping = s.Infer<typeof radiusMapping>;
const bandCommon = {color: vector3, opacity: n, arcs: opt(obj({centers: arr(n), halfWidth: n}))};
/** A ring band is drawn from its published normal optical depth alone: its opacity is 1 - exp(-opticalDepth) and its colour
* the neutral gray of a body with no measured colour (rings.ts). A recipe sets neither. */
const bandCommon = {opticalDepth: n, arcs: opt(obj({centers: arr(n), halfWidth: n}))};
const band = s.union(
obj({...bandCommon, envelope: s.literal('constant'), bounds: vector2, inclusive: opt(s.tuple(s.boolean, s.boolean))}),
obj({...bandCommon, envelope: s.literal('tent'), center: n, width: n, edgeGain: n, minimumHalfWidthPixels: opt(n)}),
Expand All @@ -18,7 +20,7 @@ export type RadialShadow = s.Infer<typeof shadow>;
const overlay = obj({kind: s.literal('projected-strip-shadow'), bodyRadius: n, outerRadius: n, direction: vector2, color: vector3, startFraction: n, edgeFraction: n, maximumAlpha: n, centerInset: n, marginPixels: n, output: str, encoding: webpEncoding});
export type RadialOverlay = s.Infer<typeof overlay>;
const common = {size: n, densities: arr(n), output: str, encoding: webpEncoding, densityMode: opt(s.literal('independent', 'downsample-highest')), overlays: opt(arr(overlay))};
const annular = obj({...common, kind: s.literal('annular-field'), outerRadius: n, grid: obj({centerInset: n, sampleOffset: n, marginPixels: n, scaleOrder: s.literal('divide-multiply', 'multiply-hypot')}), composition: s.literal('maximum', 'front-to-back'), alphaUnits: n, maximumAlpha: n, mapping: radiusMapping, bands: arr(band), defaultColor: opt(vector3), shadow: opt(shadow), wedges: opt(obj({count: n}))});
const annular = obj({...common, kind: s.literal('annular-field'), outerRadius: n, grid: obj({centerInset: n, sampleOffset: n, marginPixels: n, scaleOrder: s.literal('divide-multiply', 'multiply-hypot')}), composition: s.literal('maximum', 'front-to-back'), alphaUnits: n, maximumAlpha: n, mapping: radiusMapping, bands: arr(band), shadow: opt(shadow), wedges: opt(obj({count: n}))});
export type AnnularLayer = s.Infer<typeof annular>;
const operationCommon = {bounds: vector2, inclusive: opt(s.tuple(s.boolean, s.boolean))};
const operation = s.union(
Expand Down
4 changes: 2 additions & 2 deletions packages/bake/src/objects/layers/giant/giant-layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ export function parseRadialLayerRecipe(input: unknown) {
(index > 0 && (knot[0] <= mapping.knots[index - 1][0] || knot[1] <= mapping.knots[index - 1][1])))) fail('invalid radius mapping.');
if (!isArray(layer.bands) || layer.bands.length === 0) fail('empty annular field.');
for (const band of layer.bands) {
if (!['constant', 'tent', 'smooth-annulus'].includes(band.envelope) || !color(band.color) ||
!Number.isFinite(band.opacity) || band.opacity < 0) fail('invalid band.');
if (!['constant', 'tent', 'smooth-annulus'].includes(band.envelope) || !positive(band.opticalDepth)) fail('invalid band.');
if ('color' in band || 'opacity' in band) fail('a band states its optical depth; its opacity and colour are not authored.');
if (band.envelope === 'tent' ? !positive(band.center) || !positive(band.width) || !positive(band.edgeGain) : !pair(band.bounds)) fail('invalid radial bounds.');
if (band.envelope === 'smooth-annulus' && (!isArray(band.fade) || band.fade.length !== 2 || !band.fade.every(positive) || band.bounds[0] <= band.fade[0])) fail('invalid annular fade.');
if (band.arcs && (!isArray(band.arcs.centers) || band.arcs.centers.length === 0 || !band.arcs.centers.every(Number.isFinite) || !positive(band.arcs.halfWidth))) fail('invalid arcs.');
Expand Down
1 change: 0 additions & 1 deletion packages/bake/src/objects/layers/giant/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ export * from './object.ts';
export * from './observed-polar.ts';
export * from './photometric-contract.ts';
export * from './photometric-disc.ts';
export * from './polar-continuation.ts';
export * from './polar-dome.ts';
export * from './polar-source-contract.ts';
export * from './presentation-contract.ts';
Expand Down
42 changes: 18 additions & 24 deletions packages/bake/src/objects/layers/giant/observed-polar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {Channels} from 'sharp';
import type { ObservedRgb, PolarDetails } from './polar-continuation.ts';
import type { DetailImage } from './polar-source-contract.ts';
import type { ObservedRgb } from './polar-dome.ts';
import { parseObservedPolarSource } from './polar-source-contract.ts';
import {mkdir,readFile,writeFile} from 'node:fs/promises';
import {resolve} from 'node:path';
Expand All @@ -10,7 +9,6 @@ import {planetographicRowsToMeshLatitude} from '../../geometry/index.ts';
import { readFitsPrimary } from '@cssearth/fits';
import {verifyObservationSources} from '../observed-surfaces/index.ts';
import { latitudeRasterBands } from './geometry.ts';
import { preparePolarContinuationAtlas, preparePolarSurfaceTransition } from './polar-continuation.ts';
import { compositePolarOverlay, layoutPolarAtlasForCaps, writeDomeRings, type DomeRingWarp, type PoleProjection } from './polar-dome.ts';
import { validateRelativePath } from './relative-path.ts';
import {measureScalarCoverage,finitePercentiles,falseColorMap} from '../observed-surfaces/index.ts';
Expand All @@ -29,13 +27,12 @@ export function parseObservedPolarRecipe(input: unknown) {
if(!/^[a-z][a-z0-9-]*$/.test(dataset.id)||ids.has(dataset.id))throw new TypeError('Invalid observed polar dataset operation.');
ids.add(dataset.id);source(dataset.source);
for(const filename of Object.values(dataset.files)){validateRelativePath(filename);if(outputs.has(filename))throw new TypeError('Observed polar outputs must be unique.');outputs.add(filename);}
for(const detail of Object.values(dataset.polarDetails??{})){source(detail.structure.path);if(detail.palette)source(detail.palette.path);}
if(dataset.operation==='rgb-polar-structure'&&(!Number.isInteger(dataset.coverage.columnStride)||dataset.coverage.columnStride<1))throw new TypeError('Invalid observed RGB coverage stride.');
if(dataset.operation==='rgb-measured-rows'&&(!Number.isInteger(dataset.coverage.columnStride)||dataset.coverage.columnStride<1))throw new TypeError('Invalid observed RGB coverage stride.');
if(dataset.operation==='rgb-observed-gaps'){
if(dataset.coverageSources.length!==3||!(dataset.planetographicAxisRatio>=1))throw new TypeError('RGB maps require three component coverage maps and an ellipsoid ratio.');
dataset.coverageSources.forEach(source);
}
if(dataset.operation==='scalar-observed-gaps'&&(!Array.isArray(dataset.palette)||dataset.palette.length<2||Object.keys(dataset.polarDetails??{}).length>0||dataset.scalar.range&&!(dataset.scalar.range[1]>dataset.scalar.range[0])))throw new TypeError('Invalid measured scalar parameters.');
if(dataset.operation==='scalar-observed-gaps'&&(!Array.isArray(dataset.palette)||dataset.palette.length<2||dataset.scalar.range&&!(dataset.scalar.range[1]>dataset.scalar.range[0])))throw new TypeError('Invalid measured scalar parameters.');
}
return {...config,sourcePins:[...sourcePaths].map(path=>({path}))};
}
Expand All @@ -45,7 +42,6 @@ export function parseObservedPolarRecipe(input: unknown) {
export function polarImageProjection(config: unknown): PoleProjection {
const recipe=parseObservedPolarRecipe(config);
const projections=recipe.datasets.map(dataset=>{
if(dataset.operation==='rgb-polar-structure')return{id:dataset.id,edgeLatitudeDegrees:dataset.continuation.measuredProjectionEdgeLatitudeDegrees??dataset.continuation.edgeLatitudeDegrees,scale:dataset.continuation.overlap??1.035};
if((dataset.projection.projection??'latitude-linear')!=='latitude-linear')throw new TypeError(`${recipe.namespace} dataset ${dataset.id}: pole tiles in the ${dataset.projection.projection} projection cannot share a dome with latitude-linear ones.`);
return{id:dataset.id,edgeLatitudeDegrees:dataset.projection.boundaryLatitudeDegrees,scale:dataset.projection.overlap??1.035};
});
Expand All @@ -70,11 +66,6 @@ export async function prepareObservedPolarSurfaces({sourceDirectory,publicDirect
maps=new Map<string, {data: Uint8Array; width: number; height: number; channels: number}>(),
coverage: Record<string, Record<string, unknown>>={},controls=[];
const {width,height,polarTileSize}=recipe.dimensions;
const load=async (spec: DetailImage)=>{
let pipeline=sharp(requireSource(sources,spec.path)).removeAlpha();
if(spec.extract)pipeline=pipeline.extract(spec.extract);
return pipeline.resize(polarTileSize*2,polarTileSize*2,spec.resize).raw().toBuffer({resolveWithObject:true});
};
const add=async(filename: string,data: Buffer)=>{
const info=await sharp(data).metadata();
if (!info.width || !info.height) throw new Error(`Observed asset has no dimensions: ${filename}`);
Expand All @@ -91,20 +82,23 @@ export async function prepareObservedPolarSurfaces({sourceDirectory,publicDirect
};
for(const dataset of recipe.datasets) {
let source1x: ObservedRgb,source2x: ObservedRgb;
let polar: ReturnType<typeof preparePolarContinuationAtlas> | ReturnType<typeof prepareMeasuredPolarAtlas>;
let polar: ReturnType<typeof prepareMeasuredPolarAtlas>;
let sourceRange: [number,number] | undefined;
let measured: {firstMeasuredRow: number; lastMeasuredRow: number; sourceMissingPixels?: number};
const bytes=requireSource(sources,dataset.source),details: PolarDetails={};
if(dataset.operation==='rgb-polar-structure') {
const original=await sharp(bytes).removeAlpha().raw().toBuffer({resolveWithObject:true});
measured=measureRgbCoverage(original,dataset.coverage);
source2x=await sharp(original.data,{raw:original.info}).resize(width*2,height*2,{fit:'fill'}).raw().toBuffer({resolveWithObject:true});
source1x=await sharp(original.data,{raw:original.info}).resize(width,height,{fit:'fill'}).raw().toBuffer({resolveWithObject:true});
for(const pole of ['north','south'] as const) { const detail=dataset.polarDetails[pole]; if(detail)details[pole]={...detail,structure:await load(detail.structure),palette:await load(detail.palette)}; }
const {edgeLatitudeDegrees,...continuation}=dataset.continuation;
polar=preparePolarContinuationAtlas({source:source2x,tileSize:polarTileSize*2,firstMeasuredRow:Math.round((90-edgeLatitudeDegrees)/180*(original.info.height-1)),lastMeasuredRow:Math.round((90+edgeLatitudeDegrees)/180*(original.info.height-1)),measuredHeight:original.info.height,polarDetails:details,...continuation});
source2x=preparePolarSurfaceTransition({source:source2x,polarDetails:details,...dataset.transition});
source1x=preparePolarSurfaceTransition({source:source1x,polarDetails:details,...dataset.transition});
const bytes=requireSource(sources,dataset.source);
if(dataset.operation==='rgb-measured-rows') {
// The map's own rows are all it shows: a row outside the pinned measured range is missing, and nothing is drawn into it.
const image=await sharp(bytes).removeAlpha().raw().toBuffer({resolveWithObject:true});
measured=measureRgbCoverage(image,dataset.coverage);
const pixels=image.info.width*image.info.height,data=Buffer.alloc(pixels*4),missing=new Uint8Array(pixels).fill(1);
for(let row=measured.firstMeasuredRow;row<=measured.lastMeasuredRow;row++)for(let column=0;column<image.info.width;column++){
const i=row*image.info.width+column;
data.set(image.data.subarray(i*image.info.channels,i*image.info.channels+3),i*4);data[i*4+3]=255;missing[i]=0;
}
const original={data,info:{width:image.info.width,height:image.info.height,channels:4},missing};
source1x=resizeObservedRgb(original,width,height);source2x=resizeObservedRgb(original,width*2,height*2);
measured={...measured,sourceMissingPixels:missing.reduce((sum,n)=>sum+n,0)};
polar=prepareMeasuredPolarAtlas(original,polarTileSize*2,{projection:'latitude-linear',...dataset.projection});
} else if(dataset.operation==='rgb-observed-gaps') {
const image=await sharp(bytes).removeAlpha().raw().toBuffer({resolveWithObject:true});
const masks=dataset.coverageSources.map(path=>{
Expand Down
Loading
Loading