diff --git a/docs/navigation-identity.md b/docs/navigation-identity.md index b4df70eb1d7..cfba0bb5e14 100644 --- a/docs/navigation-identity.md +++ b/docs/navigation-identity.md @@ -8,7 +8,7 @@ aliases and classification tabs use this inventory. | Object | A package with a catalogue entry: a route, a prepared world frame and a scene loader. A planet, a star, a system, a galaxy, a nebula and a cluster of galaxies are all this. `?dataset=` selects one of its datasets. | | Parent | The one object an object is inside (`parent` in its object.json). The Observable Universe is the root, with none. The registry refuses a missing parent, an unknown parent or a loop ([object-tree.ts](../packages/objects/src/registry/object-tree.ts)). | | Zoom facts | An object seen from inside (the Milky Way, the Local Group, the Nearby and the Observable Universe) authors when the camera's view hands over to it and how its page frames the camera (`properties.zoom`). Its scene has no body and is seen from inside, centred on the star the zoom came from (the Sun on a page opened cold). | -| Rendering resource | A volume, image bank, point field or other prepared content, attached to the object it draws for (`properties.host`). A resource descriptor alone does not publish a destination. An image bank whose light lies on walls around its middle says so (`properties.surrounds`, written by its bake): the world draws the first such bank of an object around a body that stands inside that object, as a nebula's walls around its central star, leaving out the sheets the camera stands on. A galaxy's photograph, one sheet seen from outside, is not drawn around a star inside it. | +| Rendering resource | A volume, image bank, point field or other prepared content, attached to the object it draws for (`properties.host`). A resource descriptor alone does not publish a destination. An image bank whose light lies on walls around its middle says so (`properties.surrounds`, written by its bake): the world draws the first such bank of an object around a body that stands inside that object, as a nebula's walls around its central star, leaving out the sheets the camera stands on and the sheets through the body, and with its sheets and patches taken out of their shared 3D scenes, so the browser neither cuts one along another's plane nor leaves one undrawn. A galaxy's photograph, one sheet seen from outside, is not drawn around a star inside it. | | Dataset view | A selectable `(objectId, datasetId)` presentation, which may combine several products and published sources. | | Published source | A scientific work, release or product identified in the source catalogue; a local file hash identifies retained bytes separately. | @@ -34,7 +34,10 @@ body from outside, so it takes the view once the camera is outside it: as far fr the body as that object's centre and its radius. A star of the Large Magellanic Cloud zooms out into the Cloud, M87* into M87, M87 into the Virgo Cluster. That scene is then left the way any body's is ([overview-selection.mts](../site/overview-selection.mts), -[inside-view.mts](../site/inside-view.mts)). A system can be inside another: a star +[inside-view.mts](../site/inside-view.mts)). The way back in needs no click where the object's walls +surround a body: its entry names that body (`inner`, [surrounded-body.mts](../site/surrounded-body.mts)), and +a zoom in that reaches the nearest view the object's scene allows goes on into the body's scene, which draws +the walls around it. The Homunculus Nebula zooms in to Eta Carinae, NGC 2392 to HD 59088. A system can be inside another: a star bound to another, with bodies of its own, hosts its system inside that star's. The brown dwarfs Epsilon Indi Ba and Bb are Epsilon Indi B, inside Epsilon Indi A's system, and zooming out of Epsilon Indi B hands the view to that system the same way. diff --git a/docs/performance/motion-freezes-membership.md b/docs/performance/motion-freezes-membership.md index f83e959ce4a..326c4e4efc5 100644 --- a/docs/performance/motion-freezes-membership.md +++ b/docs/performance/motion-freezes-membership.md @@ -92,6 +92,7 @@ These change paint every frame on purpose, and each has a budget: | Batched star points (`universe/batched-spatial-points.ts`, `universe/point-layer.ts`) | SVG paths of round-capped dots (`M x y h.01` in pixels: a zero-length cap is painted twice by WebKit, and under a group scaled down Chrome on a GPU draws the caps eight times as wide while a turn warps the layer), one per prepared paint color with its alpha byte, all in the world's one svg layer, over the galaxies' pictures and under the markers and labels (on the iPad at the Milky Way a layer per bank left 37 and 77 frames over 20 ms in 690 and 202 MB of layers, three shared layers 25 and 25 and 130 MB, 2026-10-03; zooming out of Earth three layers held 60.5 MB and one holds 40 MB, 2026-10-04); a bank dims by its strokes' opacity, since a group's opacity is an offscreen pass on each repaint; a turn or a zoom warps the layer's last paint, and dots a zoom adds arrive through the pacer; a bank that arrives during a zoom is read, styled and resolved in the pacer's slices and drawn once it is whole (the nearby galaxies' 39,916 points were one frame of 172 to 218 ms on the iPad and are frames of at most 33 ms, 2026-10-03); a camera whose travel moves the dots repaints them as it goes, without the margin a turn warps in, a third of the dots written, which the pause paints back; every repaint (the travelling one, the dots a zoom adds, the pause's exact one) writes the share of the dots the frame can afford: the dots are dealt into eight slots, a paint writes whole slots in turn and leaves the rest as they were, a few frames old, without projecting their points; the share is the field's part of the pacer's budget and starts at one slot after a rest (zooming in from the observable universe, the first frame set 162 KB of dot text and ran the frame loop 26 to 28 ms; it sets 22 KB and the frame is 23 to 24 ms where it was 32 to 39, and the longest frame loop of two zooms out of Earth is 13 to 14 ms where it was 18 to 27, iPad timed by the page, 2026-10-04) | One retained path per color; a color with over 2,000 points has one per slot, and its overlapping dots add up across them | Camera motion changes paint, never DOM shape | | Earth's lighting frame (`rendering/prepared-material.ts`) | `background-position` on one layer | one layer | Pending an iPad measurement | | Sky faces (`sky/prepared-sky-runtime.ts`) | `visibility` and the first `background-image` as a face crosses the view edge | the faces in view (at most 3) | A face's layer is about 85 MB at 3x; staging one ahead or keeping one through a spin would multiply memory | +| Sheets drawn around a body (`image-layers/prepared-image-layer-runtime.ts` `order`) | `z-index` of one sheet as the camera passes its plane | one write a passing, on a sheet that is left out at that moment | Around a body the sheets are out of the browser's depth sort, which did not draw the sheet beside the camera at some camera places; their paint order follows the side of the camera each is on | The footer readout (distance, coordinates, the scale ruler) holds its last reading while the camera moves, and reads once it stops. Texture levels and the body-wide seam step also wait for the camera to stop. Nothing about them has to diff --git a/packages/astronomy/data/bodies/eta-carinae.json b/packages/astronomy/data/bodies/eta-carinae.json new file mode 100644 index 00000000000..108e98e118f --- /dev/null +++ b/packages/astronomy/data/bodies/eta-carinae.json @@ -0,0 +1,30 @@ +{ + "id": "eta-carinae", + "classification": "star", + "order": 5777, + "physical": { + "name": "Eta Carinae", + "horizonsCode": null, + "meanRadiusKm": 905105700, + "gravitationalParameterKm3PerS2": 0, + "parent": null, + "effectiveTemperatureK": 9400 + }, + "physicalNotes": "Radius 1301 solar radii from Weigelt et al. (2007), arXiv:astro-ph/0609715, Table 2: the wind's K-band continuum light (2.174 µm, VLTI/AMBER) falls to a tenth of its central brightness at a diameter of 5.15 mas (±4%); half of that at 2,350 pc is 6.05 AU, 1,301 solar radii (https://arxiv.org/abs/astro-ph/0609715): 905,105,700 km at 695,700 km per solar radius. No mass is measured, so GM is 0, the records' unpublished value. Temperature 9400 K from Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3) (https://arxiv.org/abs/1204.1963). presentationUp: the display axis is a sky-plane convention; the spin axis's position angle on the sky is not measured.", + "star": { + "rightAscensionDegrees": 161.2647407537247, + "declinationDegrees": -59.68445877770452, + "positionEpochJulianYear": 2016, + "distanceParsecs": 2350, + "properMotionRaMasPerYear": 0, + "properMotionDecMasPerYear": 0, + "radialVelocityKmPerS": -25, + "presentationUp": "display-axis", + "sources": { + "position": "Gaia DR3 source 5350358584482202880 (Gaia Collaboration 2023, A&A 674, A1), ICRS at epoch J2016.0, from the archived row src/objects/eta-carinae/source/photometry/gaia-dr3-source.csv; cross-identification HD 93308, HR 4210: https://simbad.cds.unistra.fr/simbad/sim-id?Ident=Gaia+DR3+5350358584482202880", + "distance": "Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at (https://arxiv.org/abs/astro-ph/0602464): 2350 pc; Gaia DR3 gives this source no parallax (a two-parameter solution)", + "properMotion": "Gaia DR3 source 5350358584482202880 is a two-parameter solution with no proper motion; zero is assumed (at 2,350 pc, 1 mas/yr would be 11 km/s across the sky)", + "radialVelocity": "SIMBAD's radial velocity for Gaia DR3 5350358584482202880 (quality E), from 1953GCRV..C......0W; Gaia DR3 measures none (https://ui.adsabs.harvard.edu/abs/1953GCRV..C......0W/abstract)" + } + } +} diff --git a/packages/renderer/src/image-layers/prepared-image-layer-runtime.test.ts b/packages/renderer/src/image-layers/prepared-image-layer-runtime.test.ts index 7093ab92d8d..927737dc81e 100644 --- a/packages/renderer/src/image-layers/prepared-image-layer-runtime.test.ts +++ b/packages/renderer/src/image-layers/prepared-image-layer-runtime.test.ts @@ -30,15 +30,19 @@ test('selection follows baked plane normals and sample density instead of bank n // Retained DOM stand-in: exercise resource demand without browser image fetching. class ImageElement { readonly children: ImageElement[] = []; + parent: ImageElement | null = null; readonly style: Record = {}; readonly dataset: Record = {}; className = ''; removed = false; readonly ownerDocument: ImageDocument; constructor(ownerDocument: ImageDocument) { this.ownerDocument = ownerDocument; } - appendChild(child: ImageElement) { this.children.push(child); } - insertBefore(child: ImageElement, before: ImageElement) { this.children.splice(this.children.indexOf(before), 0, child); } - remove() { this.removed = true; } + // A child that is appended moves: it leaves the parent it had. + private adopt(child: ImageElement) { child.detach(); child.parent = this; } + private detach() { if (this.parent) this.parent.children.splice(this.parent.children.indexOf(this), 1); this.parent = null; } + appendChild(child: ImageElement) { this.adopt(child); this.children.push(child); } + insertBefore(child: ImageElement, before: ImageElement) { this.adopt(child); this.children.splice(this.children.indexOf(before), 0, child); } + remove() { this.detach(); this.removed = true; } } class ImageDocument { readonly elements: ImageElement[] = []; @@ -90,7 +94,7 @@ test('image textures are demanded once when their retained axis first contribute assert.equal(resolveResource.mock.callCount(), 2); }); -test('around a body, a sheet within one sampling step of the camera is left out, by its prepared corners, and draws again farther away', () => { +test('around a body, a sheet within one sampling step of the camera or of the body is left out, by its prepared corners', () => { const document = new ImageDocument(), host = document.createElement(), before = document.createElement(); host.appendChild(before); const style = { width: '1px', height: '1px', transform: 'translate3d(0,0,0)', backgroundSize: '1px 1px', backgroundPosition: '0px 0px' }; @@ -107,7 +111,8 @@ test('around a body, a sheet within one sampling step of the camera is left out, }; const runtime = mountPreparedCssImageLayers({ host: host as unknown as HTMLElement, before: before as unknown as Element, payload, resolveResource: path => `/prepared/${path}` }); const opacity = () => ['behind', 'middle', 'ahead'].map(id => document.elements.find(element => element.dataset.imageLayerLeaf === id)!.style.opacity ?? ''); - const publish = (positionM: readonly [number, number, number], around = true) => runtime.publish({ + // The body stands far behind every sheet unless a case says otherwise. + const publish = (positionM: readonly [number, number, number], around: readonly [number, number, number] | false = [0, 0, -90]) => runtime.publish({ world: { referenceFrame: 'fixture', epochJdTt: 123, pose: { positionM, orientationXyzw: [0, 0, 0, 1] } }, viewport: { focalPixels: 600, principalOffsetPixels: [0, 0] } }, around); publish([0, 0, .2], false); assert.deepEqual(opacity(), ['', '', ''], 'as its page\'s own subject a bank draws every sheet'); @@ -119,6 +124,20 @@ test('around a body, a sheet within one sampling step of the camera is left out, assert.deepEqual(opacity(), ['', '', ''], 'a sheet beside the camera, past its edge, draws'); publish([0, 0, .2]); publish([0, 0, .2], false); assert.deepEqual(opacity(), ['', '', ''], 'left-out sheets draw again when the bank is its page\'s subject'); + publish([0, 0, 50], [0, 0, 0]); + assert.equal(opacity()[1], '0', 'the sheet through the body is left out however far the camera is: the body is drawn in its place'); + publish([0, 0, 50]); + assert.equal(opacity()[1], '', 'and draws again around a body that stands elsewhere'); + // Around a body the sheets are out of their scene, under one flat element, and paint farthest first on each side of + // the camera; back as the page's subject they are in their scene again, sorted by the browser. + const over = () => ['behind', 'middle', 'ahead'].map(id => document.elements.find(element => element.dataset.imageLayerLeaf === id)!.style.zIndex ?? ''); + assert.deepEqual(over(), ['0', '1', '2'], 'in front of them all, the nearest sheet is painted last'); + publish([0, 0, .5]); + assert.deepEqual(over(), ['0', '1', '0'], 'between two sheets, each side paints toward the camera'); + publish([0, 0, -5]); + assert.deepEqual(over(), ['2', '1', '0'], 'behind them all, the order is the other way'); + publish([0, 0, -5], false); + assert.deepEqual(over(), ['', '', ''], 'in its own scene again a sheet has no paint order of its own'); }); test('a stack mounts a camera for each of its scenes, and a stack without leaves is not mounted and takes no part', () => { @@ -154,9 +173,30 @@ test('a stack mounts a camera for each of its scenes, and a stack without leaves assert.deepEqual(named('css-volume-camera').map(camera => camera.style.perspective), ['600px', '600px']); assert.deepEqual(resolveResource.mock.calls.map(call => call.arguments), [['flat.png'], ['atlas.png']]); assert.equal(leaf('a').style.backgroundImage, leaf('b').style.backgroundImage); + // Around a body the patches leave their runs' scenes for one flat element that carries the perspective, in the stack's + // order, each with the camera's transform before its own; as the page's subject again they are back in their runs. + const world = { referenceFrame: 'fixture', epochJdTt: 123, pose: { positionM: [0, 0, 10] as const, orientationXyzw: [0, 0, 0, 1] as const } }; + const projection = named('css-volume-projection')[0]!, cameras = named('css-volume-camera'), ids = (parent: ImageElement) => parent.children.map(element => element.dataset.imageLayerLeaf); + const viewport = { focalPixels: 600, principalOffsetPixels: [3, 4] as const }; + runtime.publish({ world, viewport }); + const camera = first!.style.transform; + runtime.publish({ world, viewport }, [0, 0, -90]); + assert.equal(projection.children.length, 1); + const flat = projection.children[0]!; + assert.deepEqual([flat.className, flat.style.transformStyle, flat.style.perspective, flat.style.perspectiveOrigin], ['css-volume-mesh', 'flat', '600px', '3px 4px']); + assert.deepEqual(ids(flat), ['flat', 'a', 'b']); + assert.ok(flat.children.every(element => element.style.transform === `${camera} ${style.transform}`), 'each patch carries the camera before its own transform'); + runtime.publish({ world: { ...world, pose: { ...world.pose, positionM: [0, 0, 12] } }, viewport }, [0, 0, -90]); + assert.ok(flat.children.every(element => element.style.transform !== `${camera} ${style.transform}` && element.style.transform.endsWith(` ${style.transform}`)), 'and follows it'); + runtime.publish({ world, viewport }); + assert.deepEqual(projection.children, cameras); + assert.deepEqual(named('css-volume-mesh').filter(mesh => mesh !== flat).map(ids), [['flat'], ['a', 'b']]); + assert.ok([leaf('flat'), leaf('a'), leaf('b')].every(element => element.style.transform === style.transform)); + assert.ok(first!.style.transform === camera && second!.style.transform === camera && cameras.every(each => each.style.perspective === '600px')); // Seen edge-on the stack is still the only one that draws: it keeps the whole weight. publish([Math.SQRT1_2, 0, 0, Math.SQRT1_2]); - assert.equal(named('css-volume-projection')[0]!.style.opacity, '0.999'); + // A stack of patches is drawn at 1, not under the ceiling a stack of sheets is held to. + assert.equal(named('css-volume-projection')[0]!.style.opacity, '1'); assert.equal(named('css-volume-projection')[0]!.style.visibility, 'visible'); assert.deepEqual(imageLayerAxisWeights([Math.SQRT1_2, 0, 0, Math.SQRT1_2], views.filter(view => view.axis === 'z')), { z: 1 }); }); diff --git a/packages/renderer/src/image-layers/prepared-image-layer-runtime.ts b/packages/renderer/src/image-layers/prepared-image-layer-runtime.ts index 0e12aece06d..8b81d9669da 100644 --- a/packages/renderer/src/image-layers/prepared-image-layer-runtime.ts +++ b/packages/renderer/src/image-layers/prepared-image-layer-runtime.ts @@ -48,26 +48,71 @@ export function mountPreparedCssImageLayers({ host, before, payload, resolveReso // Same camera-driven scene as a volume: its will-change keeps slice raster scales through rotation. A stack is one // scene, or the runs of leaves its view names (sceneSizes): each run under a camera of its own in the stack's // projection, painted in the stack's order, so the browser sorts and cuts only one run's leaves against each other. - const textures: { element: HTMLElement; path: string; large: boolean; sheet: Sheet | null; shown: string }[] = [], scenes: { camera: HTMLElement; scene: HTMLElement }[] = []; + const textures: { element: HTMLElement; own: string; path: string; large: boolean; sheet: Sheet | null; shown: string; + /** Where the leaf stands along its stack's normal, its place in that order, and the paint order last written. */ + along: number; rank: number; over: string }[] = []; + const runs: { camera: HTMLElement; scene: HTMLElement; mesh: HTMLElement; leaves: HTMLElement[] }[] = []; const view = views.find(view => view.axis === stack.axis); let next = 0; for (const size of view?.sceneSizes ?? [stack.leaves.length]) { - const camera = document.createElement('div'), scene = document.createElement('div'), mesh = document.createElement('div'); + const camera = document.createElement('div'), scene = document.createElement('div'), mesh = document.createElement('div'), leaves: HTMLElement[] = []; camera.className = 'css-volume-camera'; scene.className = 'css-volume-scene'; mesh.className = 'css-volume-mesh'; for (const leaf of stack.leaves.slice(next, next + size)) { const element = document.createElement('s'); element.dataset.imageLayerLeaf = leaf.id; Object.assign(element.style, leaf.style); - textures.push({ element, path: leaf.texturePath, large: leaf.widthPx * leaf.heightPx >= LARGE_IMAGE_PIXELS, - sheet: leaf.verticesUnits ? sheetOf(leaf.centerUnits, leaf.verticesUnits) : null, shown: '' }); - mesh.appendChild(element); + textures.push({ element, own: leaf.style.transform, path: leaf.texturePath, large: leaf.widthPx * leaf.heightPx >= LARGE_IMAGE_PIXELS, + sheet: leaf.verticesUnits ? sheetOf(leaf.centerUnits, leaf.verticesUnits) : null, shown: '', + along: view ? leaf.centerUnits[0] * view.normalUnits[0] + leaf.centerUnits[1] * view.normalUnits[1] + leaf.centerUnits[2] * view.normalUnits[2] : 0, rank: 0, over: '' }); + mesh.appendChild(element); leaves.push(element); } next += size; - scene.appendChild(mesh); camera.appendChild(scene); projection.appendChild(camera); scenes.push({ camera, scene }); + scene.appendChild(mesh); camera.appendChild(scene); projection.appendChild(camera); + runs.push({ camera, scene, mesh, leaves }); } + [...textures].sort((a, b) => a.along - b.along).forEach((texture, rank) => { texture.rank = rank; }); root.appendChild(projection); - return { axis: stack.axis, projection, scenes, textures, loaded: false, perspective: '', perspectiveOrigin: '', transform: '', reach: view?.samplingStepUnits ?? 0 }; + return { axis: stack.axis, projection, runs, textures, normal: view?.normalUnits ?? null, loaded: false, perspective: '', perspectiveOrigin: '', transform: '', reach: view?.samplingStepUnits ?? 0, + patches: view?.sceneSizes !== undefined, apart: null as HTMLElement | null, spare: null as HTMLElement | null }; }); + /** A stack leaves its scenes for one flat element (`apart`), or goes back. In a shared scene the browser sorts the + * leaves by depth and cuts each along the planes of the others, and seen from inside it does not draw some of them: + * turning inside the Homunculus, a wall patch 1,010 px wide went black for a step of the drag and came back, at the + * same camera places on every pass, and drew whenever one other patch of its run was taken away; inside NGC 2392 the + * sheet beside the camera came and went the same way. The same turns had 15 such jumps of the picture in the + * Homunculus and 4 to 6 in NGC 2392 with shared scenes; apart, 1 (a patch the camera passes through) and none + * (headless Chrome, 2026-10-05). Apart, the element is flat and carries the perspective, and every leaf under it + * carries the camera's transform before its own: no leaf is sorted or cut against another. Patches paint in the + * stack's order, as their runs already do; parallel sheets paint farthest first on each side of the camera (`order`). + * A leaf's transform is then written each frame: a frame of a drag went from 4.5 to 5.0 ms of main-thread time with + * the Homunculus's 1,025 patches and from 4.5 to 5.7 ms with Cassiopeia A's 1,397, so a stack is apart only while it + * is drawn around a body. The element stays built when it is not used. */ + const arrange = (bank: (typeof banks)[number], apart: boolean) => { + if ((bank.apart !== null) === apart) return; + // What was written to the arrangement left behind says nothing of this one: every write is made again. + bank.perspective = bank.perspectiveOrigin = bank.transform = ''; + if (apart) { + const flat = bank.spare ?? document.createElement('div'); + flat.className = 'css-volume-mesh'; flat.style.transformStyle = 'flat'; flat.style.willChange = 'transform'; + for (const run of bank.runs) { for (const leaf of run.leaves) flat.appendChild(leaf); run.camera.remove(); } + bank.projection.appendChild(flat); bank.apart = bank.spare = flat; + return; + } + for (const run of bank.runs) { for (const leaf of run.leaves) run.mesh.appendChild(leaf); bank.projection.appendChild(run.camera); } + for (const texture of bank.textures) { texture.element.style.transform = texture.own; if (texture.over !== '') { texture.over = ''; texture.element.style.zIndex = ''; } } + bank.apart?.remove(); bank.apart = null; + }; + /** Parallel sheets that are apart paint farthest first on each side of the camera: a sight line crosses the sheets of + * one side only, so the two sides need no order between them, and a sheet's place changes only when the camera + * passes its plane, where the sheet is left out. One write a passing, on that sheet. */ + const order = (bank: (typeof banks)[number], cameraUnits: readonly number[]) => { + if (bank.patches || !bank.normal) return; + const at = cameraUnits[0]! * bank.normal[0] + cameraUnits[1]! * bank.normal[1] + cameraUnits[2]! * bank.normal[2], last = bank.textures.length - 1; + for (const texture of bank.textures) { + const over = String(texture.along < at ? texture.rank : last - texture.rank); + if (texture.over !== over) { texture.over = over; texture.element.style.zIndex = over; } + } + }; host.insertBefore(root, before); let destroyed = false; const url = (path: string) => resolveResource(path).replace(/["\\\n\r]/g, char => `\\${char}`); @@ -79,25 +124,39 @@ export function mountPreparedCssImageLayers({ host, before, payload, resolveReso return Object.freeze({ root, /** The bank root is shown again: its large leaves wait for their decode (layer-reveal.ts). */ revealLarge() { for (const bank of banks) if (bank.projection.style.display !== 'none') revealLarge(bank); }, - /** `around`: the bank is drawn around a body that stands inside it, so the sheets the camera stands on are left out; - * as its page's own subject a bank draws every sheet, as it always has. */ - publish(publication: VolumeCameraPublication, around = false) { + /** `around`: the bank is drawn around a body that stands inside it, at that place (reference metres). The sheets the + * camera stands on are left out, and so are the sheets through the body: they hold the picture's own image of it, a + * saturated glare many times its size, and the body is drawn in their place (Eta Carinae inside the Homunculus was a + * white screen 1,200 AU out, 2026-10-05), and the leaves of each stack leave their shared scenes (`arrange`). As its + * page's own subject a bank draws every sheet, as it always has. */ + publish(publication: VolumeCameraPublication, around: readonly [number, number, number] | false = false) { if (destroyed) return; const transform = preparedVolumeCameraTransform(publication, payload.frame); const cssTransform = `translate3d(${transform.translationCssPixels.map(value => `${value}px`).join(',')}) ${worldRotationCss(transform.rotation)}`; const local = presentPhysicalPoseInVolume(publication.world.pose, payload.frame); + const body = around ? presentPhysicalPoseInVolume({ positionM: around, orientationXyzw: publication.world.pose.orientationXyzw }, payload.frame).positionUnits : null; const weights = imageLayerAxisWeights(local.orientationXyzw, views); const [ox, oy] = publication.viewport.principalOffsetPixels; const perspective = `${transform.focalPixels}px`, perspectiveOrigin = `calc(50% + ${ox}px) calc(50% + ${oy}px)`; for (const bank of banks) { // Every write is on change: this publishes every camera frame (motion-freezes-membership.md). A stack's scenes - // share one camera, so what was last written is kept once for the stack, not read back from each scene. + // take the same camera, so what was last written is kept once for the stack, not read back from each scene. const set = (element: HTMLElement, property: 'opacity' | 'visibility' | 'display', value: string) => { if (element.style[property] !== value) element.style[property] = value; }; - if (bank.perspective !== perspective) { bank.perspective = perspective; for (const { camera } of bank.scenes) camera.style.perspective = perspective; } - if (bank.perspectiveOrigin !== perspectiveOrigin) { bank.perspectiveOrigin = perspectiveOrigin; for (const { camera } of bank.scenes) camera.style.perspectiveOrigin = perspectiveOrigin; } - if (bank.transform !== cssTransform) { bank.transform = cssTransform; for (const { scene } of bank.scenes) scene.style.transform = cssTransform; } + arrange(bank, body !== null); + if (bank.apart) { + // The flat element is a point at the stage's centre: the perspective's origin is measured from there. + const origin = `${ox}px ${oy}px`; + if (bank.perspective !== perspective) { bank.perspective = perspective; bank.apart.style.perspective = perspective; } + if (bank.perspectiveOrigin !== origin) { bank.perspectiveOrigin = origin; bank.apart.style.perspectiveOrigin = origin; } + if (bank.transform !== cssTransform) { bank.transform = cssTransform; for (const texture of bank.textures) texture.element.style.transform = `${cssTransform} ${texture.own}`; } + order(bank, local.positionUnits); + } else { + if (bank.perspective !== perspective) { bank.perspective = perspective; for (const { camera } of bank.runs) camera.style.perspective = perspective; } + if (bank.perspectiveOrigin !== perspectiveOrigin) { bank.perspectiveOrigin = perspectiveOrigin; for (const { camera } of bank.runs) camera.style.perspectiveOrigin = perspectiveOrigin; } + if (bank.transform !== cssTransform) { bank.transform = cssTransform; for (const { scene } of bank.runs) scene.style.transform = cssTransform; } + } const weight = weights[bank.axis]; const returning = weight > 0 && bank.projection.style.display === 'none'; if (weight > 0 && !bank.loaded) { @@ -111,15 +170,18 @@ export function mountPreparedCssImageLayers({ host, before, payload, resolveReso bank.loaded = true; } if (returning) revealLarge(bank); - // Around a body, the sheets the camera stands within one sampling step of are left out (Sheet): an opacity write on - // change only, and every sheet back when the bank is its page's subject again. + // Around a body, the sheets within one sampling step of the camera or of the body are left out (Sheet): an opacity + // write on change only, and every sheet back when the bank is its page's subject again. if (weight > 0 && bank.reach > 0) for (const texture of bank.textures) { - const shown = around && texture.sheet && withinReach(texture.sheet, local.positionUnits, bank.reach) ? '0' : ''; + const shown = body && texture.sheet && (withinReach(texture.sheet, local.positionUnits, bank.reach) || withinReach(texture.sheet, body, bank.reach)) ? '0' : ''; if (texture.shown !== shown) { texture.shown = shown; texture.element.style.opacity = shown; } } - // Never 1 (STACK_OPACITY_CEILING): a drag across M31 had its longest frame at 108 to 111 ms with a bank's opacity - // reaching 1, and 67 to 72 ms under the ceiling (iPad, 2026-10-04). - set(bank.projection, 'opacity', String(Math.min(STACK_OPACITY_CEILING, weight))); + // Never 1 for a stack of sheets (STACK_OPACITY_CEILING): a drag across M31 had its longest frame at 108 to 111 ms + // with a bank's opacity reaching 1, and 67 to 72 ms under the ceiling (iPad, 2026-10-04). A stack of patches is + // the only stack of its bank and keeps the whole weight, so it is drawn at 1: under the ceiling the browser + // composites the stack as one group, and at the Homunculus's nearest view a wedge of that group, a third of the + // picture, was black for one step of a drag, at the same camera places on every pass (headless Chrome, 2026-10-05). + set(bank.projection, 'opacity', String(Math.min(bank.patches ? 1 : STACK_OPACITY_CEILING, weight))); set(bank.projection, 'visibility', weight > 0 ? 'visible' : 'hidden'); // A zero-weight axis contributes nothing; its 3D leaves leave compositing. set(bank.projection, 'display', weight > 0 ? '' : 'none'); diff --git a/packages/renderer/src/navigation/object-orbit.ts b/packages/renderer/src/navigation/object-orbit.ts index 89543843e51..7aedc789688 100644 --- a/packages/renderer/src/navigation/object-orbit.ts +++ b/packages/renderer/src/navigation/object-orbit.ts @@ -300,6 +300,7 @@ export function createRetainedCubicSkyOrbit({ currentResponsiveZoom: () => responsiveFit.zoom, setZoomOutCentering(enabled: boolean) { camera.setZoomOutCentering(enabled); }, setZoomOutOpen(open: boolean) { camera.setZoomOutOpen(open); }, + nearest: () => camera.state.distance <= camera.minimumDistance(), zoomRate: () => controls.zoomRate(), resumeZoom(rate: number) { controls.resumeZoom(rate); }, captureWorldCamera(frame: PreparedWorldCameraFrame): WorldCameraPose { diff --git a/packages/renderer/src/runtime/object-runtime.test.mts b/packages/renderer/src/runtime/object-runtime.test.mts index d7234a8d665..564a3b31f83 100644 --- a/packages/renderer/src/runtime/object-runtime.test.mts +++ b/packages/renderer/src/runtime/object-runtime.test.mts @@ -102,7 +102,7 @@ function harness(options: HarnessOptions = {}, overrides: Partial => ({ ...publication, distance: 12345000, distanceKilometers: 12345, distanceRadii: 12345000, focal: 1000, projectionScale: 1, principalOffset: [0,0], visibleRect: null, offAxisDegrees: null, silhouetteRadius: null, levelOfDetail: { stage: 'geometry', silhouetteDiameter: 400, billboardOpacity: 0, markerOpacity: 0, proxyOpacity: 0 }, pitch: publication.controlPitch, pose: { schema: "cssearth-camera-pose@2" as const, scene: matrix } }); - return { publicationState: () => ({ requestedRevision: 0, presentedRevision: 0, presentedWorld: null }), mobilePageFlow: () => false, initialResponsiveZoom: () => definition.camera.defaultZoom, currentResponsiveZoom: () => definition.camera.defaultZoom, setZoomOutCentering() {}, setZoomOutOpen() {}, zoomRate: () => 0, resumeZoom() {}, captureWorldCamera() { throw new Error("unused"); }, applyWorldCamera() {}, rebaseScene() {}, flyToState: async (...args: Parameters) => { flights.push(args); return { completed: true }; }, turn: async () => ({ completed: true }), invalidate: () => orbitConfiguration.onPublish?.(publication), refresh: () => orbitConfiguration.onPublish?.(publication), setState: (value: Parameters[0]) => { events.push("camera:reset"); Object.assign(publication, value); return state(); }, state, sharedState: () => ({ distanceKilometers: 12345, pose: { schema: "cssearth-camera-pose@2", scene: matrix } }), skyState: () => ({ sunViewDirection: null, sunVisible: false, sunClassification: "absent" }), stats: (): never => { throw new Error("Orbit stats are outside this mount harness."); }, destroy() { events.push("remove:orbit"); } } satisfies Orbit; + return { publicationState: () => ({ requestedRevision: 0, presentedRevision: 0, presentedWorld: null }), mobilePageFlow: () => false, initialResponsiveZoom: () => definition.camera.defaultZoom, currentResponsiveZoom: () => definition.camera.defaultZoom, setZoomOutCentering() {}, setZoomOutOpen() {}, nearest: () => false, zoomRate: () => 0, resumeZoom() {}, captureWorldCamera() { throw new Error("unused"); }, applyWorldCamera() {}, rebaseScene() {}, flyToState: async (...args: Parameters) => { flights.push(args); return { completed: true }; }, turn: async () => ({ completed: true }), invalidate: () => orbitConfiguration.onPublish?.(publication), refresh: () => orbitConfiguration.onPublish?.(publication), setState: (value: Parameters[0]) => { events.push("camera:reset"); Object.assign(publication, value); return state(); }, state, sharedState: () => ({ distanceKilometers: 12345, pose: { schema: "cssearth-camera-pose@2", scene: matrix } }), skyState: () => ({ sunViewDirection: null, sunVisible: false, sunClassification: "absent" }), stats: (): never => { throw new Error("Orbit stats are outside this mount harness."); }, destroy() { events.push("remove:orbit"); } } satisfies Orbit; }, waitDocument: () => Promise.resolve(), waitPaint: () => Promise.resolve(), ...overrides, }); diff --git a/packages/renderer/src/runtime/object-runtime.ts b/packages/renderer/src/runtime/object-runtime.ts index 99e28bced90..112f79444e9 100644 --- a/packages/renderer/src/runtime/object-runtime.ts +++ b/packages/renderer/src/runtime/object-runtime.ts @@ -177,6 +177,7 @@ export function createObjectRuntime(definition: ObjectRuntimeDefinition, service ...(definition.camera.framingScale === undefined ? {} : { framingScale: definition.camera.framingScale }), setZoomOutCentering(enabled: boolean) { if (!lifetime.disposed) getOrbit().setZoomOutCentering(enabled); }, setZoomOutOpen(open: boolean) { if (!lifetime.disposed) getOrbit().setZoomOutOpen(open); }, + nearest: () => !lifetime.disposed && getOrbit().nearest(), zoomRate: () => lifetime.disposed ? 0 : getOrbit().zoomRate(), resumeZoom(rate: number) { if (!lifetime.disposed) getOrbit().resumeZoom(rate); }, holdPresentation() { return getSelection().holdPresentation(); }, diff --git a/packages/renderer/src/runtime/world-navigation-types.ts b/packages/renderer/src/runtime/world-navigation-types.ts index 54c8e1b6208..28efe659347 100644 --- a/packages/renderer/src/runtime/world-navigation-types.ts +++ b/packages/renderer/src/runtime/world-navigation-types.ts @@ -21,6 +21,8 @@ export interface ObjectWorldNavigation { setZoomOutCentering?(enabled: boolean): void; /** A wider scene can take the camera over as it zooms out: this scene's own far limit does not stop the zoom. */ setZoomOutOpen?(open: boolean): void; + /** Whether the camera is as near the body as this scene lets it come: a zoom in has reached the scene's near limit. */ + nearest?(): boolean; /** The signed log rate per millisecond a wheel zoom is moving the camera at now (positive recedes), 0 at rest. */ zoomRate?(): number; /** Take up a zoom carried from the scene before this one: glide on from that rate. */ diff --git a/packages/renderer/src/universe/prepared-universe-runtime.ts b/packages/renderer/src/universe/prepared-universe-runtime.ts index c86859bef6c..e56bc6c76a7 100644 --- a/packages/renderer/src/universe/prepared-universe-runtime.ts +++ b/packages/renderer/src/universe/prepared-universe-runtime.ts @@ -425,7 +425,7 @@ export function createPreparedUniverse({ context, volume, pointAppearance, resol // picture lies on walls (a nebula's central star) stands inside those walls (`selectionHolders`). const insideGalaxy = catalogBanks.imageBankContaining(selected.positionM); catalogBanks.publishImages(world, viewport, volumeOpacity, detailedFocus?.objectId, insideGalaxy === undefined || insideGalaxy === detailedFocus?.objectId ? undefined - : { objectId: insideGalaxy, opacity: logarithmicFade(eyeDistanceM(world.pose, selected.positionM), plan.stars.fadeStartDistanceM, plan.stars.fullDistanceM) }, selectionHolders); + : { objectId: insideGalaxy, opacity: logarithmicFade(eyeDistanceM(world.pose, selected.positionM), plan.stars.fadeStartDistanceM, plan.stars.fullDistanceM) }, selectionHolders, selected.positionM as readonly [number, number, number]); // A bank of plain-dot stars dims like every marker outside a highlighted category and like every body outside // the focus star's system (the frame's `otherSystems`). catalogBanks.publishPoints(world, viewport, companion ?? undefined, selectedSystem, diff --git a/packages/renderer/src/universe/universe-catalog-banks.test.ts b/packages/renderer/src/universe/universe-catalog-banks.test.ts index 0a756bcf2a8..eb7fa3efb13 100644 --- a/packages/renderer/src/universe/universe-catalog-banks.test.ts +++ b/packages/renderer/src/universe/universe-catalog-banks.test.ts @@ -11,7 +11,7 @@ mock.module('../image-layers/prepared-image-layer-runtime.js', { namedExports: { mountPreparedCssImageLayers: ({ host, before }: { host: HTMLElement; before: Element }) => { const root = host.ownerDocument.createElement('div'); host.insertBefore(root, before); - return { root, publish(_publication: unknown, around = false) { drawnAround.push(around); }, revealLarge() {}, destroy() { root.remove(); } }; + return { root, publish(_publication: unknown, around: readonly number[] | false = false) { drawnAround.push(around !== false); }, revealLarge() {}, destroy() { root.remove(); } }; }, } }); // The modules under test import the mocked ones, so they load after the mocks. @@ -53,7 +53,7 @@ test('a bank whose light lies on walls draws around a body inside its host, the loadImageLayer: async id => ({ payload: { id, frame } }) as never }); // The camera stands inside every bank's framing sphere; `within` is what the selected body is inside, by the object tree. const publish = (within: readonly string[]) => banks.publishImages({ referenceFrame: 'fixture', epochJdTt: 1, pose: { positionM: [0, 0, .5], orientationXyzw: [0, 0, 0, 1] } }, - { focalPixels: 1000, principalOffsetPixels: [0, 0], widthPixels: 400, heightPixels: 300 }, 1, undefined, undefined, within); + { focalPixels: 1000, principalOffsetPixels: [0, 0], widthPixels: 400, heightPixels: 300 }, 1, undefined, undefined, within, [0, 0, 0]); const layers = () => [...root.children].filter(node => node.tagName === 'DIV') as HTMLElement[]; publish(['galaxy', 'local-group']); assert.deepEqual([layers().length, root.dataset.imageLayerResidentBankCount ?? '0'], [0, '0'], 'a photograph seen from outside is not drawn around a star inside its galaxy'); @@ -62,7 +62,7 @@ test('a bank whose light lies on walls draws around a body inside its host, the publish(['nebula', 'milky-way']); assert.deepEqual([layers().length, layers()[0]!.style.display, Number(layers()[0]!.style.opacity)], [1, '', 1], 'the first wall bank declared for the host draws whole around the body inside it'); - assert.equal(drawnAround.at(-1), true, 'and is told it is drawn around a body, so the sheets the camera stands on are left out'); + assert.equal(drawnAround.at(-1), true, 'and is told where the body it is drawn around stands, so the sheets through it and under the camera are left out'); publish([]); assert.equal(layers()[0]!.style.display, 'none', 'around a body inside no such host nothing draws'); }); diff --git a/packages/renderer/src/universe/universe-catalog-banks.ts b/packages/renderer/src/universe/universe-catalog-banks.ts index 3de9762c6f3..7ecd7ad3a44 100644 --- a/packages/renderer/src/universe/universe-catalog-banks.ts +++ b/packages/renderer/src/universe/universe-catalog-banks.ts @@ -197,9 +197,10 @@ export function createUniverseCatalogBanks({ root, end, stage, lifetime, declara /** `inside` is the galaxy the selected body is in, and how much of its dots show: a star of M33 stands among M33's * catalogue dots, without the photograph that is the galaxy seen from outside. `within` is the objects the selected body * is inside, by the object tree: a bank whose light lies on walls (`surrounds`) draws around a body inside its host, as - * a nebula's walls around its central star; the first such bank declared for that host stands for it. */ + * a nebula's walls around its central star; the first such bank declared for that host stands for it. `bodyM` is + * that body's place: the sheets through it are left out (prepared-image-layer-runtime.ts). */ publishImages(world: WorldCameraPose, viewport: WorldCameraViewport, volumeOpacity: number, detailedObjectId?: string, - inside?: { readonly objectId: string; readonly opacity: number }, within: readonly string[] = []) { + inside?: { readonly objectId: string; readonly opacity: number }, within: readonly string[] = [], bodyM?: readonly [number, number, number]) { if (lifetime.disposed) return; let around: string | undefined; if (within.length) for (const bank of images) if (bank.surrounds && bank.host !== undefined && within.includes(bank.host)) { around = bank.id; break; } @@ -229,7 +230,7 @@ export function createUniverseCatalogBanks({ root, end, stage, lifetime, declara bank.mounted.root.style.display = opacity > 0 ? '' : 'none'; bank.publishedOpacity = opacity; } - if (opacity > 0) bank.mounted.publish({ world, viewport }, bank.id === around && bank.id !== detailedObjectId); + if (opacity > 0) bank.mounted.publish({ world, viewport }, bank.id === around && bank.id !== detailedObjectId && bodyM !== undefined ? bodyM : false); } }, }; diff --git a/packages/telescope-cli/src/new-object/archives/archives.mts b/packages/telescope-cli/src/new-object/archives/archives.mts index 70be9c70c6b..5dfbcbd23a1 100644 --- a/packages/telescope-cli/src/new-object/archives/archives.mts +++ b/packages/telescope-cli/src/new-object/archives/archives.mts @@ -229,7 +229,7 @@ export function parseArxivEntry(xml: string, arxiv: string, url: string): Public if (!title || !published) throw new TypeError(`arXiv ${arxiv}: the entry has no title or date.`); const creators = [...entry.matchAll(/\s*([\s\S]*?)<\/name>/gu)].map(m => clean(m[1]!)); const doi = field('doi'), journal = field('journal_ref'); - return { id: `arxiv-${arxiv.replace(/\./gu, '-')}`, title: clean(title), creators, year: published.slice(0, 4), url, + return { id: `arxiv-${arxiv.replace(/[./]/gu, '-')}`, title: clean(title), creators, year: published.slice(0, 4), url, ...(journal ? { publisher: clean(journal) } : {}), ...(doi ? { doi: clean(doi) } : {}), arxiv }; } /** A DOI resolved through Crossref. */ @@ -276,7 +276,8 @@ export function fetchPublication(archive: Archive, url: string): Promise { - const arxiv = /arxiv\.org\/abs\/([0-9]{4}\.[0-9]{4,5})/u.exec(url)?.[1]; + // An arXiv number as issued since 2007 (2401.02477) or before it, under its archive (astro-ph/0602464). + const arxiv = /arxiv\.org\/abs\/([0-9]{4}\.[0-9]{4,5}|[a-z-]+(?:\.[A-Z]{2})?\/[0-9]{7})/u.exec(url)?.[1]; if (arxiv) return parseArxivEntry(await archive.text(`https://export.arxiv.org/api/query?id_list=${arxiv}`), arxiv, url); const bibcode = /adsabs\.harvard\.edu\/abs\/([^/?#]+)/u.exec(url)?.[1]; if (bibcode) { diff --git a/packages/telescope-cli/src/new-object/new-object.test.mts b/packages/telescope-cli/src/new-object/new-object.test.mts index f0760e4d90f..715410465f3 100644 --- a/packages/telescope-cli/src/new-object/new-object.test.mts +++ b/packages/telescope-cli/src/new-object/new-object.test.mts @@ -975,3 +975,13 @@ test('a binary whose primary Gaia sees eclipsing on another period is refused; l assert.equal(eclipsingPeriodAgrees(214.3655, 2.998979), true, 'a long orbit Gaia saw twice is not checked'); assert.ok(GAIA_EB_CHECKED_DAYS > 100 && GAIA_EB_CHECKED_DAYS < 110); }); + +test('a paper cited by an arXiv number from before 2007 is read as that paper, not as a web page', async () => { + const { fetchPublication } = await import('./archives/archives.mts'); + const asked: string[] = [], archive = { text: async (url: string) => { asked.push(url); + return 'The Structure of the Homunculus2006-02-21T00:00:00ZNathan Smith'; }, + bytes: async () => Buffer.alloc(0), exists: async () => true }; + const old = await fetchPublication(archive, 'https://arxiv.org/abs/astro-ph/0602464'); + assert.deepEqual([old?.id, old?.arxiv, old?.year, asked[0]], ['arxiv-astro-ph-0602464', 'astro-ph/0602464', '2006', 'https://export.arxiv.org/api/query?id_list=astro-ph/0602464']); + assert.equal((await fetchPublication(archive, 'https://arxiv.org/abs/2401.02477'))?.id, 'arxiv-2401-02477'); +}); diff --git a/packages/telescope-cli/src/new-object/star-limb.mts b/packages/telescope-cli/src/new-object/star-limb.mts index b7191b09729..1ecbc94bfc3 100644 --- a/packages/telescope-cli/src/new-object/star-limb.mts +++ b/packages/telescope-cli/src/new-object/star-limb.mts @@ -2,7 +2,8 @@ * * The law comes from the first source that holds the star: * 1. a law transcribed from a paper beside the star (`source/photometry/-limb-darkening.json`, cssearth-published-limb-darkening@1, - * quadratic or power), a measurement or the model a paper fixed for this star; + * quadratic or power), a measurement or the model a paper fixed for this star. A power law that follows from sizes a paper + * prints, not from an exponent it prints, says how in `derived`, and the star's texts say it in those words; * 2. the model grids of limb.mts, at the star's temperature and gravity (and mass, for spherical models). The gravity is the star's * mass and radius in its astronomy record, else a published spectroscopic value (gravity.mts), searched at the star's J2000 * position: the record's, carried back from its own epoch by its proper motion. @@ -72,6 +73,7 @@ async function publishedLaw(root: string, id: string): Promise Promise(); +export const knownInner = (id: string): NavigableObject | undefined => { const body = inner.get(id); return body === undefined ? undefined : knownObject(body); }; +/** Loads the body the walls of `id` surround, when its entry names one: a zoom in on `id` hands the view to it + * (overview-selection.mts). Null for an object that names none. */ +export async function loadInner(id: string, read: (id: string) => Promise = fetchEntry): Promise { + const entry = await read(id), named: unknown = entry && typeof entry === 'object' && 'inner' in entry ? entry.inner : undefined; + if (named === undefined) return null; + if (typeof named !== 'string') throw new TypeError(`/objects/${id}/entry.json: inner must be an object id; got ${JSON.stringify(named)}.`); + const body = await loadObject(named, read); + if (body) inner.set(id, named); + return body; +} + /** A prepared catalogue entry as the directory serves it (`@cssearth/objects` catalogueObject), bound to the shell's scene loader. */ export function objectFromEntry(value: unknown): NavigableObject { return catalogueObject(value, descriptor => async (signal?: AbortSignal) => { diff --git a/site/overview-selection.characterization.test.mts b/site/overview-selection.characterization.test.mts index 23bba7e2457..88e997427f2 100644 --- a/site/overview-selection.characterization.test.mts +++ b/site/overview-selection.characterization.test.mts @@ -40,3 +40,29 @@ test('unavailable camera publications preserve a crossing until an available ret dispose(); assert.equal(unsubscribed, 1); dispose.refresh(); assert.equal(changes.length, 1); }); + +test('a zoom in that reaches the near limit goes on into the body the walls surround', () => { + // A nebula of radius 1 with nothing it is inside but the Sun's world, opened from 3 radii out. + const nebula = frame(1000 * au), around = [...objects, objectFixture('nebula', nebula, { classification: 'nebula', systemName: 'Nebula' })]; + const from = (range: number) => worldCameraFromCenteredPresentation({ rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1], distanceUnits: range }, nebula, viewport); + let listener: ObjectWorldNavigationListener | null = null, nearest = false, returns = 0; + const timers = new Map void>(), changes: unknown[] = []; + const watch = (inner: string | null) => watchCameraSelection({ objects: around, systems: around, objectId: 'nebula', getSelection: () => ({ objectId: 'nebula' }), + isAvailable: () => true, inner: () => inner, onChange: next => changes.push(next), onReturn: () => { returns++; }, + navigation: { ...navigationFixture(nebula, () => from(3), () => ({ ...viewport, framingRadiusPixels: 1, detailHandoffDiameterPixels: 1, visibleRect: null })), nearest: () => nearest, subscribe(value) { listener = value; return () => {}; } }, + windowTarget: { setTimeout(callback: () => void) { timers.set(1, callback); return 1; }, clearTimeout(id: number) { timers.delete(id); } } as unknown as Window }); + const publish = (range: number) => required(listener)(from(range), viewport); + const settle = () => { required(timers.get(1))(); timers.clear(); }; + + watch('star'); + publish(3); publish(2); assert.equal(timers.size, 0); + nearest = true; publish(1.2); settle(); + assert.deepEqual(changes, [{ objectId: 'star' }]); + nearest = false; publish(1.5); assert.equal(returns, 1); + + // A page that came to rest on its nearest view stays the nebula's, and so does a nebula with no body inside. + changes.length = 0; nearest = true; + watch('star'); publish(1.2); publish(1.2); assert.equal(timers.size, 0); + watch(null); publish(3); publish(1.2); assert.equal(timers.size, 0); + assert.deepEqual(changes, []); +}); diff --git a/site/overview-selection.mts b/site/overview-selection.mts index 125f6f63f6e..a5a06658d97 100644 --- a/site/overview-selection.mts +++ b/site/overview-selection.mts @@ -82,7 +82,8 @@ export function selectionAtCamera({ world, viewport, objects, systems, objectId, /** The selection the camera frames from the scene of body `objectId` when it is not the committed one, farthest first: * out of the body, its star's system or the object it is inside (selectionAtCamera); short of that, its planet's * system or its own moons' (satelliteSelectionAtCamera); and back in, the body. Null while the camera frames - * `selection` still. A system is named by its own object id. */ + * `selection` still. A system is named by its own object id. A zoom in past the scene's near limit is the watcher's + * (watchCameraSelection `inward`). */ export function framedAtCamera({ selection, optics, ...facts }: Omit[0], 'overview'> & { /** The committed selection of the mounted scene: the body, or its system. */ selection: SceneSubject; @@ -98,13 +99,19 @@ export function framedAtCamera({ selection, optics, ...facts }: Omit[]; systems: SystemObjects; objectId: string; + onChange, onReturn, windowTarget, inside, inner }: { navigation: ObjectWorldNavigation; objects: readonly Pick[]; systems: SystemObjects; objectId: string; /** The committed selection of the mounted scene. */ getSelection(): SceneSubject; isAvailable(): boolean; /** The object the body is inside, when it has a scene of its own; asked on each camera, as its entry may be read late. */ inside?(): InsideBody | null; + /** The body the mounted object's walls surround, once the page has read it (object-directory.mts `knownInner`). */ + inner?(): string | null; /** `landed`: a flight to a framing has just landed here, so the camera is at rest. */ onChange(selection: SceneSubject, landed: boolean): void; /** The camera frames the committed selection again after a crossing was reported. */ @@ -122,8 +129,16 @@ export function watchCameraSelection({ navigation, objects, systems, objectId, g }; // What every reading of the camera is asked with. const facts = (publication: SelectionPublication) => ({ ...publication, objects, systems, objectId, inside: inside?.() ?? null, restRangeM: restRangeM ?? 0 }); + // The zoom in has reached the scene's near limit, on the body's own selection. Not where the scene came to rest: a page + // opened on its nearest view stays the object's, and a camera handed in from the body it surrounds is on its way out. + const inward = (publication: SelectionPublication): SceneSubject | null => { + const body = inner?.() ?? null; + return body !== null && getSelection().objectId === objectId && navigation.nearest?.() === true + && restRangeM !== null && range(publication.world) < restRangeM ? { objectId: body } : null; + }; const framed = (publication: SelectionPublication, landed = false) => - framedAtCamera({ ...facts(publication), selection: getSelection(), optics: navigation.optics?.() ?? null, landed }); + framedAtCamera({ ...facts(publication), selection: getSelection(), optics: navigation.optics?.() ?? null, landed }) + ?? (landed ? null : inward(publication)); /** Whether the camera is clearly past an exit out of the body: no wait is needed to know it has left. */ const clearlyOut = (publication: SelectionPublication) => zoomStepOf(getSelection()) === null && selectionAtCamera({ ...facts(publication), overview: false, exitScale: policy.clearExitScale }) !== null; diff --git a/site/pages/objects/[id]/entry.json.ts b/site/pages/objects/[id]/entry.json.ts index 0206a0ee0ba..fe1f034bea0 100644 --- a/site/pages/objects/[id]/entry.json.ts +++ b/site/pages/objects/[id]/entry.json.ts @@ -2,9 +2,13 @@ import type { APIRoute, GetStaticPaths } from 'astro'; import { OBJECT_ENTRY_IDS, objectEntry } from '../../../object-entry.mts'; import { resolveSceneAddressesDeep } from '../../../asset-origin.mts'; import { worldPlaceOf } from '../../../world-places.mts'; -import { ancestorsOf } from '../../../objects.mts'; +import { OBJECTS, ancestorsOf } from '../../../objects.mts'; import { APPLICATION_WORLD_CONTEXT } from '../../../world-context-plan.mts'; import { hostedBanksOf } from '../../../hosted-banks.mts'; +import { surroundedBody, surroundingHosts } from '../../../surrounded-body.mts'; +import bankAssets from '../../../prepared-context-bank-assets.json' with { type: 'json' }; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; // One navigable object's prepared entry, read by the page's object directory (site/object-directory.mts) the first time // it needs that object. @@ -12,6 +16,12 @@ export const getStaticPaths: GetStaticPaths = async () => OBJECT_ENTRY_IDS.map(i // The body each world body orbits, read once: the build reads the whole world (world-context-plan.mts). let centres: ReadonlyMap | null = null; +// The objects whose picture lies on walls around their middle, read once: each context bank's descriptor says it +// (`properties.surrounds`). The banks are the ones the catalogue prepared; their descriptors are read from the tree, as +// the page data is (object-page-data.mts), because the module that holds them is the browser's. +let surrounding: Promise> | null = null; +const readSurrounding = async (root = process.cwd()) => surroundingHosts(Object.fromEntries(await Promise.all(Object.keys(bankAssets).map(async id => + [id, JSON.parse(await readFile(resolve(root, 'src/objects', id, 'object.json'), 'utf8')) as unknown] as const)))); export const GET: APIRoute = async ({ params }) => { const found = params.id ? objectEntry(params.id) : null; if (!found) return new Response('Not found', { status: 404 }); @@ -24,7 +34,9 @@ export const GET: APIRoute = async ({ params }) => { // The banks the world draws only for this object (site/hosted-banks.mts): a page learns them with the object, never in its code. centres ??= new Map(APPLICATION_WORLD_CONTEXT.bodies.flatMap(body => body.orbit ? [[body.id, body.orbit.centerBodyId] as const] : [])); const banks = hostedBanksOf(params.id!, centres.get(params.id!)); - const entry = { ...(found as Record), ...(ancestors.length ? { ancestors } : {}), ...(world ? { world } : {}), ...(banks.length ? { banks } : {}) }; + // The body its walls surround, when its picture lies on walls: a zoom in on it leads to that body (object-directory.mts `loadInner`). + const inner = surroundedBody(OBJECTS, await (surrounding ??= readSurrounding()), params.id!); + const entry = { ...(found as Record), ...(ancestors.length ? { ancestors } : {}), ...(inner ? { inner } : {}), ...(world ? { world } : {}), ...(banks.length ? { banks } : {}) }; // A flight covers its arrival with the entry's photograph, read as written: it carries its published address. return new Response(JSON.stringify(await resolveSceneAddressesDeep(entry)), { headers: { 'Content-Type': 'application/json; charset=utf-8', diff --git a/site/scene/scene-router.mts b/site/scene/scene-router.mts index 8a4bf85b5ad..da3f841daee 100644 --- a/site/scene/scene-router.mts +++ b/site/scene/scene-router.mts @@ -30,7 +30,7 @@ import { createCameraMotion } from '@cssearth/renderer/navigation'; import { WORLD_HOST_ID, namesSystem } from '../navigation/navigation-scope.mts'; import { systemHostId } from '../navigation/system-address.mts'; import { insideBody, pastCentreGalaxy, setZoomCentre, zoomStepOf } from '../inside-view.mts'; -import { loadAncestors, loadHolder } from '../object-directory.mts'; +import { knownInner, loadAncestors, loadHolder, loadInner } from '../object-directory.mts'; import { bodyInView, createCameraHandover } from './camera-handover.mts'; import { OVERVIEW_SELECTION_POLICY } from '../runtime-policy.mts'; import { createNavigationTiming } from '../navigation/navigation-timing.mts'; @@ -754,8 +754,8 @@ export function createSceneRouter({ .catch((error: unknown) => { if (!session.signal.aborted) reportError(error); }); } /** One watcher follows the camera on a body's scene: out to its own system, its planet's or its star's, out to the - * object it is inside, and back onto the body. The scene of an object seen from inside is left by zooming - * (followSelectionCamera), not by this watcher. */ + * object it is inside, back onto the body, and in to the body its walls surround. The scene of an object seen from + * inside is left by zooming (followSelectionCamera), not by this watcher. */ function connectCameraSelection(ready: RouterContext, session: Session) { const owner = session.mount?.navigation; if (!owner) return; @@ -763,8 +763,11 @@ export function createSceneRouter({ // The object the mounted body is inside is read now, when the page has not read it (another galaxy): it takes the view // as the camera backs out of the body. void loadHolder(objectId).catch(error => console.error(`The object ${objectId} is inside could not be read; zooming out of it skips it.`, error)); + // The body its walls surround is read too, when its entry names one: it takes the view as the camera zooms in on it. + void loadInner(objectId).catch(error => console.error(`The body inside ${objectId} could not be read; zooming in on it stops at its nearest view.`, error)); const watch = registry.watchCameraSelection({ navigation: owner, objects: registry.SCENE_OBJECTS, systems: objects, objectId, inside: () => { const frame = insideBody(objectId)?.worldFrame, id = insideBody(objectId)?.id; return frame && id !== undefined ? { id, originM: frame.originM, radiusM: frame.bodyRadiusM } : null; }, + inner: () => knownInner(objectId)?.id ?? null, getSelection: () => current.context, // The pending flight owns the camera; repeat-click bookkeeping must not // suppress zoom-out deselection after that flight has finished. diff --git a/site/surrounded-body.mts b/site/surrounded-body.mts new file mode 100644 index 00000000000..8dc79f8b9a4 --- /dev/null +++ b/site/surrounded-body.mts @@ -0,0 +1,28 @@ +import { isRecord } from '@cssearth/core'; + +/** What the rule reads of an object: its place in the object tree and in the world. */ +interface Placed { readonly id: string; readonly parent?: string; readonly system?: { readonly host: string }; readonly worldFrame: { readonly originM: readonly number[] } } + +/** The objects whose picture lies on walls around their middle: the hosts of the image banks that say so + * (`properties.surrounds`, packages/objects/src/image-layer-bank.ts), among the world's context descriptors. */ +export function surroundingHosts(descriptors: Readonly>): ReadonlySet { + return new Set(Object.values(descriptors).flatMap(descriptor => + isRecord(descriptor) && isRecord(descriptor.properties) && descriptor.properties.surrounds === true && typeof descriptor.properties.host === 'string' + ? [descriptor.properties.host] : [])); +} + +/** + * The body the walls of object `id` surround: the object inside it, by the object tree, that stands nearest its centre, as + * a planetary nebula's central star. A system inside it stands for its host. Undefined for an object whose picture is not on + * walls around its middle (a galaxy's photograph), and for one with nothing inside it. The object's entry names it + * (`inner`), and a zoom in on the object leads to it (overview-selection.mts). + */ +export function surroundedBody(objects: readonly Placed[], surrounding: ReadonlySet, id: string): string | undefined { + const holder = objects.find(object => object.id === id); + if (!holder || !surrounding.has(id)) return undefined; + const from = holder.worldFrame.originM; + return objects.filter(object => object.parent === id) + .map(object => object.system ? objects.find(host => host.id === object.system!.host) ?? object : object) + .map(object => ({ id: object.id, offsetM: Math.hypot(...object.worldFrame.originM.map((value, axis) => value - from[axis]!)) })) + .sort((a, b) => a.offsetM - b.offsetM || a.id.localeCompare(b.id))[0]?.id; +} diff --git a/site/surrounded-body.test.mts b/site/surrounded-body.test.mts new file mode 100644 index 00000000000..2447733dba3 --- /dev/null +++ b/site/surrounded-body.test.mts @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { surroundedBody, surroundingHosts } from './surrounded-body.mts'; + +const at = (x: number) => ({ worldFrame: { originM: [x, 0, 0] } }); +const objects = [{ id: 'nebula', ...at(100) }, { id: 'star', parent: 'nebula', ...at(101) }, { id: 'far-star', parent: 'nebula', ...at(140) }, + { id: 'galaxy', ...at(500) }, { id: 'galaxy-star', parent: 'galaxy', ...at(500) }, + { id: 'shell', ...at(900) }, { id: 'pair-system', parent: 'shell', system: { host: 'pair' }, ...at(900) }, { id: 'pair', parent: 'pair-system', ...at(902) }, + { id: 'empty', ...at(1200) }]; +const surrounding = surroundingHosts({ + 'nebula-layers': { id: 'nebula-layers', properties: { host: 'nebula', surrounds: true } }, + 'galaxy-layers': { id: 'galaxy-layers', properties: { host: 'galaxy' } }, + 'shell-layers': { id: 'shell-layers', properties: { host: 'shell', surrounds: true } }, + 'empty-layers': { id: 'empty-layers', properties: { host: 'empty', surrounds: true } }, +}); + +test('the body an object\'s walls surround is the object inside it nearest its centre', () => { + assert.equal(surroundedBody(objects, surrounding, 'nebula'), 'star'); + assert.equal(surroundedBody(objects, surrounding, 'shell'), 'pair', 'a system inside it stands for its host'); +}); + +test('an object whose picture is not on walls, or with nothing inside it, names none', () => { + assert.equal(surroundedBody(objects, surrounding, 'galaxy'), undefined); + assert.equal(surroundedBody(objects, surrounding, 'empty'), undefined); + assert.equal(surroundedBody(objects, surrounding, 'star'), undefined); + assert.equal(surroundedBody(objects, surrounding, 'unknown'), undefined); +}); diff --git a/src/objects/eta-carinae/NOTICE.md b/src/objects/eta-carinae/NOTICE.md new file mode 100644 index 00000000000..d1e95857a26 --- /dev/null +++ b/src/objects/eta-carinae/NOTICE.md @@ -0,0 +1,9 @@ +# Eta Carinae credits + +Radius, mass and temperature: Radius 1301 solar radii from Weigelt et al. (2007), arXiv:astro-ph/0609715, Table 2: the wind's K-band continuum light (2.174 µm, VLTI/AMBER) falls to a tenth of its central brightness at a diameter of 5.15 mas (±4%); half of that at 2,350 pc is 6.05 AU, 1,301 solar radii (https://arxiv.org/abs/astro-ph/0609715); No mass is measured, so GM is 0, the records' unpublished value; temperature from Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3). + +Color: a Planck spectrum at the temperature of Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3), through the CIE 1931 2° color-matching functions (CIE 2019, CC BY-SA 4.0, doi:10.25039/CIE.DS.xvudnb9b). + +Placement: Gaia DR3 source 5350358584482202880: position; distance: Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at. This work has made use of data from the European Space Agency (ESA) mission Gaia, processed by the Gaia Data Processing and Analysis Consortium (DPAC). Identifiers: SIMBAD, CDS, Strasbourg. + +Limb darkening: Weigelt et al. (2007), arXiv:astro-ph/0609715. diff --git a/src/objects/eta-carinae/README.md b/src/objects/eta-carinae/README.md new file mode 100644 index 00000000000..c94380895bb --- /dev/null +++ b/src/objects/eta-carinae/README.md @@ -0,0 +1,33 @@ +# Eta Carinae + +## Sources + +Eta Carinae (HD 93308, HR 4210) is 2350.0 parsecs away. It is the star inside the [Homunculus Nebula](../homunculus-nebula/README.md), the cloud of dust around it: a luminous blue variable (SIMBAD's class for it) so wrapped in its own wind that no surface is seen. It is an object of its own, inside the nebula, so the star can be opened from the nebula's page, where the nebula's surface stays drawn around it. What is drawn is the wind, as the VLTI measured its near-infrared light (Weigelt et al. 2007, Table 2): a sphere out to where that light is a tenth as bright as at the centre, dimmed toward its edge so that it is half as bright at the measured half-brightness size, in the color of a black body at the temperature where the wind turns opaque (Groh et al. 2012, Table 2). + +**Star.** Placement: Gaia DR3 source 5350358584482202880, distance 2,350 pc from Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at; Gaia DR3 gives it no parallax. Radius 1301 solar radii from Weigelt et al. (2007), arXiv:astro-ph/0609715, Table 2: the wind's K-band continuum light (2.174 µm, VLTI/AMBER) falls to a tenth of its central brightness at a diameter of 5.15 mas (±4%); half of that at 2,350 pc is 6.05 AU, 1,301 solar radii (https://arxiv.org/abs/astro-ph/0609715). No mass is measured, so GM is 0, the records' unpublished value. Temperature 9,400 K from Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3). No surface gravity of this star is published. + +**Color.** A Planck spectrum at 9,400 K, because no archive holds a spectrum of this star (stis-ngsl: HD 93308 is not in the library; gaia-xp: Gaia DR3 published no sampled BP/RP spectrum of it; pulkovo: HR 4210 is not in the catalogue; kiehling: HR 4210 is not among its 60 stars; kharitonov: HR 4210 is not in the catalogue; burnashev: BS 4210 is not in part2), through the CIE 1931 2° observer: #d2ddff. Routes tried in order: stis-ngsl: HD 93308 is not in the library; gaia-xp: Gaia DR3 published no sampled BP/RP spectrum of it; pulkovo: HR 4210 is not in the catalogue; kiehling: HR 4210 is not among its 60 stars; kharitonov: HR 4210 is not in the catalogue; burnashev: BS 4210 is not in part2; planck: used. + +**Limb.** The disc is dimmed toward the limb by the power law I(mu) = mu^6.05 whose brightness is half the centre's at the diameter Weigelt et al. (2007), arXiv:astro-ph/0609715 measured for it, 2.33 mas, on a disc that ends at their tenth-brightness diameter, 5.15 mas (VLTI/AMBER K-band continuum at 2.174 um; not a visible band). + +## Evidence + +Generated 2026-10-05 by [new-object-cli.mts](../../../packages/telescope-cli/src/new-object/new-object-cli.mts) from Gaia DR3, SIMBAD and the archives named above; each choice was read with the dataset's own reader. + +![The star's page inside the Homunculus at four distances, and a zoom in on the nebula's page with the wheel alone](evidence/2026-10-05/pages.jpg) + +Headless Chromium at 1440 × 900, device pixel ratio 2, on 2026-10-05, no page errors. Top row: the star's own page as it opens, 42 AU from the star, its disc dimmed toward the edge; then 4, 8 and 12 wheel steps out, at 333, 2,388 and 16,890 AU, with the nebula's surface ([Homunculus Nebula image layers](../homunculus-nebula-layers/README.md)) drawn around it without the sheet through the star. Bottom row: the nebula's page, then a zoom in with the wheel alone, no click: 4 steps in, the view has handed over to the star, 42,973 AU from it; 8 and 13 steps in, 6,083 and 523 AU. + +## Known problems + +- **Assumptions of the frame.** The axis's position angle and the rotation phase are conventions. +- **Not shown.** What is drawn is the star's wind, not its surface. The star under the wind is modelled at 60 solar radii and 35,200 K, and models from 60 to 480 solar radii fit its spectrum equally (Groh et al. 2012, Table 2 and Sect. 3, https://arxiv.org/abs/1204.1963). +- **Not shown.** The near-infrared light is not round: VLTI measurements find it elongated, which fast rotation or the companion's cavity in the wind both explain (Groh et al. 2010, https://arxiv.org/abs/1006.4816). It is drawn as a sphere. +- **Not shown.** Half of the wind's near-infrared light comes from outside a diameter of 4.23 mas, in a faint halo that reaches past the sphere drawn (Weigelt et al. 2007, Table 2, https://arxiv.org/abs/astro-ph/0609715). The halo is not drawn. +- **The limb is a fit to two sizes.** The paper prints no exponent: the power law is the one that is half as bright at the printed half-brightness diameter on a disc ending at the printed tenth-brightness diameter. It is dark at the edge, where the measured light is a tenth of the centre's, and reaches a tenth at 0.73 of the radius. +- **Not shown.** It is a binary with a period of 5.54 years. The companion is not seen directly, and the orbit's tilt and direction come from models of the colliding winds (Madura et al. 2012, as adopted by Groh et al. 2012, Table 2: inclination 138 degrees, longitude of periastron 260 degrees). The companion is not drawn. +- **Not shown.** The star's mass is not measured. +- **A model color.** The color is a black body at 9,400 K. No spectrum of the star alone is used: its light reaches us through its own dust, and the Homunculus reflects most of it. +- **Measured limb, other band.** The law was measured or fixed outside the visible band the color is drawn in; the visible limb is not measured. + +[Investigation ledger](investigations.json) · [Inputs](source/manifest.json) · [Preparation](source/preparation) · [Delivered files](inventory.json) · [Credits](NOTICE.md) diff --git a/src/objects/eta-carinae/evidence/2026-10-05/pages.jpg b/src/objects/eta-carinae/evidence/2026-10-05/pages.jpg new file mode 100644 index 00000000000..19456000114 Binary files /dev/null and b/src/objects/eta-carinae/evidence/2026-10-05/pages.jpg differ diff --git a/src/objects/eta-carinae/inventory.json b/src/objects/eta-carinae/inventory.json new file mode 100644 index 00000000000..02528ca6fc2 --- /dev/null +++ b/src/objects/eta-carinae/inventory.json @@ -0,0 +1,125 @@ +{ + "schema": "cssearth-inventory@1", + "assets": [ + { + "location": "public", + "filename": "eta-carinae-arrival.webp", + "bytes": 16936, + "sha256": "2b7097b211870203580d9d25da539a6bc2839109f82cb9f5858973c0e214cd59" + }, + { + "location": "public", + "filename": "eta-carinae-billboard.webp", + "bytes": 3208, + "sha256": "575ce803f86970d53274d1d398c54abeb169a02e206cd0eb866cb3376378c8f5" + }, + { + "location": "public", + "filename": "eta-carinae-dataset-color.webp", + "bytes": 44, + "sha256": "ff05f6f8255f5de0240845390db2033983ac2b79985531f02c81472cfa6585e9" + }, + { + "location": "public", + "filename": "eta-carinae-limb-color@2x.webp", + "bytes": 100778, + "sha256": "4ec64c990948c8425123d513c909e2e414496b1852b1e511ce6dc2936f9cc460" + }, + { + "location": "public", + "filename": "eta-carinae-poles-color@2x.webp", + "bytes": 1340, + "sha256": "1bef74c0484ef1af5ef9275e4f1a91e93f8dd3e2615139a606d32e102d8f64e7" + }, + { + "location": "public", + "filename": "eta-carinae-surface-color@2x.webp", + "bytes": 38, + "sha256": "88dfd546381a2486b05f912db0af361be5e72462e859b0fb071d2f6f6b513e24" + }, + { + "location": "prepared", + "filename": "arrival-billboard.json", + "bytes": 289, + "sha256": "35a33a77a5aefc82590670e5d1e53cd4044e05e615d686ac61f5c7b482d8e0ca" + }, + { + "location": "prepared", + "filename": "assets.json", + "bytes": 1945, + "sha256": "014dc3d78a194c464b8566364ecb0d846fdd71f38a70048b4feeb0d266b7630f" + }, + { + "location": "prepared", + "filename": "authored-preparation.json", + "bytes": 664, + "sha256": "bc3f56ebec96a1a86fc900ad4aaf3b9ff6fd1de657d2c1fa493330529f6c86c3" + }, + { + "location": "prepared", + "filename": "content.json", + "bytes": 1310, + "sha256": "28e67b25eb1b551dc938068c681e12189cbdf41079ae2782a392d3cab62ff104" + }, + { + "location": "prepared", + "filename": "controls.json", + "bytes": 372, + "sha256": "1d4fb8c05c8723cd2061fb8048583d47412514911d82580cb91660b5c791cb3a" + }, + { + "location": "prepared", + "filename": "datasets.json", + "bytes": 1009, + "sha256": "d6790864f8a0fae8b9e3212203e7e948689f4dadfb8cdead8ce8c137358a82b6" + }, + { + "location": "prepared", + "filename": "minimaps.json", + "bytes": 101, + "sha256": "087c2291c1fc076eb71a6d0d7ff5d5b77074bd9ac9b4e63929c77733a8528617" + }, + { + "location": "prepared", + "filename": "minimaps/color.webp", + "bytes": 442, + "sha256": "08d7dab0a0d9d9d167a5b6105b7c4474bbdd27440159c01387b51e8f262ca5fd" + }, + { + "location": "prepared", + "filename": "runtime.json", + "bytes": 249402, + "sha256": "53b99da1b73104823960d37e2b2aa7b29a35120df335340e84b5af45b08dde2a" + }, + { + "location": "prepared", + "filename": "scene.json", + "bytes": 394569, + "sha256": "602165c5c988dfbcaabcba3892cb832fe178a0a0f076c501cee3ca37f7c6f14c" + }, + { + "location": "prepared", + "filename": "sky.json", + "bytes": 1185, + "sha256": "e05041f246db1bbd7e97a3242ddaf827643cfffcd8094f869239cfaa2bf53e84" + }, + { + "location": "prepared", + "filename": "sun.json", + "bytes": 5, + "sha256": "38e0b9de817f645c4bec37c0d4a3e58baecccb040f5718dc069a72c7385a0bed" + }, + { + "location": "prepared", + "filename": "text.json", + "bytes": 1400, + "sha256": "900a92d2fa497580da76cb6c992bc1bfd6291a23192243d3111dd6ab57f36bbe" + }, + { + "location": "prepared", + "filename": "world-navigation.json", + "bytes": 1098, + "sha256": "489a842c2cdeec7be60f2324c0ca493bac26589efd87e0585b1afef820d26da7" + } + ] +} diff --git a/src/objects/eta-carinae/investigations.json b/src/objects/eta-carinae/investigations.json new file mode 100644 index 00000000000..855dbc81928 --- /dev/null +++ b/src/objects/eta-carinae/investigations.json @@ -0,0 +1,45 @@ +{ + "schema": "cssearth-investigation-ledger@1", + "objectId": "eta-carinae", + "entries": [ + { + "id": "placement", + "subject": "Placement", + "status": "included", + "finding": "Gaia DR3 source 5350358584482202880: position at J2016.0. Distance: Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at (https://arxiv.org/abs/astro-ph/0602464): 2350 pc; Gaia DR3 gives this source no parallax (a two-parameter solution). Proper motion: Gaia DR3 source 5350358584482202880 is a two-parameter solution with no proper motion; zero is assumed (at 2,350 pc, 1 mas/yr would be 11 km/s across the sky). Radial velocity from SIMBAD's radial velocity for Gaia DR3 5350358584482202880 (quality E), from 1953GCRV..C......0W; Gaia DR3 measures none.", + "evidence": [ + "https://gea.esac.esa.int/archive/", + "https://arxiv.org/abs/astro-ph/0602464", + "https://ui.adsabs.harvard.edu/abs/1953GCRV..C......0W/abstract" + ] + }, + { + "id": "radius-mass-and-temperature", + "subject": "Radius, mass and temperature", + "status": "included", + "finding": "Radius 1032 solar radii from Groh et al. (2010), arXiv:1006.4816, Sect. 4: most of the star's K-band light comes from a region with a 50% encircled-energy radius of 4.8 AU, measured with VLTI/AMBER by Weigelt et al. (2007); 4.8 AU is 1,032 solar radii (https://arxiv.org/abs/1006.4816). No mass is measured, so GM is 0, the records' unpublished value. Temperature 9400 K from Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3).", + "evidence": [ + "https://arxiv.org/abs/1204.1963", + "https://arxiv.org/abs/1006.4816" + ] + }, + { + "id": "color", + "subject": "Color", + "status": "included", + "finding": "A Planck spectrum at 9,400 K, because no archive holds a spectrum of this star (stis-ngsl: HD 93308 is not in the library; gaia-xp: Gaia DR3 published no sampled BP/RP spectrum of it; pulkovo: HR 4210 is not in the catalogue; kiehling: HR 4210 is not among its 60 stars; kharitonov: HR 4210 is not in the catalogue; burnashev: BS 4210 is not in part2), through the CIE 1931 2-degree observer: #d2ddff. Routes tried in order: stis-ngsl: HD 93308 is not in the library; gaia-xp: Gaia DR3 published no sampled BP/RP spectrum of it; pulkovo: HR 4210 is not in the catalogue; kiehling: HR 4210 is not among its 60 stars; kharitonov: HR 4210 is not in the catalogue; burnashev: BS 4210 is not in part2; planck: used.", + "evidence": [ + "https://arxiv.org/abs/1204.1963" + ] + }, + { + "id": "limb-darkening", + "subject": "Limb darkening", + "status": "included", + "finding": "The disc is dimmed toward the limb by the power law I(mu) = mu^6.05 whose brightness is half the centre's at the diameter Weigelt et al. (2007), arXiv:astro-ph/0609715 measured for it, 2.33 mas, on a disc that ends at their tenth-brightness diameter, 5.15 mas (VLTI/AMBER K-band continuum at 2.174 um; not a visible band).", + "evidence": [ + "https://arxiv.org/abs/astro-ph/0609715" + ] + } + ] +} diff --git a/src/objects/eta-carinae/object.json b/src/objects/eta-carinae/object.json new file mode 100644 index 00000000000..3d9f1a1266c --- /dev/null +++ b/src/objects/eta-carinae/object.json @@ -0,0 +1,146 @@ +{ + "schema": "cssearth-object@2", + "id": "eta-carinae", + "parent": "homunculus-nebula", + "type": "layered-body", + "properties": { + "preparation": { + "schema": "cssearth-object-preparation@1", + "label": "Eta Carinae", + "steps": [ + "verify-sources", + "assets", + "panel-content", + "datasets", + "starfield", + "scene", + "controls", + "presentation", + "runtime-assets" + ] + }, + "recipe": { + "schema": "cssearth-authored-object@2", + "surfaces": [ + { + "id": "body", + "source": "geometry", + "projection": "equirectangular", + "datasets": [ + { + "id": "color", + "source": "content", + "material": "emission" + } + ] + } + ], + "shape": { + "kind": "sphere", + "radiusKm": 905105700 + }, + "materials": [ + { + "id": "emission", + "source": "raster", + "model": "emissive" + } + ], + "sources": [ + { + "id": "raster", + "path": "source/preparation/raster.json" + }, + { + "id": "geometry", + "path": "source/preparation/geometry.json" + }, + { + "id": "presentation", + "path": "source/preparation/presentation.json" + }, + { + "id": "content", + "path": "source/content/object.json" + }, + { + "id": "solar-system", + "path": "source/presentation/solar-system.json" + }, + { + "id": "rotation", + "path": "source/preparation/rotation.json" + }, + { + "id": "navigation", + "path": "source/preparation/navigation.json" + }, + { + "id": "acquisition", + "path": "source/preparation/acquisition.json" + } + ], + "emission": { + "source": "raster", + "material": "emission" + } + }, + "page": { + "stylesheets": [ + "src/renderers/css/styles/body-surfaces.css", + "src/renderers/css/styles/eta-carinae-surfaces.css" + ], + "metadata": { + "url": "prepared/page.json" + } + }, + "catalog": { + "name": "Eta Carinae", + "classification": "star", + "color": "#d2ddff", + "distanceAu": 484722238.4, + "description": "Eta Carinae is the star inside the Homunculus Nebula, about 2,350 parsecs away.", + "systemName": "Homunculus Nebula", + "aliases": [ + "HD 93308", + "HR 4210" + ], + "order": 5777, + "context": { + "order": 5774 + }, + "featured": true + }, + "worldFrame": { + "referenceFrame": "sun-icrf", + "epochJdTt": 2461286.5, + "originM": [ + -34662563854399783000, + 11756410400574024000, + -62597833826125234000 + ], + "presentationToReference": [ + -0.4780158812766173, + 0.8175170051929901, + 0.32119583351535463, + 0.16212738624545397, + -0.2772750874075072, + 0.9470127963931515, + -0.8632586679996652, + -0.5047617974088805, + 5.285934823009988e-17 + ], + "orbitUpReference": [ + -0.8175170051929901, + 0.2772750874075072, + 0.5047617974088805 + ], + "metersPerUnit": 2919695806.451613, + "bodyRadiusM": 905105700000 + } + }, + "prepared": { + "format": "cssearth-css-object@5", + "url": "prepared/object.json" + } +} diff --git a/src/objects/eta-carinae/source/content/object.json b/src/objects/eta-carinae/source/content/object.json new file mode 100644 index 00000000000..2e0fba139d6 --- /dev/null +++ b/src/objects/eta-carinae/source/content/object.json @@ -0,0 +1,81 @@ +{ + "schema": "cssearth-object-content@2", + "version": 1, + "id": "eta-carinae", + "displayName": "Eta Carinae", + "panel": { + "facts": [ + { + "id": "radius", + "label": "Radius", + "value": "1301 solar radii", + "source": { + "catalogueId": "arxiv-astro-ph-0609715", + "url": "https://arxiv.org/abs/astro-ph/0609715", + "label": "Weigelt (2006)", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "radiusKm; radiusSource" + } + }, + { + "id": "distance", + "label": "Distance from the Sun", + "value": "2,350 parsecs", + "source": { + "catalogueId": "publication-smith-2006-homunculus-shape", + "url": "https://arxiv.org/abs/astro-ph/0602464", + "label": "Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at", + "checked": "2026-10-05", + "path": "source/measurements.json", + "locator": "distanceParsecs; distanceSource" + } + } + ], + "moreFacts": [] + }, + "datasets": { + "titleKey": "datasets", + "defaultDataset": "color", + "controls": [ + { + "id": "color", + "label": "Color", + "qualification": "Color of a blackbody at the star's temperature. The disc itself is unresolved; darkening toward the edge from a published law.", + "thumbnail": "eta-carinae-dataset-color.webp", + "surface": "eta-carinae-surface-color@2x.webp", + "poles": "eta-carinae-poles-color@2x.webp", + "source": { + "id": "eta-carinae-stellar-color", + "path": "../manifest.json", + "url": "https://arxiv.org/abs/1204.1963" + }, + "falseColor": false, + "notes": "The color of a blackbody at the star's published temperature. The wind's measured brightness profile is drawn from its record beside the star (source/photometry, new-object --star-limb).. The spin axis's direction on the sky is a display convention. The darkening toward the edge is the power law I(mu) = mu^6.05 whose brightness is half the centre's at the diameter Weigelt et al. (2007), arXiv:astro-ph/0609715 measured for it, 2.33 mas, on a disc that ends at their tenth-brightness diameter, 5.15 mas (VLTI/AMBER K-band continuum at 2.174 um; not a visible band)." + } + ] + }, + "settings": { + "titleKey": "settings", + "controls": [] + }, + "charts": [], + "resources": [ + { + "label": "Research", + "role": "facts", + "description": "Groh et al. (2012), arXiv:1204.1963", + "href": "https://arxiv.org/abs/1204.1963" + } + ], + "provenance": { + "editorial": { + "url": "https://arxiv.org/abs/1204.1963", + "credit": "Groh et al. (2012), arXiv:1204.1963" + }, + "physical": { + "path": "../../../../../packages/astronomy/data/bodies/eta-carinae.json", + "credit": "Weigelt et al. radius; Gaia DR3 position; cited distance; no measured rotation axis (display convention)" + } + } +} diff --git a/src/objects/eta-carinae/source/manifest.json b/src/objects/eta-carinae/source/manifest.json new file mode 100644 index 00000000000..a428e7fcf4c --- /dev/null +++ b/src/objects/eta-carinae/source/manifest.json @@ -0,0 +1,196 @@ +{ + "schema": "cssearth-authoritative-sources@3", + "inputs": [ + { + "id": "eta-carinae-observational-measurements", + "path": "measurements.json", + "origin": "https://arxiv.org/abs/1204.1963", + "credit": "Groh et al. (2012), arXiv:1204.1963", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Transcribed published measurements with their sources", + "redistribution": "Factual parameter transcription only; no paper figures", + "consumers": [ + "shape-model" + ], + "sourceBinding": { + "kind": "local", + "reason": "Measurements transcribed in this package with their sources; repinned when edited." + } + }, + { + "id": "eta-carinae-preparation-raster", + "path": "preparation/raster.json", + "origin": "Repository-authored raster recipe: the shared neutral gray on the reference sphere, transparent plates", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "assets", + "datasets" + ] + }, + { + "id": "eta-carinae-preparation-geometry", + "path": "preparation/geometry.json", + "origin": "Repository-authored CSS geometry profile: 248-unit sphere, 16 x 32 leaves, emissive material", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene", + "presentation" + ] + }, + { + "id": "eta-carinae-preparation-presentation", + "path": "preparation/presentation.json", + "origin": "Repository-authored presentation profile: emissive mode", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "presentation" + ] + }, + { + "id": "eta-carinae-physical-solar-system-recipe", + "path": "presentation/solar-system.json", + "origin": "Repository-authored scene recipe: published radius, camera plan", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored preparation record; published inputs retain their own identities and hashes." + }, + "credit": "cssEarth and the institutional sources identified in this record", + "license": "Project-authored preparation record; referenced observations retain their source terms", + "acquisition": "checked repository source", + "redistribution": "checked authored source with embedded provenance", + "consumers": [ + "scene" + ] + }, + { + "id": "eta-carinae-stellar-color", + "path": "photometry/stellar-color.json", + "origin": "https://arxiv.org/abs/1204.1963", + "credit": "Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3); CIE 1931 2° observer", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Authored method record: names the published temperature and the color computation applied", + "redistribution": "Method record only", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "local", + "reason": "Project-authored color recipe naming the cited temperature; repinned when edited." + } + }, + { + "id": "eta-carinae-gaia-dr3-source", + "path": "photometry/gaia-dr3-source.csv", + "origin": "https://gea.esac.esa.int/tap-server/tap/sync", + "credit": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26 (FLAME)", + "license": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "licenseEvidence": [ + "https://www.cosmos.esa.int/web/gaia-users/credits" + ], + "acquisition": "Gaia Archive TAP query in source/preparation/acquisition.json: the gaia_source row of source_id 5350358584482202880 (position, radial velocity) with its FLAME mass and radius.", + "redistribution": "One catalogue row, retained unchanged with its credit.", + "consumers": [ + "placement" + ], + "sourceBinding": { + "kind": "catalogued", + "references": [ + { + "catalogueId": "source-eta-carinae-gaia-dr3-source", + "role": "material", + "evidence": "Origin and product identifier recorded on this manifest entry." + } + ] + } + }, + { + "id": "eta-carinae-weigelt-2007-limb-darkening", + "path": "photometry/weigelt-2007-limb-darkening.json", + "origin": "https://arxiv.org/abs/astro-ph/0609715", + "credit": "Weigelt et al. (2007), arXiv:astro-ph/0609715", + "license": "Factual numerical measurements; source attribution retained", + "acquisition": "Transcribed from the paper, each value with its quoted cell", + "redistribution": "Factual parameter transcription only; no paper figures", + "consumers": [ + "assets", + "datasets" + ], + "sourceBinding": { + "kind": "local", + "reason": "Published limb-darkening law transcribed with its cells; repinned when edited." + } + } + ], + "generatedIntermediates": [ + { + "id": "limb-darkened-disc-context-marker", + "path": "presentation/context.png", + "origin": "https://arxiv.org/abs/1204.1963", + "credit": "The color dataset as a disc, dimmed toward the limb by its law; rendered by packages/telescope-cli/src/source-authoring/context-markers.mts", + "license": "Project-authored display derivative.", + "consumers": [ + "navigation" + ], + "recipe": { + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "inputs": [ + "eta-carinae-stellar-color", + "eta-carinae-preparation-raster", + "eta-carinae-weigelt-2007-limb-darkening" + ] + }, + "generator": "packages/telescope-cli/src/source-authoring/context-markers.mts", + "sourceBinding": { + "kind": "local", + "reason": "The color dataset rendered as a disc; `context-markers.mts --check` recomputes it." + } + } + ], + "documents": [ + { + "path": "content/object.json", + "sourceBinding": { + "kind": "local", + "reason": "Project-authored factsheet, dataset recipe and legend." + } + }, + { + "path": "preparation/acquisition.json" + }, + { + "path": "preparation/navigation.json" + }, + { + "path": "preparation/rotation.json" + }, + { + "path": "preparation/new-object.json", + "sourceBinding": { + "kind": "local", + "reason": "The spec telescope new-object generated this package from, with the order it was given; `telescope new-object --refresh` regenerates from it." + } + } + ] +} diff --git a/src/objects/eta-carinae/source/measurements.json b/src/objects/eta-carinae/source/measurements.json new file mode 100644 index 00000000000..a2e75ab7d08 --- /dev/null +++ b/src/objects/eta-carinae/source/measurements.json @@ -0,0 +1,16 @@ +{ + "schema": "cssearth-uniform-disc-star@1", + "id": "eta-carinae", + "angularDiameterMas": 5.15, + "angularDiameterSource": "Computed here from the record's radius and distance: 2 x 905,105,700 km at 2350.00 pc = 5.15 mas. No interferometric diameter of this star is used.", + "distanceParsecs": 2350, + "distanceSource": "Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at (https://arxiv.org/abs/astro-ph/0602464): 2350 pc; Gaia DR3 gives this source no parallax (a two-parameter solution)", + "radiusKm": 905105700, + "radiusSource": "Radius 1301 solar radii from Weigelt et al. (2007), arXiv:astro-ph/0609715, Table 2: the wind's K-band continuum light (2.174 µm, VLTI/AMBER) falls to a tenth of its central brightness at a diameter of 5.15 mas (±4%); half of that at 2,350 pc is 6.05 AU, 1,301 solar radii (https://arxiv.org/abs/astro-ph/0609715): 905,105,700 km at 695,700 km per solar radius. No mass is measured, so GM is 0, the records' unpublished value. Temperature 9400 K from Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3) (https://arxiv.org/abs/1204.1963). presentationUp: the display axis is a sky-plane convention; the spin axis's position angle on the sky is not measured.", + "effectiveTemperatureK": 9400, + "effectiveTemperatureSource": "Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3) (https://arxiv.org/abs/1204.1963)", + "shape": { + "kind": "uniform-disc-sphere", + "qualification": "A sphere at the published radius, colored from a blackbody at the star's published temperature (photometry/stellar-color.json); no image of the photosphere exists." + } +} diff --git a/src/objects/eta-carinae/source/photometry/stellar-color.json b/src/objects/eta-carinae/source/photometry/stellar-color.json new file mode 100644 index 00000000000..205fb1f73b0 --- /dev/null +++ b/src/objects/eta-carinae/source/photometry/stellar-color.json @@ -0,0 +1,16 @@ +{ + "schema": "cssearth-stellar-photometric-color@1", + "objectId": "eta-carinae", + "spectrum": "planck", + "temperature": { + "published": { + "kelvin": 9400, + "lowerKelvin": 9400, + "upperKelvin": 9400, + "citation": "Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3): 9400 K; the source gives no uncertainty, so the range is the value itself, https://arxiv.org/abs/1204.1963" + }, + "note": "No archive holds a spectrum of this star (stis-ngsl: HD 93308 is not in the library; gaia-xp: Gaia DR3 published no sampled BP/RP spectrum of it; pulkovo: HR 4210 is not in the catalogue; kiehling: HR 4210 is not among its 60 stars; kharitonov: HR 4210 is not in the catalogue; burnashev: BS 4210 is not in part2), so the color is a Planck spectrum at its published temperature." + }, + "whitePoint": "sRGB D65", + "normalization": "brightest linear sRGB channel = 1" +} diff --git a/src/objects/eta-carinae/source/photometry/weigelt-2007-limb-darkening.json b/src/objects/eta-carinae/source/photometry/weigelt-2007-limb-darkening.json new file mode 100644 index 00000000000..14c96627a77 --- /dev/null +++ b/src/objects/eta-carinae/source/photometry/weigelt-2007-limb-darkening.json @@ -0,0 +1,14 @@ +{ + "schema": "cssearth-published-limb-darkening@1", + "objectId": "eta-carinae", + "law": "power", + "source": "Weigelt et al. (2007), arXiv:astro-ph/0609715, Table 2, the diameters of the wind's light at 2.174 um from the VLTI/AMBER visibilities (https://arxiv.org/abs/astro-ph/0609715)", + "band": "VLTI/AMBER K-band continuum at 2.174 um; not a visible band", + "alpha": { + "value": 6.05, + "uncertainty": 0.8, + "cell": "continuum 2.1740 | d_FWHM 2.33 | d_10% 5.15 | d_50%EED 4.23 [mas]; errors are ±4% for the diameters in the continuum" + }, + "derived": "whose brightness is half the centre's at the diameter Weigelt et al. (2007), arXiv:astro-ph/0609715 measured for it, 2.33 mas, on a disc that ends at their tenth-brightness diameter, 5.15 mas", + "note": "The paper prints no exponent. I(mu)/I(1) = mu^alpha with mu = sqrt(1 - (r/R)^2): the disc's edge R is half the printed tenth-brightness diameter, and alpha = ln(0.5) / ln(sqrt(1 - (2.33/5.15)^2)) = 6.05 puts half brightness at the printed FWHM diameter. The uncertainty carries the ±4% of each diameter (alpha 5.3 to 6.9). The law is dark at the edge where the paper's profile is a tenth of the centre, and reaches a tenth at 0.73 of the radius. The wind's faint halo beyond, which holds half of the light outside 4.23 mas, is not drawn." +} diff --git a/src/objects/eta-carinae/source/preparation/acquisition.json b/src/objects/eta-carinae/source/preparation/acquisition.json new file mode 100644 index 00000000000..cff6a042686 --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/acquisition.json @@ -0,0 +1,21 @@ +{ + "schema": "cssearth-acquisition-plan@1", + "operations": [ + { + "kind": "request-download", + "groups": [ + "restore", + "refresh" + ], + "path": "photometry/gaia-dr3-source.csv", + "url": "https://gea.esac.esa.int/tap-server/tap/sync", + "form": { + "REQUEST": "doQuery", + "LANG": "ADQL", + "FORMAT": "csv", + "QUERY": "SELECT s.source_id, s.ref_epoch, s.ra, s.dec, s.parallax, s.parallax_error, s.pmra, s.pmdec, s.radial_velocity, s.radial_velocity_error, s.ruwe, s.phot_g_mean_mag, s.bp_rp, s.has_xp_sampled, a.mass_flame, a.mass_flame_lower, a.mass_flame_upper, a.radius_flame, a.radius_flame_lower, a.radius_flame_upper FROM gaiadr3.gaia_source AS s LEFT JOIN gaiadr3.astrophysical_parameters AS a ON a.source_id = s.source_id WHERE s.source_id = 5350358584482202880" + }, + "requiredPrefix": "source_id," + } + ] +} diff --git a/src/objects/eta-carinae/source/preparation/geometry.json b/src/objects/eta-carinae/source/preparation/geometry.json new file mode 100644 index 00000000000..97f45e7c0e1 --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/geometry.json @@ -0,0 +1,62 @@ +{ + "schema": "cssearth-css-geometry-profile@1", + "namespace": "eta-carinae", + "surface": { + "radius": 248, + "polarRadius": 248, + "latitudeSegments": 16, + "longitudeSegments": 32, + "surface": { + "url": "/scenes/eta-carinae/eta-carinae-surface-color@2x.webp", + "width": 1024, + "height": 768 + }, + "surfaceLatitudeHeight": 512, + "packedBandGutter": 8, + "poles": { + "url": "/scenes/eta-carinae/eta-carinae-poles-color@2x.webp", + "width": 512, + "height": 256 + }, + "polarTileSize": 256, + "polarRadiusScale": 1.035, + "polarOffset": 0.1, + "uv": "cell", + "color": "#d2ddff" + }, + "projection": { + "tileSize": 50, + "layerElevation": 50, + "seamBleed": 0, + "interiorSeamBleed": 8, + "overlap": 0.015625, + "seamOutset": { + "targetPixels": 0.5, + "stepRatio": 1.4142135623730951, + "hysteresis": 0.1, + "firstDiameter": 16, + "lastDiameter": 32768 + }, + "fitToSource": false, + "rasterScale": 2, + "rasterGutter": 8, + "rasterOverscan": 0.5, + "projectivePoles": false, + "lightColor": "#ffffff", + "ambientIntensity": 1 + }, + "bodyRotationDegrees": 0, + "output": { + "schema": "csseta-carinae-prepared-runtime-scene@1", + "materialSchema": "csseta-carinae-prepared-emission@1", + "layout": "body-container", + "body": { + "axialTiltDegrees": 0, + "rotationDirection": "prograde", + "rotationPeriodEarthDays": 36525, + "sourceProjection": "the color dataset: a uniform photosphere color on the reference sphere", + "polarPreparation": "the same color on the polar tiles", + "axialTiltNote": "the star has no measured rotation axis; the object's rotation record places a display axis along celestial north in the plane of the sky, and this profile's tilt is not used for the frame" + } + } +} diff --git a/src/objects/eta-carinae/source/preparation/navigation.json b/src/objects/eta-carinae/source/preparation/navigation.json new file mode 100644 index 00000000000..ce4070e3c1e --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/navigation.json @@ -0,0 +1,41 @@ +{ + "schema": "cssearth-navigation-marker@2", + "objectId": "eta-carinae", + "owner": "object", + "presentation": { + "size": 5 + }, + "source": { + "path": "presentation/context.png" + }, + "operations": [ + { + "type": "resize", + "width": "tile", + "height": "tile", + "fit": "cover", + "position": "centre", + "kernel": "lanczos3" + }, + { + "type": "ensure-alpha" + }, + { + "type": "ellipse-mask", + "cx": 0.5, + "cy": 0.5, + "rx": 0.45, + "ry": 0.45, + "shading": { + "ambient": 0.35, + "diffuse": 0.65 + } + }, + { + "type": "png" + } + ], + "context": { + "pixels": 512 + } +} diff --git a/src/objects/eta-carinae/source/preparation/new-object.json b/src/objects/eta-carinae/source/preparation/new-object.json new file mode 100644 index 00000000000..9f4998a3569 --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/new-object.json @@ -0,0 +1,53 @@ +{ + "id": "eta-carinae", + "name": "Eta Carinae", + "system": "Homunculus Nebula", + "description": "Eta Carinae is the star inside the Homunculus Nebula, about 2,350 parsecs away.", + "aliases": [ + "HD 93308", + "HR 4210" + ], + "parent": "homunculus-nebula", + "featured": true, + "order": 5777, + "target": "eta Car", + "gaia": "5350358584482202880", + "paper": { + "url": "https://arxiv.org/abs/1204.1963", + "credit": "Groh et al. (2012), arXiv:1204.1963" + }, + "radius": { + "value": 1301, + "source": "Weigelt et al. (2007), arXiv:astro-ph/0609715, Table 2: the wind's K-band continuum light (2.174 µm, VLTI/AMBER) falls to a tenth of its central brightness at a diameter of 5.15 mas (±4%); half of that at 2,350 pc is 6.05 AU, 1,301 solar radii", + "url": "https://arxiv.org/abs/astro-ph/0609715" + }, + "mass": "unmeasured", + "temperature": { + "value": 9400, + "source": "Groh et al. (2012), arXiv:1204.1963, Table 2: the primary's effective temperature where its wind turns opaque (Rosseland optical depth 2/3)", + "url": "https://arxiv.org/abs/1204.1963" + }, + "radialVelocity": { + "value": -25, + "source": "SIMBAD's radial velocity for Gaia DR3 5350358584482202880 (quality E), from 1953GCRV..C......0W; Gaia DR3 measures none", + "url": "https://ui.adsabs.harvard.edu/abs/1953GCRV..C......0W/abstract", + "uncertainty": 2 + }, + "distance": { + "value": 2350, + "source": "Smith (2006), ApJ 644, 1151: 2350 ± 50 pc from the Homunculus's shape and expansion, the distance its nebula's page stands at", + "url": "https://arxiv.org/abs/astro-ph/0602464" + }, + "limb": { + "none": "No model atmosphere describes the light of an opaque wind. The wind's measured brightness profile is drawn from its record beside the star (source/photometry, new-object --star-limb)." + }, + "planets": [], + "companions": [], + "notes": [ + "What is drawn is the star's wind, not its surface. The star under the wind is modelled at 60 solar radii and 35,200 K, and models from 60 to 480 solar radii fit its spectrum equally (Groh et al. 2012, Table 2 and Sect. 3, https://arxiv.org/abs/1204.1963).", + "The near-infrared light is not round: VLTI measurements find it elongated, which fast rotation or the companion's cavity in the wind both explain (Groh et al. 2010, https://arxiv.org/abs/1006.4816). It is drawn as a sphere.", + "Half of the wind's near-infrared light comes from outside a diameter of 4.23 mas, in a faint halo that reaches past the sphere drawn (Weigelt et al. 2007, Table 2, https://arxiv.org/abs/astro-ph/0609715). The halo is not drawn.", + "It is a binary with a period of 5.54 years. The companion is not seen directly, and the orbit's tilt and direction come from models of the colliding winds (Madura et al. 2012, as adopted by Groh et al. 2012, Table 2: inclination 138 degrees, longitude of periastron 260 degrees). The companion is not drawn.", + "The star's mass is not measured." + ] +} diff --git a/src/objects/eta-carinae/source/preparation/presentation.json b/src/objects/eta-carinae/source/preparation/presentation.json new file mode 100644 index 00000000000..c2c8618fbff --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/presentation.json @@ -0,0 +1,5 @@ +{ + "schema": "cssearth-css-presentation-profile@2", + "namespace": "eta-carinae", + "mode": "emissive" +} diff --git a/src/objects/eta-carinae/source/preparation/raster.json b/src/objects/eta-carinae/source/preparation/raster.json new file mode 100644 index 00000000000..c1e07691299 --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/raster.json @@ -0,0 +1,65 @@ +{ + "schema": "cssearth-raster-recipe@2", + "publicBase": "/scenes/eta-carinae/", + "sourceWidth": 1024, + "sourceHeight": 512, + "width": 1024, + "height": 512, + "latitudeBands": 16, + "polarTile": 256, + "resample": "density-before-pack", + "polarProjection": "orthographic-bilinear", + "polesOutput": "eta-carinae-poles-{id}{suffix}.webp", + "surfaceMetadata": { + "schema": "csseta-carinae-prepared-assets@1" + }, + "thumbnail": { + "size": 64, + "centerLongitudeDegrees": 0 + }, + "surfaces": [ + { + "id": "color", + "output": "eta-carinae-surface-{id}{suffix}.webp", + "thumbnail": "eta-carinae-dataset-{id}.webp", + "source": "photometry/stellar-color.json", + "falseColor": false, + "science": { + "kind": "stellar-photometric-color", + "qualification": "Photosphere color from a blackbody at the star's published temperature. The wind's measured brightness profile is drawn from its record beside the star (source/photometry, new-object --star-limb).. The disc is unresolved: no map or absolute brightness is implied. Self-luminous, so no lighting. The disc is dimmed toward the limb by the power law I(mu) = mu^6.05 whose brightness is half the centre's at the diameter Weigelt et al. (2007), arXiv:astro-ph/0609715 measured for it, 2.33 mas, on a disc that ends at their tenth-brightness diameter, 5.15 mas (VLTI/AMBER K-band continuum at 2.174 um; not a visible band).", + "limbDarkening": { + "law": "power", + "published": true, + "path": "photometry/weigelt-2007-limb-darkening.json" + } + } + } + ], + "emission": { + "offLimbSize": 600, + "limbSize": 512, + "bodyDiameter": 496, + "offLimbOutput": "eta-carinae-context-{id}{suffix}.webp", + "limbOutput": "eta-carinae-limb-{id}{suffix}.webp", + "metadata": { + "schema": "csseta-carinae-prepared-emission@1", + "presentation": "prepared-emissive-surface-with-stationary-off-limb-context-and-limb-plate", + "offLimbContext": { + "logicalSize": 600, + "source": "none: no observation, a transparent plate", + "composition": "transparent", + "rotation": "none", + "runtimeAlphaProcessing": false + }, + "limbMaterial": { + "logicalSize": 496, + "composition": "black alpha darkens the photosphere according to the selected limb-darkening law; transparent outside the silhouette", + "surfaceReplacement": false, + "runtimeAlphaProcessing": false + }, + "lighting": false, + "shadows": false, + "runtimeLighting": false + } + } +} diff --git a/src/objects/eta-carinae/source/preparation/rotation.json b/src/objects/eta-carinae/source/preparation/rotation.json new file mode 100644 index 00000000000..1b02edde29e --- /dev/null +++ b/src/objects/eta-carinae/source/preparation/rotation.json @@ -0,0 +1,10 @@ +{ + "schema": "cssearth-display-orientation@1", + "rightAscensionDegrees": 161.2647407537247, + "declinationDegrees": 30.31554122229548, + "displayMeridianDegrees": 90, + "phase": "arbitrary-display-phase", + "source": "No measured rotation axis or period is used. The display axis is celestial north at the catalogue position, placed in the plane of the sky; computed by skyPlaneOrientation in @cssearth/astronomy.", + "coordinateSystem": "ICRF/J2000. +Z is the display axis: the sky-north direction at the star, in the plane of the sky. +X is the display meridian, set so that grid longitude 0 faces the Sun and Earth at the scene epoch; east longitude. No spin is propagated.", + "qualification": "Display convention, not a measurement. The rotation axis, spin sense, period and prime meridian of Eta Carinae are unmeasured; the axis shown is where celestial north lies on the sky." +} diff --git a/src/objects/eta-carinae/source/presentation/context.png b/src/objects/eta-carinae/source/presentation/context.png new file mode 100644 index 00000000000..8cb77a966d2 Binary files /dev/null and b/src/objects/eta-carinae/source/presentation/context.png differ diff --git a/src/objects/eta-carinae/source/presentation/solar-system.json b/src/objects/eta-carinae/source/presentation/solar-system.json new file mode 100644 index 00000000000..961b6da9e2e --- /dev/null +++ b/src/objects/eta-carinae/source/presentation/solar-system.json @@ -0,0 +1,9 @@ +{ + "schema": "cssearth-solar-system-preparation@1", + "bodyId": "eta-carinae", + "displayName": "Eta Carinae", + "bodyRadiusUnits": 248, + "bodyRadiusKilometers": 905105700, + "defaultZoom": 1.25, + "geometryScale": 1.25 +} diff --git a/src/objects/eta-carinae/text.json b/src/objects/eta-carinae/text.json new file mode 100644 index 00000000000..4daba92c6cd --- /dev/null +++ b/src/objects/eta-carinae/text.json @@ -0,0 +1,42 @@ +{ + "schema": "cssearth-object-text@1", + "objectId": "eta-carinae", + "card": { + "text": "Eta Carinae is the star inside the Homunculus Nebula, about 2,350 parsecs away.", + "sources": [ + { + "catalogueId": "publication-smith-2006-homunculus-shape", + "url": "https://arxiv.org/abs/astro-ph/0602464", + "label": "Smith (2006), ApJ 644, 1151", + "checked": "2026-10-05", + "locator": "The Homunculus's shape and expansion give 2350 ± 50 pc" + } + ] + }, + "introduction": { + "text": "No surface is seen, only a wind that turns opaque at 9,400 K; its near-infrared glow fades to a tenth 6 au from the centre. An unseen companion circles it every 5.54 years.", + "sources": [ + { + "catalogueId": "arxiv-astro-ph-0609715", + "url": "https://arxiv.org/abs/astro-ph/0609715", + "label": "Weigelt et al. (2007), arXiv:astro-ph/0609715", + "checked": "2026-10-05", + "locator": "Table 2, continuum at 2.174 µm: FWHM diameter 2.33 mas, diameter at 10% of the peak intensity 5.15 mas, 50% encircled-energy diameter 4.23 mas" + }, + { + "catalogueId": "arxiv-1204-1963", + "url": "https://arxiv.org/abs/1204.1963", + "label": "Groh et al. (2012), arXiv:1204.1963", + "checked": "2026-10-05", + "locator": "Table 2: effective temperature 9400 K at Rosseland optical depth 2/3; orbital parameters adopted from Madura et al. (2012). Sect. 1: an eccentric binary with a period of 5.54 yr" + } + ] + }, + "datasets": { + "color": { + "title": "Photosphere color", + "detail": "From its temperature", + "summary": "The color of the star's light." + } + } +} diff --git a/src/objects/homunculus-nebula/README.md b/src/objects/homunculus-nebula/README.md index 80d98f32252..6b264671284 100644 --- a/src/objects/homunculus-nebula/README.md +++ b/src/objects/homunculus-nebula/README.md @@ -1,6 +1,6 @@ # Homunculus Nebula -The Homunculus Nebula, the two-lobed cloud of dust around the star Eta Carinae, as an object of the world: its place, its card and its list marker. It has no surface of its own here. Its dataset shows the [Homunculus Nebula image layers](../homunculus-nebula-layers/README.md) bank, whose README holds the sources, processing and known problems of the picture. +The Homunculus Nebula, the two-lobed cloud of dust around the star Eta Carinae, as an object of the world: its place, its card and its list marker. It has no surface of its own here. Its dataset shows the [Homunculus Nebula image layers](../homunculus-nebula-layers/README.md) bank, whose README holds the sources, processing and known problems of the picture. The star is an object of its own inside it, [Eta Carinae](../eta-carinae/README.md). ## Sources @@ -14,4 +14,4 @@ The card's facts cite their papers in [source/content/object.json](source/conten ## Known problems - The framing radius is a presentation value, not a measured extent: the lobes reach about 11 arcsec from the star along their pole. -- Eta Carinae itself has no page yet; the nebula stands at the star's place. +- The nebula stands at the star's place. [Eta Carinae](../eta-carinae/README.md) has its own page: a zoom in on the nebula goes on into it, and its page draws the nebula's surface around the star. diff --git a/src/objects/homunculus-nebula/inventory.json b/src/objects/homunculus-nebula/inventory.json index 4fe3cf8cb45..f0840cb23c4 100644 --- a/src/objects/homunculus-nebula/inventory.json +++ b/src/objects/homunculus-nebula/inventory.json @@ -25,6 +25,12 @@ "bytes": 613, "sha256": "fbbdb9c048a1b6ac72e1549547bad9cba6d846c5c8e261e8759251a47da59c85" }, + { + "location": "prepared", + "filename": "members.json", + "bytes": 620, + "sha256": "0a8bb566d127d065cec9b1f9a39145304f5886ef54017618aac97ed365b41e45" + }, { "location": "prepared", "filename": "runtime.json", diff --git a/src/objects/observable-universe/inventory.json b/src/objects/observable-universe/inventory.json index 5a5e593c32e..5faeff5cf88 100644 --- a/src/objects/observable-universe/inventory.json +++ b/src/objects/observable-universe/inventory.json @@ -64,14 +64,14 @@ { "location": "prepared", "filename": "world-context.json", - "bytes": 58689179, - "sha256": "d66e886591c6af3dbf307d60b653af82fd7544b073cf01bd6ff84c163a22f791" + "bytes": 58694714, + "sha256": "a24a4aaa69e4b8b98389452525b6dbac9c96019e81606e714b1997a6b643431d" }, { "location": "prepared", "filename": "world-index.json", - "bytes": 1416604, - "sha256": "326c1ae0c29eb411079b1ccc007a8bb137775280f2586c7d226501e552416a21" + "bytes": 1416638, + "sha256": "2d4aeee184a2de5e9231b74e51e964483d5c6f90b6a73f8349793d985ad3910e" }, { "location": "prepared", @@ -83,7 +83,7 @@ "location": "prepared", "filename": "world.json", "bytes": 2534, - "sha256": "5ad4639a1abb053f453e063b83f34211623c1e6abb65f81cbbeabe0c4f62919f" + "sha256": "f19e2b44dba61c41225039f8cf85f1344f87a0a7767630cdfa92b67ab98ad675" } ] } diff --git a/src/renderers/css/styles/eta-carinae-surfaces.css b/src/renderers/css/styles/eta-carinae-surfaces.css new file mode 100644 index 00000000000..197c7e488c2 --- /dev/null +++ b/src/renderers/css/styles/eta-carinae-surfaces.css @@ -0,0 +1,76 @@ +/* Eta Carinae: the Sun's emissive presentation (body-surfaces.css, SUN block) scoped to this object, loaded after the shared + body-surfaces.css base rules. No spin: the rotation axis and period are unmeasured. Both plates are transparent: no observation is cast. */ +/* The shared stage rule (body-surfaces.css) sizes the camera and the Sun's plates; this object's plates need the same box. */ +.object-stage[data-object-id="eta-carinae"] > :is(.eta-carinae-corona-layer, .eta-carinae-limb-layer) { + position: absolute; + inset: 0; + transform-origin: 50% 50%; + pointer-events: none; +} + +/* Paint order: off-limb context behind the sphere, limb plate above it. */ +.object-stage[data-object-id="eta-carinae"] .eta-carinae-corona-layer { + z-index: 0; +} + +.object-stage[data-object-id="eta-carinae"] .polycss-camera { + z-index: 1; +} + +.object-stage[data-object-id="eta-carinae"] .eta-carinae-limb-layer { + z-index: 2; +} + +.object-stage[data-object-id="eta-carinae"] .eta-carinae-body { + transform-style: preserve-3d; +} + +.object-stage[data-object-id="eta-carinae"] .polycss-scene s { + position: absolute; + display: block; + transform-origin: 0 0; + margin: 0; + padding: 0; + line-height: 0; + text-decoration: none; + background-repeat: no-repeat; + pointer-events: none; +} + +/* The silhouette-fit binding already includes physical framing. Keep the prepared plate's native dimensions. */ +.object-stage[data-object-id="eta-carinae"] .eta-carinae-corona-layer { + background-position: center; + background-repeat: no-repeat; + background-size: + 600px + 600px; +} + +/* Limb plate: 496 px = raster.json emission.bodyDiameter = camera.logicalBodyDiameter. */ +.object-stage[data-object-id="eta-carinae"] .eta-carinae-limb-layer { + background-position: center; + background-repeat: no-repeat; + background-size: + 496px + 496px; +} + +.object-stage[data-object-id="eta-carinae"] .polycss-camera { + display: block; + width: 100%; + height: 100%; +} + +.object-stage[data-object-id="eta-carinae"] .polycss-scene { + will-change: transform; +} + +.object-stage[data-object-id="eta-carinae"] .polycss-scene s { + width: 64px; + height: 64px; + transform-style: preserve-3d; + font: inherit; + font-weight: normal; + font-style: normal; + line-height: 0; +} diff --git a/src/sources/arxiv-1006-4816.json b/src/sources/arxiv-1006-4816.json new file mode 100644 index 00000000000..f571a270837 --- /dev/null +++ b/src/sources/arxiv-1006-4816.json @@ -0,0 +1,51 @@ +{ + "id": "arxiv-1006-4816", + "kind": "publication", + "identityLevel": "work", + "title": "Groh et al. (2010): Is Eta Carinae a fast rotator, and how much does the companion influence the inner wind structure?", + "identifiers": [ + { + "type": "arXiv", + "value": "1006.4816" + }, + { + "type": "DOI", + "value": "10.1088/2041-8205/716/2/L223" + } + ], + "links": [ + { + "role": "archive", + "url": "https://arxiv.org/abs/1006.4816", + "label": "arXiv preprint" + }, + { + "role": "landing", + "url": "https://doi.org/10.1088/2041-8205/716/2/L223", + "label": "Journal version" + } + ], + "evidence": [ + { + "url": "https://arxiv.org/abs/1006.4816", + "checkedOn": "2026-10-05", + "locator": "arXiv API record: title, authors, journal reference" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "Groh et al. (2010)", + "scope": "citation", + "evidence": "https://arxiv.org/abs/1006.4816" + } + ], + "creators": [ + "Jose H. Groh", + "Thomas I. Madura", + "Stanley P. Owocki", + "et al." + ], + "publicationDate": "2010" +} diff --git a/src/sources/arxiv-1204-1963.json b/src/sources/arxiv-1204-1963.json new file mode 100644 index 00000000000..e4b6f356603 --- /dev/null +++ b/src/sources/arxiv-1204-1963.json @@ -0,0 +1,51 @@ +{ + "id": "arxiv-1204-1963", + "kind": "publication", + "identityLevel": "work", + "title": "Groh et al. (2012): On the influence of the companion star in Eta Carinae: 2D radiative transfer modeling of the ultraviolet and optical spectra", + "identifiers": [ + { + "type": "arXiv", + "value": "1204.1963" + }, + { + "type": "DOI", + "value": "10.1111/j.1365-2966.2012.20984.x" + } + ], + "links": [ + { + "role": "archive", + "url": "https://arxiv.org/abs/1204.1963", + "label": "arXiv preprint" + }, + { + "role": "landing", + "url": "https://doi.org/10.1111/j.1365-2966.2012.20984.x", + "label": "Journal version" + } + ], + "evidence": [ + { + "url": "https://arxiv.org/abs/1204.1963", + "checkedOn": "2026-10-05", + "locator": "arXiv API record: title, authors, journal reference" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "Groh et al. (2012)", + "scope": "citation", + "evidence": "https://arxiv.org/abs/1204.1963" + } + ], + "creators": [ + "Jose H. Groh", + "D. John Hillier", + "Thomas I. Madura", + "et al." + ], + "publicationDate": "2012" +} diff --git a/src/sources/arxiv-astro-ph-0609715.json b/src/sources/arxiv-astro-ph-0609715.json new file mode 100644 index 00000000000..e5a5dc43d63 --- /dev/null +++ b/src/sources/arxiv-astro-ph-0609715.json @@ -0,0 +1,48 @@ +{ + "id": "arxiv-astro-ph-0609715", + "kind": "publication", + "identityLevel": "work", + "title": "Weigelt (2006): Near-Infrared interferometry of Eta Carinae with high spatial and spectral resolution using the VLTI and the AMBER instrument", + "identifiers": [ + { + "type": "arXiv", + "value": "astro-ph/0609715" + }, + { + "type": "DOI", + "value": "10.1051/0004-6361:20065577" + } + ], + "links": [ + { + "role": "archive", + "url": "https://arxiv.org/abs/astro-ph/0609715", + "label": "arXiv preprint" + }, + { + "role": "landing", + "url": "https://doi.org/10.1051/0004-6361:20065577", + "label": "Journal version" + } + ], + "evidence": [ + { + "url": "https://arxiv.org/abs/astro-ph/0609715", + "checkedOn": "2026-10-05", + "locator": "arXiv API record: title, authors, journal reference" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "Weigelt (2006)", + "scope": "citation", + "evidence": "https://arxiv.org/abs/astro-ph/0609715" + } + ], + "creators": [ + "G. Weigelt" + ], + "publicationDate": "2006" +} diff --git a/src/sources/gaia-dr3-eta-carinae.json b/src/sources/gaia-dr3-eta-carinae.json new file mode 100644 index 00000000000..d0c45e8d293 --- /dev/null +++ b/src/sources/gaia-dr3-eta-carinae.json @@ -0,0 +1,41 @@ +{ + "id": "gaia-dr3-eta-carinae", + "kind": "data-product", + "identityLevel": "work", + "title": "Gaia DR3 gaia_source row for Eta Carinae (source_id 5350358584482202880)", + "identifiers": [ + { + "type": "Gaia DR3 source_id", + "value": "5350358584482202880" + } + ], + "links": [ + { + "role": "archive", + "url": "https://gea.esac.esa.int/archive/", + "label": "Gaia Archive" + }, + { + "role": "landing", + "url": "https://doi.org/10.1051/0004-6361/202243940", + "label": "Gaia Collaboration (2023), Gaia DR3 summary" + } + ], + "evidence": [ + { + "url": "https://gea.esac.esa.int/tap-server/tap/sync?REQUEST=doQuery&LANG=ADQL&FORMAT=csv&QUERY=SELECT+s.source_id%2C+s.ref_epoch%2C+s.ra%2C+s.dec%2C+s.parallax%2C+s.parallax_error%2C+s.pmra%2C+s.pmdec%2C+s.radial_velocity%2C+s.radial_velocity_error%2C+s.ruwe%2C+s.phot_g_mean_mag%2C+s.bp_rp%2C+s.has_xp_sampled%2C+a.mass_flame%2C+a.mass_flame_lower%2C+a.mass_flame_upper%2C+a.radius_flame%2C+a.radius_flame_lower%2C+a.radius_flame_upper+FROM+gaiadr3.gaia_source+AS+s+LEFT+JOIN+gaiadr3.astrophysical_parameters+AS+a+ON+a.source_id+%3D+s.source_id+WHERE+s.source_id+%3D+5350358584482202880", + "checkedOn": "2026-10-05", + "locator": "gaiadr3.gaia_source at epoch 2016.0: a two-parameter solution (position only), has_xp_sampled false" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26", + "scope": "citation", + "evidence": "https://www.cosmos.esa.int/web/gaia-users/credits" + } + ], + "publisher": "European Space Agency, Gaia Data Processing and Analysis Consortium" +} diff --git a/src/sources/source-eta-carinae-gaia-dr3-source.json b/src/sources/source-eta-carinae-gaia-dr3-source.json new file mode 100644 index 00000000000..e6c1e854ba3 --- /dev/null +++ b/src/sources/source-eta-carinae-gaia-dr3-source.json @@ -0,0 +1,35 @@ +{ + "id": "source-eta-carinae-gaia-dr3-source", + "kind": "data-product", + "identityLevel": "work", + "title": "Eta Carinae · eta carinae gaia dr3 source", + "identifiers": [], + "links": [ + { + "role": "landing", + "url": "https://gea.esac.esa.int/tap-server/tap/sync", + "label": "Source" + } + ], + "evidence": [ + { + "path": "src/objects/eta-carinae/source/manifest.json", + "locator": "/inputs/6" + } + ], + "relations": [], + "statements": [ + { + "kind": "credit", + "text": "ESA/Gaia/DPAC; Gaia Collaboration (2023), A&A 674, A1; Creevey et al. (2023), A&A 674, A26 (FLAME)", + "scope": "The eta carinae gaia dr3 source input.", + "evidence": "Manifest credit field." + }, + { + "kind": "rights", + "text": "Gaia data are public under the ESA Gaia data policy; the Gaia/DPAC credit is retained", + "scope": "Redistributed display derivatives.", + "evidence": "Manifest license field." + } + ] +}