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
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
18 changes: 17 additions & 1 deletion docs/surface-preparation.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,19 @@ its full box. [leaf-box.ts](../packages/bake/src/presentation/leaf-box.ts) holds
[prepared-leaf-box-direct.ts](../packages/renderer/src/rendering/prepared-leaf-box-direct.ts)): on an iPad, two
device pixels a CSS pixel, Io's level 4,160 texels wide gives 65 px of its 128 px box and its widest image 130 px.
The leaf takes that box with its image, in one write, once its step needs the whole image.
- **Stated by the bake.** An image's width and height are on its prepared entry, written from the published file by
the last step of every bake (`withImageSizes`,
[prepared-image-sizes.ts](../packages/bake/src/prepared-presentation/prepared-image-sizes.ts)), and the leaves
that draw it are the ones its texture write is bound to. The renderer reads both. It never measures an image, works
a size out of a byte count, or guesses which image a leaf draws: a leaf whose image states no size keeps its step's
box.
- **No variable carries an image.** A texture write names a slot, and the slot lists the elements that draw it
(`tree.textureBindings`). The page writes `background-image` on each of those elements, and the served markup
does the same; an image a container draws itself is a write of that container's own background. The bake still
finds the elements in a headless browser through a custom property, then ships records that name none
([texture-image-records.ts](../packages/bake/src/presentation/texture-image-records.ts)), and the runtime
shipped-form check ([shipped-runtime.ts](../packages/objects/src/prepared-data/runtime-validation/shipped-runtime.ts))
refuses to pin or mount a runtime whose tree sets a custom property or whose element reads its image from one.
- **One texel per device pixel.** The box follows the screen's pixel ratio where that is a whole number of two or
more (a phone's three gives 43.33 px for the same level); any other screen keeps the bake's two texels a CSS
pixel. The copy is by backing pixel: with the iPad's page scaled to four backing pixels a CSS pixel, Io's faces in
Expand Down Expand Up @@ -669,7 +682,10 @@ These still set their own encoding:
- Earth's full pages keep the qualities its recipe declares; its smaller
texture levels follow their page, lossy ones through the lane.
- Lighting rows and their billboards carry shading in alpha and stay lossless.
- Saturn's layered and spectral materials keep their encodings.
- Saturn's layered and spectral materials keep their encodings. Its ultraviolet and methane surface maps and their
pole atlases joined the lane on 2026-10-05: the maps, 5.92 and 3.77 MB lossless, became 0.23 and 0.14 MB with 0 and 3
of 12.8 million pixels flagged; the atlases, 0.39 and 0.35 MB, became 0.04 MB each with none flagged and alpha exact.
Decoding a lossless map took 130 to 141 ms inside the frame of a dataset switch on the iPad.
- Image-layer galaxies (M31, M33), the LMC and SMC volume banks and the Milky
Way sky keep their recipe qualities.
- Volume atlases (density in alpha, seen as stacked slices) and the
Expand Down
4 changes: 3 additions & 1 deletion packages/bake/src/contract/prepared-object-pin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { projectRoot as checkoutProjectRoot } from '@cssearth/core/node';
import { OBJECT_SCHEMA, PREPARED_OBJECT_SCHEMA, OBJECT_RUNTIME_SCHEMA, parseObjectDescriptor, PREPARED_CSS_OBJECT_FORMAT } from '@cssearth/objects';
import { OBJECT_SCHEMA, PREPARED_OBJECT_SCHEMA, OBJECT_RUNTIME_SCHEMA, parseObjectDescriptor, requireShippedRuntime, PREPARED_CSS_OBJECT_FORMAT } from '@cssearth/objects';

// Pinning a prepared object to its transport: the descriptor's `prepared` pin and page reference, and the body's inventory.
// The `prepared/object.json` transport and the page data are built from the runtime when read (@cssearth/objects/node
Expand Down Expand Up @@ -34,6 +34,8 @@ export async function pinPreparedObject(id: string, originalDescriptor: Record<s
const { preparedDirectory, descriptorPath } = target;
await mkdir(preparedDirectory, { recursive: true });
const definition = requireObjectRuntimeDefinition(JSON.parse(await readFile(resolve(preparedDirectory, 'runtime.json'), 'utf8')));
// What is pinned ships: records, with no custom property left from the bindings' working form.
requireShippedRuntime(definition);
const runtime: unknown = JSON.parse(await readFile(resolve(preparedDirectory, 'runtime.json'), 'utf8'));
const originalProperties = requireRecord(originalDescriptor.properties);
const descriptor = parseObjectDescriptor({ ...originalDescriptor, properties: { ...originalProperties, ...properties } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ export async function prepareNormalizedDiscPresentation({config:input,geometry,m
const b=createPreparedNodeTree({cssomReads:await prepareCssomDeclarationReads(geometry.leaves.map(leaf=>leaf.style))}),camera=b.element('div','polycss-camera object-render-root','perspective:1000000px');
const cameraState=createPolyCamera({zoom:config.initialScene.zoom,rotX:config.initialScene.totalPitchDegrees-materialConfig.systemRotationXDegrees,rotY:0,target:[0,0,0]}).state;
const scene=b.element('div','polycss-scene',`transform:${buildPolyCameraSceneTransform(cameraState)}`),system=b.mesh(`${namespace}-system`,depth.materialSystemTransform),body=b.mesh(`${namespace}-body`,depth.materialMeshTransform),rings=b.mesh(`${namespace}-rings`,'');
b.append(null,camera);b.append(camera,scene);for(const leaf of geometry.leaves)b.append(body,b.leaf(leaf));for(const leaf of geometry.ringLeaves)b.append(rings,b.element('s',leaf.className,leaf.style));b.append(system,rings,body);
b.append(null,camera);b.append(camera,scene);for(const leaf of geometry.leaves){const face=b.leaf(leaf);const cap='className' in leaf&&typeof leaf.className==='string'&&leaf.className.split(/\s+/u).includes(`${namespace}-pole`);face.style.backgroundImage=`var(--${namespace}-${cap?'poles':'surface'}-image)`;b.append(body,face);}for(const leaf of geometry.ringLeaves)b.append(rings,b.element('s',leaf.className,leaf.style));b.append(system,rings,body);
const materialSystem=b.mesh(`${namespace}-material-system`,depth.materialSystemTransform),counter=b.mesh(`${namespace}-fixed-material-counter`,''),material=b.mesh(`${namespace}-material`,depth.materialMeshTransform),leaf=b.element('s');leaf.style.transform=depth.leafTransform;
b.append(scene,materialSystem,system);b.append(materialSystem,counter);b.append(counter,material);b.append(material,leaf);
const width=materialConfig.presentationSize,half=width/2,centerTranslation=[1,0,0,0,0,1,0,0,0,0,1,0,half,half,0,1],inverseCenterTranslation=[1,0,0,0,0,1,0,0,0,0,1,0,-half,-half,0,1],shape=prepareNormalizedDiscProjection(materialConfig),fit=[1,0,0,0,0,shape.radiusY/shape.radiusX,0,0,0,0,1,0,0,0,0,1];
const baseProjection=multiplyPreparedMatrix4(readPreparedMatrix4(leaf.style.transform),multiplyPreparedMatrix4(centerTranslation,multiplyPreparedMatrix4(fit,inverseCenterTranslation)));leaf.style.transform=serializePreparedMatrix4(baseProjection);
const bodySystemMatrix=preparedRotationMatrix4('x',materialConfig.systemRotationXDegrees),bodyMeshMatrix=preparedRotationMatrix4('z',config.bodyRotationZDegrees),projection={equatorialRadius:materialConfig.shape.equatorialRadius*BASE_TILE,polarRadius:materialConfig.shape.polarRadius*BASE_TILE,coverageScale:half/materialConfig.rasterSurfaceRadius,bodySystemMatrix,bodyMeshMatrix,materialSystemMatrix:bodySystemMatrix,materialMeshMatrix:bodyMeshMatrix,baseProjection,centerTranslation,inverseCenterTranslation};
const{tree,index}=b.finish({camera,scene,stageClasses:[`${namespace}-stage`]});
const track:MaterialSourceTrack={id:'lighting',target:index(leaf),frame:{count:materialConfig.bank.frames,samples:lighting.presentations.map(p=>p.cameraLightDirection)},banks:[{id:'lighting',frames:lighting.presentations.map(p=>({resource:`lighting:${p.rowIndex}`,frame:p.frameIndex,row:p.rowIndex,backgroundPosition:p.backgroundPosition,backgroundSize:p.backgroundSize})),rows:lighting.rows.map((row,index)=>({row:index,resource:`lighting:${index}`,firstFrame:index*materialConfig.bank.framesPerRow,lastFrame:Math.min(lighting.presentations.length-1,(index+1)*materialConfig.bank.framesPerRow-1)})),default:null,fixed:{resource:'shadowless',frame:null,row:null,backgroundPosition:'0px 0px',backgroundSize:`${materialConfig.presentationSize}px ${materialConfig.presentationSize}px`}}],demand:{capacity,defaultFrame:lighting.defaultFrame},rotation:{kind:'ellipsoid',source:'view-sun',reference:'initial',baseDegrees:0,zeroAtPole:false,width,height:width,projection,systemTransform:materialSystem.style.transform,polePolicy:'azimuth'},frameAttribute:null,modeAttribute:null,quoted:true};
const variants=config.datasets.flatMap(dataset=>[false,true].flatMap(shadows=>[false,true].map(rings=>({when:{datasetId:dataset.id,shadows,rings},required:datasetKeys(dataset.id),writes:[{kind:'attribute',target:-1,name:'data-dataset',value:dataset.id===config.defaultDataset?null:dataset.id},{kind:'class',target:-1,name:`${namespace}-hide-rings`,value:!rings},{kind:'class',target:-1,name:`${namespace}-hide-shadows`,value:!shadows}],materials:[{track:'lighting',bank:'lighting',mode:shadows?'frames':'fixed',enabled:true,rotationEnabled:shadows,frameOverride:null,clearWhenHidden:false,fixedMode:'shadowless'}]}))));
const variants=config.datasets.flatMap(dataset=>[false,true].flatMap(shadows=>[false,true].map(rings=>({when:{datasetId:dataset.id,shadows,rings},required:datasetKeys(dataset.id),writes:[...(['surface','poles'] as const).map(layer=>({kind:'texture' as const,target:index(body),name:`--${namespace}-${layer}-image`,resource:`${layer}:${dataset.id}`,quoted:true})),{kind:'attribute',target:-1,name:'data-dataset',value:dataset.id===config.defaultDataset?null:dataset.id},{kind:'class',target:-1,name:`${namespace}-hide-rings`,value:!rings},{kind:'class',target:-1,name:`${namespace}-hide-shadows`,value:!shadows}],materials:[{track:'lighting',bank:'lighting',mode:shadows?'frames':'fixed',enabled:true,rotationEnabled:shadows,frameOverride:null,clearWhenHidden:false,fixedMode:'shadowless'}]}))));
// Lighting rows are not startup assets: shadows start off and show the one shadowless frame; rows load when shadows turn on.
const result={schema:PREPARED_PRESENTATION_SCHEMA,camera:cameraPlan,sky,sun,assets:{entries,pools:[preparedResourcePool('warm',entries,{retention:'warm',decoding:'sync'}),preparedResourcePool('lighting',entries,{retention:'selection',decoding:'sync',capacity,concurrency:capacity,reuse:true,eviction:'capacity'})],startup:[...warm.map(entry=>entry.key),...datasetKeys(config.defaultDataset)]},tree,variants,materials:[track],viewBindings:[{kind:'counter-rotation',target:index(counter),systemTransform:depth.materialSystemTransform.replace(/^transform:/u,'')}],animations:[]};
return{...result,materials:prepareMaterialTracks(result)};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {dotVector as dot3} from '../../geometry/index.ts';

/** Interior cutaways are smooth illustrations, written in the lossy lane (lossy-lane.ts). */
const INTERIOR_WEBP = { alphaQuality: 100, effort: 6 } as const;
/** Cutaway sources require an exterior default and at least three cited sources; angular/radial ordering stays local. */
const CUTAWAY_SOURCE_POLICY = { defaultView: 'exterior', minimumSources: 3, maximumWidthDegrees: 180 };
import {validateMaterialRecipe} from './recipe.ts';
import {validateRelativePath} from '../giant/index.ts';
/** Declared radial composition, two-face cutaway shading, and polar wedge rasters. */
Expand Down Expand Up @@ -125,8 +127,6 @@ const prepared = Object.freeze({



/** Cutaway sources require an exterior default and at least three cited sources; angular/radial ordering stays local. */
const CUTAWAY_SOURCE_POLICY = { defaultView: 'exterior', minimumSources: 3, maximumWidthDegrees: 180 };
function validateManifest(value:InteriorSource) {
if (value?.schema !== config.sourceSchema ||
value.defaultView !== CUTAWAY_SOURCE_POLICY.defaultView ||
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ export async function prepareLayeredOblatePresentation({publicDirectory,config:i
const carrier = b.mesh(polar ? `${namespace}-body ${namespace}-body-polar` : `${namespace}-body`, `${plan.meshTransform};animation-duration:${band.visualRotationSeconds}s`);
b.append(system, carrier); carriers.set(key, carrier);
}
for (const leaf of band.leaves) b.append(carriers.get(key)!, leaf.tag === "s" ? b.leaf(leaf) : b.element(requireString(leaf.tag), null, leaf.style));
for (const leaf of band.leaves) {
const node = leaf.tag === "s" ? b.leaf(leaf) : b.element(requireString(leaf.tag), null, leaf.style);
// A polar cap reads the dataset's pole image as the faces read its surface: from the texture write on its mesh.
if (polar && leaf.tag === "s") node.style.backgroundImage = `var(--${namespace}-poles-image)`;
b.append(carriers.get(key)!, node);
}
}
const cutaway = cutawayShown ? b.mesh(`${namespace}-cutaway`) : null;
if (cutaway) { cutaway.style.display = "none"; b.append(system, cutaway); }
Expand Down Expand Up @@ -207,6 +212,11 @@ export async function prepareLayeredOblatePresentation({publicDirectory,config:i
writes: [...cutaway ? [{ kind: "style", target: index(cutaway), name: "display", value: interiorView ? "block" : "none" }] : [],
{ kind: "attribute", target: -1, name: "data-view", value: interiorView ? "interior" : null },
{ kind: "attribute", target: -1, name: "data-dataset", value: interiorView || dataset.id === datasets.defaultDataset ? null : dataset.id },
// The surface is a texture write on each mesh of body faces, which read it (layered-oblate.ts); the bindings list
// those faces (prepared-texture-bindings.ts), so the page knows which image each face draws and its stated size.
...[...carriers].map(([key, carrier]) => key.startsWith("polar:")
? { kind: "texture" as const, target: index(carrier), name: `--${namespace}-poles-image`, resource: `poles:${content.id}`, quoted: true }
: { kind: "texture" as const, target: index(carrier), name: "--polycss-projective-texture-image", resource: `surface:${content.id}`, quoted: true }),
{ kind: "texture", target: index(ring), name: `--${namespace}-rings`, resource: `rings:${content.id}`, quoted: true },
{ kind: "class", target: -1, name: `${namespace}-hide-rings`, value: !rings },
{ kind: "class", target: -1, name: `${namespace}-hide-shadows`, value: !shadows }],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,15 @@ async function writeProjectiveSurface(source: RawImage, width: number, height: n
bandCount: latitudeBandCount,
gutter: height / latitudeBandCount / 4,
});
await sharp(packed.data, { raw: {
// A false-color picture of a band's brightness, written in the lossy lane (lossy-lane.ts). Lossless, Saturn's
// ultraviolet and methane maps were 5.92 and 3.77 MB and their decode put 150 ms in the frame of a dataset switch on
// the iPad (176 to 233 ms, where its lossy maps of the same size take 20 to 27); in the lane they are 0.23 and
// 0.14 MB, with 0 and 3 of 12.8 million pixels flagged by pixelmatch at threshold 0.1 (2026-10-05).
await writeLossyWebp(sharp(packed.data, { raw: {
width: packed.packedWidth,
height: packed.packedHeight,
channels: resized.info.channels,
} })
.webp({ lossless: true, effort: 6 })
.toFile(outputPath);
} }), outputPath);
}

async function prepareScalarSurface(plan: SpectralDataset): Promise<RawImage> {
Expand Down Expand Up @@ -327,9 +329,11 @@ async function preparePolarAtlas(body: RawImage, plan: SpectralDataset, outputPa
}
}
}
await sharp(output, {
// The caps' picture of the same band, in the lossy lane with its alpha exact (the caps' edges): 0.39 and 0.35 MB
// lossless became 0.04 MB each, with no pixel flagged (2026-10-05).
await writeLossyWebp(sharp(output, {
raw: { width: poleAtlasWidth, height: poleAtlasHeight, channels: 4 },
}).webp({ lossless: true, effort: 6 }).toFile(outputPath);
}), outputPath, { alphaQuality: 100 });
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface PagedPresentationInput { config: PresentationConfiguration; pla
textureLevels?: Awaited<ReturnType<typeof prepareTextureLevels>>; sky: PreparedCubicSkyPlan; sun: PreparedDirectionalSunPlan; controls: ShellObjectControls;
catalog?: Awaited<ReturnType<typeof preparePlaces>>; }
const materialIds = ['atmosphere'] as const;
import { prepareCssomDeclarationReads, createPreparedNodeTree, textureTileVariables, prepareMaterialTracks } from "../../../presentation/index.ts";
import { prepareCssomDeclarationReads, createPreparedNodeTree, readsTexture, textureTileVariables, prepareMaterialTracks } from "../../../presentation/index.ts";
import { seamOutsetBinding, seamOutsetInitialValue } from "../../../scene/index.ts";
import { surfaceBankInventory } from "./surface-banks.ts";

Expand Down Expand Up @@ -92,7 +92,8 @@ export async function preparePagedEllipsoidPresentation({ config, plan, datasets
grouped.set(key,carrier);(isPolar?polar:surface).push(carrier);b.append(parent,carrier);
if(isPolar)carrier.style.setProperty(`--${config.namespace}-poles-texture`,poles?`url("${poles}")`:"none");else writePages(carrier,urls);
}
for(const leaf of band.leaves)b.append(carrier,b.leaf(leaf));
// A cap draws the pole write on its carrier; a page face names its page already.
for(const leaf of band.leaves)b.append(carrier,leaf.className?.includes(marker)?readsTexture(b.leaf(leaf),`--${config.namespace}-poles-texture`):b.leaf(leaf));
}
return {surface,polar};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export async function prepareShapeModel({ descriptor, sources, objectDirectory,
const materialRoot = sphereLighting ? b.element('div', 'shape-model-material-root object-render-root') : null;
const material = sphereLighting ? b.element('s', 'shape-model-material',
`width:${scene.camera.logicalBodyDiameter}px;height:${scene.camera.logicalBodyDiameter}px;margin:${-scene.camera.logicalBodyDiameter / 2}px 0 0 ${-scene.camera.logicalBodyDiameter / 2}px`) : null;
if (material && materialRoot) { b.append(null, materialRoot); b.append(materialRoot, material); }
if (material && materialRoot) { material.style.backgroundImage = 'var(--shape-lighting)'; b.append(null, materialRoot); b.append(materialRoot, material); }
const shapeLighting = !sphereLighting ? prepareShapeLighting({ builder: b, root, axes, config, scene,
image: await publishedImageSize(published, lightingUrl, id), objectId: id }) : null;
const materialReference = shapeLighting ? {
Expand Down
6 changes: 4 additions & 2 deletions packages/bake/src/objects/layers/terrestrial/solid-scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { BASE_TILE } from '@layoutit/polycss';
import { prepareSolidBodySurface, preparePerspectiveCamera } from '../../../scene/index.ts';
import { prepareAstrometricSkySceneRegistration, prepareEclipticPresentationFrame, photographDirections, prepareDefaultCameraAngles, prepareSunReferenceViewDirection, type SolarGeometry } from '../../scene/index.ts';
import { loadAstronomyPackage } from '../../../astronomy/index.ts';
import { prepareCssomDeclarationReads, createPreparedNodeTree, prepareMaterialTracks, requirePreparedPresentation } from '../../../presentation/index.ts';
import { prepareCssomDeclarationReads, createPreparedNodeTree, readsTexture, prepareMaterialTracks, requirePreparedPresentation } from '../../../presentation/index.ts';
import { requirePreparedResourceCatalog } from '../../../contract/index.ts';
import { restoreDepthSource } from '../../../prepared-presentation/index.ts';
import { publishedImageSize } from '../shape-model/index.ts';
Expand Down Expand Up @@ -167,7 +167,9 @@ export async function prepareSolidPresentation({ config, scene: plan, material:
const body = b.mesh(`${id}-body`);
b.append(null, camera); b.append(camera, scene); b.append(scene, system); b.append(system, body);
for (const leaf of plan.bodyLeaves) {
const node = b.leaf(leaf);
// A face draws the surface write and a cap the pole write, both on the body.
const cap = (leaf.className ?? '').split(/\s+/u).includes(`${id}-polar`);
const node = readsTexture(b.leaf(leaf), `--${id}-${cap ? 'poles' : 'surface'}-image`);
Object.assign(node.attributes, leaf.attributes ?? {});
b.append(body, node);
}
Expand Down
Loading
Loading