caelus 0.22.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/accuracy.json +1 -1
- package/dist/src/chart.d.ts +30 -1
- package/dist/src/chart.js +45 -3
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/skyview.d.ts +10 -3
- package/dist/src/skyview.js +23 -0
- package/dist/src/synthetic.d.ts +249 -0
- package/dist/src/synthetic.js +260 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# caelus
|
|
2
2
|
|
|
3
3
|
Clean-room TypeScript astrology computation: ephemeris, charts, events,
|
|
4
|
-
hellenistic timing techniques, Vedic methods, citable chart facts,
|
|
5
|
-
image-prompt frames. MIT, no Swiss Ephemeris code, no AGPL, no ephemeris files.
|
|
4
|
+
hellenistic timing techniques, Vedic methods, citable chart facts, sky-view
|
|
5
|
+
image-prompt frames, and synthetic ephemeris for imaginary bodies. MIT, no Swiss Ephemeris code, no AGPL, no ephemeris files.
|
|
6
6
|
1:1 port of the Python reference, checked by golden fixtures.
|
|
7
7
|
|
|
8
8
|
## Verification chain
|
|
@@ -103,4 +103,4 @@ test/golden.test.ts conformance suite vs Python fixtures
|
|
|
103
103
|
- caelus — this package
|
|
104
104
|
- [caelus-birth](https://www.npmjs.com/package/caelus-birth) — local birth time + place → UT (charts take UT; use this)
|
|
105
105
|
- [caelus-wheel](https://www.npmjs.com/package/caelus-wheel) — React SVG chart wheel
|
|
106
|
-
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, thirty-
|
|
106
|
+
- [caelus-mcp](https://www.npmjs.com/package/caelus-mcp) — MCP server, thirty-four chart tools over stdio
|
package/accuracy.json
CHANGED
package/dist/src/chart.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** astroengine chart -- public API: natal charts, aspects, retrogrades. */
|
|
2
|
-
import { EngineData, AYANAMSA_J2000 } from "./core.js";
|
|
2
|
+
import { EngineData, AYANAMSA_J2000, XyzSource } from "./core.js";
|
|
3
|
+
import type { SyntheticRender } from "./synthetic.js";
|
|
3
4
|
import type { AspectPhase } from "./electional.js";
|
|
4
5
|
export declare const BODIES: readonly ["sun", "moon", "mercury", "venus", "mars", "jupiter", "saturn", "uranus", "neptune", "pluto", "chiron", "mean_node", "true_node"];
|
|
5
6
|
export type Body = (typeof BODIES)[number];
|
|
@@ -173,7 +174,35 @@ export declare class Engine {
|
|
|
173
174
|
private moonCheb;
|
|
174
175
|
private chironCheb;
|
|
175
176
|
private packs;
|
|
177
|
+
private runtimeSources;
|
|
178
|
+
private renderAttrs;
|
|
176
179
|
constructor(data: EngineData);
|
|
180
|
+
/**
|
|
181
|
+
* Register a runtime {@link XyzSource} under a body id, so it resolves through
|
|
182
|
+
* {@link Engine.position}, {@link Engine.longitude}, {@link Engine.chartAt},
|
|
183
|
+
* and everything built on them (transits, returns, retrograde, SkyView) with
|
|
184
|
+
* no special-casing — exactly like a baked-in Chebyshev or Kepler pack. The
|
|
185
|
+
* source yields heliocentric ecliptic-J2000 xyz in AU at a **TT** Julian Day,
|
|
186
|
+
* the same contract Chiron and the Uranian bodies satisfy; the engine applies
|
|
187
|
+
* light-time, aberration, precession and nutation to it like any real body.
|
|
188
|
+
*
|
|
189
|
+
* This is the seam the `synthetic` module plugs imaginary bodies into (see
|
|
190
|
+
* {@link registerSyntheticSystem}). A registered id shadows a baked-in pack of
|
|
191
|
+
* the same name and persists for the engine's lifetime.
|
|
192
|
+
*
|
|
193
|
+
* @param id The body id to register (any string).
|
|
194
|
+
* @param source A heliocentric xyz source; see {@link XyzSource}.
|
|
195
|
+
* @returns This engine, for chaining.
|
|
196
|
+
*/
|
|
197
|
+
registerSource(id: string, source: XyzSource): this;
|
|
198
|
+
/** Author how a runtime body should look in SkyView (size, magnitude, colour).
|
|
199
|
+
* Position still comes from {@link registerSource}; this owns appearance only. */
|
|
200
|
+
registerRender(id: string, render: SyntheticRender): this;
|
|
201
|
+
/** SkyView appearance for a registered body, if any. */
|
|
202
|
+
renderFor(id: string): SyntheticRender | undefined;
|
|
203
|
+
/** Whether `body` resolves through the generic packed-source path: a baked-in
|
|
204
|
+
* Chebyshev/Kepler pack or a runtime source from {@link registerSource}. */
|
|
205
|
+
private hasPack;
|
|
177
206
|
private pack;
|
|
178
207
|
private moonInRange;
|
|
179
208
|
/**
|
package/dist/src/chart.js
CHANGED
|
@@ -135,11 +135,51 @@ export class Engine {
|
|
|
135
135
|
moonCheb;
|
|
136
136
|
chironCheb;
|
|
137
137
|
packs = new Map();
|
|
138
|
+
runtimeSources = new Map();
|
|
139
|
+
renderAttrs = new Map();
|
|
138
140
|
constructor(data) {
|
|
139
141
|
this.data = data;
|
|
140
142
|
this.moonCheb = data.moonCheb ? new ChebSeries(data.moonCheb) : null;
|
|
141
143
|
this.chironCheb = data.chiron ? new ChebSeries(data.chiron) : null;
|
|
142
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Register a runtime {@link XyzSource} under a body id, so it resolves through
|
|
147
|
+
* {@link Engine.position}, {@link Engine.longitude}, {@link Engine.chartAt},
|
|
148
|
+
* and everything built on them (transits, returns, retrograde, SkyView) with
|
|
149
|
+
* no special-casing — exactly like a baked-in Chebyshev or Kepler pack. The
|
|
150
|
+
* source yields heliocentric ecliptic-J2000 xyz in AU at a **TT** Julian Day,
|
|
151
|
+
* the same contract Chiron and the Uranian bodies satisfy; the engine applies
|
|
152
|
+
* light-time, aberration, precession and nutation to it like any real body.
|
|
153
|
+
*
|
|
154
|
+
* This is the seam the `synthetic` module plugs imaginary bodies into (see
|
|
155
|
+
* {@link registerSyntheticSystem}). A registered id shadows a baked-in pack of
|
|
156
|
+
* the same name and persists for the engine's lifetime.
|
|
157
|
+
*
|
|
158
|
+
* @param id The body id to register (any string).
|
|
159
|
+
* @param source A heliocentric xyz source; see {@link XyzSource}.
|
|
160
|
+
* @returns This engine, for chaining.
|
|
161
|
+
*/
|
|
162
|
+
registerSource(id, source) {
|
|
163
|
+
this.runtimeSources.set(id, source);
|
|
164
|
+
this.packs.set(id, source);
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
/** Author how a runtime body should look in SkyView (size, magnitude, colour).
|
|
168
|
+
* Position still comes from {@link registerSource}; this owns appearance only. */
|
|
169
|
+
registerRender(id, render) {
|
|
170
|
+
this.renderAttrs.set(id, render);
|
|
171
|
+
return this;
|
|
172
|
+
}
|
|
173
|
+
/** SkyView appearance for a registered body, if any. */
|
|
174
|
+
renderFor(id) {
|
|
175
|
+
return this.renderAttrs.get(id);
|
|
176
|
+
}
|
|
177
|
+
/** Whether `body` resolves through the generic packed-source path: a baked-in
|
|
178
|
+
* Chebyshev/Kepler pack or a runtime source from {@link registerSource}. */
|
|
179
|
+
hasPack(body) {
|
|
180
|
+
return !!(this.data.chebPacks?.[body] || this.data.keplerPack?.bodies[body])
|
|
181
|
+
|| this.runtimeSources.has(body);
|
|
182
|
+
}
|
|
143
183
|
pack(body) {
|
|
144
184
|
let s = this.packs.get(body);
|
|
145
185
|
if (!s) {
|
|
@@ -176,6 +216,7 @@ export class Engine {
|
|
|
176
216
|
...[...BODIES, ...EXTRA_BODIES].filter((b) => b !== "chiron" || this.chironCheb),
|
|
177
217
|
...Object.keys(this.data.chebPacks ?? {}),
|
|
178
218
|
...Object.keys(this.data.keplerPack?.bodies ?? {}),
|
|
219
|
+
...this.runtimeSources.keys(),
|
|
179
220
|
];
|
|
180
221
|
}
|
|
181
222
|
/**
|
|
@@ -231,8 +272,9 @@ export class Engine {
|
|
|
231
272
|
: oscApogeeSeries(this.data, jde);
|
|
232
273
|
return [lon, lat, km / KM_PER_AU];
|
|
233
274
|
}
|
|
234
|
-
if (this.
|
|
235
|
-
// same heliocentric pipeline as Chiron (Chebyshev or
|
|
275
|
+
if (this.hasPack(body)) {
|
|
276
|
+
// same heliocentric pipeline as Chiron (Chebyshev, Kepler, or a runtime
|
|
277
|
+
// source registered via registerSource — e.g. a synthetic body)
|
|
236
278
|
return chironApparent(this.data, this.pack(body), jde);
|
|
237
279
|
}
|
|
238
280
|
if (this.data.vsop[body])
|
|
@@ -425,7 +467,7 @@ export class Engine {
|
|
|
425
467
|
b = Math.atan2(z, Math.hypot(x, y));
|
|
426
468
|
[l, b] = precessEcliptic(l, b, J2000, jde);
|
|
427
469
|
}
|
|
428
|
-
else if (this.
|
|
470
|
+
else if (this.hasPack(body)) {
|
|
429
471
|
const [x, y, z] = this.pack(body).xyz(jde);
|
|
430
472
|
r = Math.sqrt(x * x + y * y + z * z);
|
|
431
473
|
l = mod(Math.atan2(y, x), TWO_PI);
|
package/dist/src/index.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./astrocartography.js";
|
|
|
16
16
|
export * from "./ephemeris.js";
|
|
17
17
|
export * from "./features.js";
|
|
18
18
|
export * from "./compiler.js";
|
|
19
|
+
export * from "./synthetic.js";
|
|
19
20
|
export * from "./lots.js";
|
|
20
21
|
export * from "./profections.js";
|
|
21
22
|
export * from "./firdaria.js";
|
package/dist/src/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./astrocartography.js";
|
|
|
16
16
|
export * from "./ephemeris.js";
|
|
17
17
|
export * from "./features.js";
|
|
18
18
|
export * from "./compiler.js";
|
|
19
|
+
export * from "./synthetic.js";
|
|
19
20
|
export * from "./lots.js";
|
|
20
21
|
export * from "./profections.js";
|
|
21
22
|
export * from "./firdaria.js";
|
package/dist/src/skyview.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { Engine,
|
|
1
|
+
import { Engine, Observer, HouseSystem } from "./chart.js";
|
|
2
|
+
import type { SyntheticRender } from "./synthetic.js";
|
|
2
3
|
export type SkyProjection = "rectilinear" | "fisheye";
|
|
3
4
|
/** A resolved lens: field of view and projection actually used. */
|
|
4
5
|
export interface SkyLens {
|
|
@@ -57,8 +58,12 @@ export interface SkyViewOptions {
|
|
|
57
58
|
/** Reference-frame overlays to project onto the sky (annotations, not part of
|
|
58
59
|
* a photoreal render). */
|
|
59
60
|
overlays?: SkyViewOverlaysRequest;
|
|
60
|
-
/** Bodies to place. Defaults to Sun, Moon, and the naked-eye planets.
|
|
61
|
-
|
|
61
|
+
/** Bodies to place. Defaults to Sun, Moon, and the naked-eye planets. Any
|
|
62
|
+
* string id works for runtime bodies registered via {@link Engine.registerSource}. */
|
|
63
|
+
bodies?: readonly string[];
|
|
64
|
+
/** Per-body appearance overrides (size, magnitude, colour). Merged with
|
|
65
|
+
* {@link Engine.renderFor} for registered bodies; opts win on conflict. */
|
|
66
|
+
render?: Record<string, SyntheticRender>;
|
|
62
67
|
}
|
|
63
68
|
export interface SkyBody {
|
|
64
69
|
id: string;
|
|
@@ -81,6 +86,8 @@ export interface SkyBody {
|
|
|
81
86
|
phaseName?: string;
|
|
82
87
|
brightLimbAngleDeg?: number;
|
|
83
88
|
brightLimbClock?: string;
|
|
89
|
+
/** Authored colour hint for synthetic or overridden bodies. */
|
|
90
|
+
color?: string;
|
|
84
91
|
note?: string;
|
|
85
92
|
}
|
|
86
93
|
export interface SkyOffFrameBody {
|
package/dist/src/skyview.js
CHANGED
|
@@ -188,6 +188,22 @@ function displayName(id) {
|
|
|
188
188
|
return id.slice(5);
|
|
189
189
|
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
190
190
|
}
|
|
191
|
+
/** Merge engine-registered and opt-in render attrs; apply size, magnitude, colour. */
|
|
192
|
+
function applyRenderAttrs(body, id, render, lens, width, limit) {
|
|
193
|
+
if (!render)
|
|
194
|
+
return;
|
|
195
|
+
if (render.sizeDeg !== undefined) {
|
|
196
|
+
body.angularDiameterDeg = Math.round(render.sizeDeg * 1e4) / 1e4;
|
|
197
|
+
body.sizePx = Math.max(1, Math.round((render.sizeDeg * width) / lens.hfovDeg));
|
|
198
|
+
}
|
|
199
|
+
if (render.magnitude !== undefined) {
|
|
200
|
+
body.magnitude = Math.round(render.magnitude * 100) / 100;
|
|
201
|
+
body.nakedEye = id === "sun" || id === "moon" || render.magnitude <= limit;
|
|
202
|
+
body.brightnessHint = brightnessDescriptor(body.magnitude, body.nakedEye);
|
|
203
|
+
}
|
|
204
|
+
if (render.color)
|
|
205
|
+
body.color = render.color;
|
|
206
|
+
}
|
|
191
207
|
/** Map apparent magnitude to a rendering cue: how prominent the point should
|
|
192
208
|
* look in the image. This is deliberately decoupled from the body's true
|
|
193
209
|
* angular size, which is sub-pixel for a planet: in a photo a bright point's
|
|
@@ -443,6 +459,11 @@ export function skyView(engine, jdUt, view, opts = {}) {
|
|
|
443
459
|
body.brightLimbAngleDeg = Math.round(angle * 10) / 10;
|
|
444
460
|
body.brightLimbClock = clockOf(angle);
|
|
445
461
|
}
|
|
462
|
+
const authored = {
|
|
463
|
+
...engine.renderFor(id),
|
|
464
|
+
...opts.render?.[id],
|
|
465
|
+
};
|
|
466
|
+
applyRenderAttrs(body, id, Object.keys(authored).length ? authored : undefined, lens, width, limit);
|
|
446
467
|
bodies.push(body);
|
|
447
468
|
}
|
|
448
469
|
// Stars. With the deep pack loaded and a dark sky, pin the complete naked-eye
|
|
@@ -873,6 +894,8 @@ function buildPrompt(bodies, offFrame, directives, starfield) {
|
|
|
873
894
|
if (b.brightnessHint)
|
|
874
895
|
parts.push(b.brightnessHint);
|
|
875
896
|
}
|
|
897
|
+
if (b.color)
|
|
898
|
+
parts.push(`colour ${b.color}`);
|
|
876
899
|
let line = `- ${parts.join(", ")}`;
|
|
877
900
|
if (b.note)
|
|
878
901
|
line += `. ${b.note}`;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caelus synthetic ephemeris -- give imaginary bodies motion, not just a
|
|
3
|
+
* single longitude.
|
|
4
|
+
*
|
|
5
|
+
* {@link compileForm} places an authored body at one static longitude
|
|
6
|
+
* (a snapshot). A sky that does not move is dead: no transits, no returns, no
|
|
7
|
+
* seasons. This module makes a body's position a deterministic **function of
|
|
8
|
+
* time** from authored parameters, so every derived computation — transits,
|
|
9
|
+
* returns, retrograde, conjunctions, phases, SkyView — works on it unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Three tiers, increasing in physics:
|
|
12
|
+
* - **placement** — a fixed longitude (parity with `compileForm`; effectively a
|
|
13
|
+
* body on the celestial sphere that never moves).
|
|
14
|
+
* - **periodic** — `lon(t) = wrap360(phaseDeg + 360·(t − epoch)/periodDays)`.
|
|
15
|
+
* Cheap, no real physics, but yields returns, cyclic transits, and (viewed
|
|
16
|
+
* from an inner observer body) apparent retrograde. Covers ~all
|
|
17
|
+
* narrative/game needs.
|
|
18
|
+
* - **kepler** — constant Keplerian elements solved each instant
|
|
19
|
+
* (`{ a, e, i, node, peri, M0, periodDays }`), giving heliocentric ecliptic
|
|
20
|
+
* (x, y, z, lon, lat, r); with a designated **observer body** the positions
|
|
21
|
+
* become geocentric/apparent, enabling true retrograde, phases and
|
|
22
|
+
* occultation.
|
|
23
|
+
*
|
|
24
|
+
* Determinism is non-negotiable: every export is a pure function of
|
|
25
|
+
* `(system, t)`. No randomness, no wall-clock reads — identical inputs yield
|
|
26
|
+
* identical outputs, like the rest of Caelus.
|
|
27
|
+
*
|
|
28
|
+
* Two ways to consume a system:
|
|
29
|
+
* 1. {@link syntheticPositions} / {@link syntheticEphemeris} — the self-
|
|
30
|
+
* contained *world frame*. An `observer` body, if set, is the vantage point;
|
|
31
|
+
* longitudes are in the world's own ecliptic, decoupled from Earth. This is
|
|
32
|
+
* the canonical position-over-time contract.
|
|
33
|
+
* 2. {@link registerSyntheticSystem} — drop the bodies into a real {@link
|
|
34
|
+
* Engine} via {@link Engine.registerSource}, so `transits`, `returns`,
|
|
35
|
+
* `skyView`, `relational` etc. consume them through their existing `Engine`
|
|
36
|
+
* interfaces with zero changes. Here the vantage point is real Earth (the
|
|
37
|
+
* system's heliocentre maps to the Sun), so the synthetic `observer` does
|
|
38
|
+
* not apply — it is "imaginary bodies in our sky".
|
|
39
|
+
*/
|
|
40
|
+
import { XyzSource } from "./core.js";
|
|
41
|
+
/** A single authored body. Its `id` is how every consumer (charts, transits,
|
|
42
|
+
* SkyView) refers to it. */
|
|
43
|
+
export type SyntheticBody =
|
|
44
|
+
/** A fixed longitude that never moves — parity with `compileForm`. */
|
|
45
|
+
{
|
|
46
|
+
id: string;
|
|
47
|
+
mode: "placement";
|
|
48
|
+
lonDeg: number;
|
|
49
|
+
}
|
|
50
|
+
/** Uniform angular motion: `lon(t) = phaseDeg + 360·(t − epoch)/periodDays`.
|
|
51
|
+
* `epoch` defaults to `0`. */
|
|
52
|
+
| {
|
|
53
|
+
id: string;
|
|
54
|
+
mode: "periodic";
|
|
55
|
+
periodDays: number;
|
|
56
|
+
phaseDeg: number;
|
|
57
|
+
epoch?: number;
|
|
58
|
+
}
|
|
59
|
+
/** Constant Keplerian elements. Angles `i`, `node`, `peri`, `M0` in **degrees**;
|
|
60
|
+
* `a` in arbitrary length units (consistent within a system); `e` in `[0, 1)`;
|
|
61
|
+
* `epoch` defaults to `0`. */
|
|
62
|
+
| {
|
|
63
|
+
id: string;
|
|
64
|
+
mode: "kepler";
|
|
65
|
+
a: number;
|
|
66
|
+
e: number;
|
|
67
|
+
i: number;
|
|
68
|
+
node: number;
|
|
69
|
+
peri: number;
|
|
70
|
+
M0: number;
|
|
71
|
+
periodDays: number;
|
|
72
|
+
epoch?: number;
|
|
73
|
+
};
|
|
74
|
+
/** Render attributes for a synthetic body — how an imaginary body should *look*
|
|
75
|
+
* in a SkyView frame. The engine owns position; these own appearance, so a body
|
|
76
|
+
* stays visually consistent across {@link skyViewSequence} frames. */
|
|
77
|
+
export interface SyntheticRender {
|
|
78
|
+
/** Apparent angular diameter in degrees (the Moon is ~0.5°). */
|
|
79
|
+
sizeDeg?: number;
|
|
80
|
+
/** Apparent visual magnitude (smaller is brighter; Sirius is ~-1.5). */
|
|
81
|
+
magnitude?: number;
|
|
82
|
+
/** A colour hint, e.g. a CSS colour or evocative name ("pale gold"). */
|
|
83
|
+
color?: string;
|
|
84
|
+
}
|
|
85
|
+
/** An authored celestial system: a set of {@link SyntheticBody} that move
|
|
86
|
+
* together, an optional `observer` body that fixes the vantage point, and
|
|
87
|
+
* optional per-body {@link SyntheticRender} attributes. */
|
|
88
|
+
export interface SyntheticSystem {
|
|
89
|
+
id: string;
|
|
90
|
+
bodies: SyntheticBody[];
|
|
91
|
+
/** Body id of the vantage point. When set, {@link syntheticPositions} returns
|
|
92
|
+
* geocentric/apparent positions seen from this body (it sees itself at the
|
|
93
|
+
* origin, so it is reported at its own heliocentric place). When unset,
|
|
94
|
+
* positions are heliocentric. */
|
|
95
|
+
observer?: string;
|
|
96
|
+
/** Render attributes keyed by body id. */
|
|
97
|
+
render?: Record<string, SyntheticRender>;
|
|
98
|
+
}
|
|
99
|
+
/** A body's position at one instant, in degrees and system length units. */
|
|
100
|
+
export interface SyntheticPosition {
|
|
101
|
+
/** Ecliptic longitude in degrees, `[0, 360)`. */
|
|
102
|
+
lonDeg: number;
|
|
103
|
+
/** Ecliptic latitude in degrees. */
|
|
104
|
+
latDeg: number;
|
|
105
|
+
/** Distance from the vantage point (heliocentric, or from the observer body
|
|
106
|
+
* when one is set), in the system's length units. */
|
|
107
|
+
r: number;
|
|
108
|
+
}
|
|
109
|
+
/** Why a system is unsatisfiable, if it is — the `impossible`/`residual` honesty
|
|
110
|
+
* pattern, mirroring {@link CompiledForm}. A system with problems
|
|
111
|
+
* still computes for the bodies that are individually valid, but flags the rest
|
|
112
|
+
* rather than silently producing garbage. */
|
|
113
|
+
export interface SyntheticDiagnosis {
|
|
114
|
+
/** `true` when at least one problem makes the system ill-defined. */
|
|
115
|
+
impossible: boolean;
|
|
116
|
+
/** One human-readable line per problem (empty when the system is sound). */
|
|
117
|
+
problems: string[];
|
|
118
|
+
}
|
|
119
|
+
/** A pluggable position-over-time source: the same shape every derived
|
|
120
|
+
* computation needs from a body. Returned by {@link syntheticEphemeris}. */
|
|
121
|
+
export interface BodyPositionSource extends SyntheticDiagnosis {
|
|
122
|
+
/** The body ids this source can place. */
|
|
123
|
+
bodies(): string[];
|
|
124
|
+
/** Apparent ecliptic longitude (degrees, `[0, 360)`) at time `t`. */
|
|
125
|
+
longitude(id: string, t: number): number;
|
|
126
|
+
/** Full position at time `t`, with longitude speed and a retrograde flag
|
|
127
|
+
* derived by central difference — matching the {@link Position}
|
|
128
|
+
* contract that real bodies satisfy. */
|
|
129
|
+
position(id: string, t: number): SyntheticPosition & {
|
|
130
|
+
speed: number;
|
|
131
|
+
retrograde: boolean;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Check an authored system for the ways it can be ill-defined — duplicate ids, a
|
|
136
|
+
* dangling `observer`, non-positive periods, or out-of-range eccentricity — and
|
|
137
|
+
* report each, in the `impossible`/`residual` honesty style. Pure.
|
|
138
|
+
*
|
|
139
|
+
* @param sys The system to check.
|
|
140
|
+
* @returns A {@link SyntheticDiagnosis}; `impossible` is `true` when any problem
|
|
141
|
+
* is found.
|
|
142
|
+
*/
|
|
143
|
+
export declare function validateSyntheticSystem(sys: SyntheticSystem): SyntheticDiagnosis;
|
|
144
|
+
/**
|
|
145
|
+
* The heliocentric xyz source for one authored body — the per-body engine behind
|
|
146
|
+
* every tier, and the exact object {@link Engine.registerSource} consumes.
|
|
147
|
+
*
|
|
148
|
+
* - **placement** sits on a very distant sphere so its direction (longitude) is
|
|
149
|
+
* fixed and parallax-free from any vantage.
|
|
150
|
+
* - **periodic** is a circular, coplanar orbit (`e = 0`, `i = 0`) whose radius
|
|
151
|
+
* follows Kepler's third law in the system's units (`a = periodDays^(2/3)`),
|
|
152
|
+
* so an inner body both moves faster and orbits tighter — making outer bodies
|
|
153
|
+
* show apparent retrograde near opposition, just like the real sky. Its
|
|
154
|
+
* heliocentric longitude is exactly `phaseDeg + 360·(t − epoch)/periodDays`.
|
|
155
|
+
* - **kepler** is the full constant-element solver ({@link KeplerOrbit}).
|
|
156
|
+
*
|
|
157
|
+
* @param body The authored body.
|
|
158
|
+
* @returns An {@link XyzSource} yielding heliocentric ecliptic xyz at time `t`.
|
|
159
|
+
*/
|
|
160
|
+
export declare function bodySource(body: SyntheticBody): XyzSource;
|
|
161
|
+
/**
|
|
162
|
+
* Build the heliocentric {@link XyzSource} for every body in a system, keyed by
|
|
163
|
+
* id — the raw material for both the world-frame contract and engine
|
|
164
|
+
* registration.
|
|
165
|
+
*
|
|
166
|
+
* @param sys The authored system.
|
|
167
|
+
* @returns One source per body id.
|
|
168
|
+
*/
|
|
169
|
+
export declare function syntheticSources(sys: SyntheticSystem): Record<string, XyzSource>;
|
|
170
|
+
/**
|
|
171
|
+
* Positions of every body at one instant — the core position-over-time
|
|
172
|
+
* contract. Pure: a function of `(sys, t)` only.
|
|
173
|
+
*
|
|
174
|
+
* When `sys.observer` is set, each other body's position is geocentric/apparent
|
|
175
|
+
* as seen from the observer body (its heliocentric vector subtracted), so an
|
|
176
|
+
* outer body shows a real apparent retrograde arc around opposition. The
|
|
177
|
+
* observer body itself is reported at its own heliocentric place. With no
|
|
178
|
+
* observer the positions are heliocentric.
|
|
179
|
+
*
|
|
180
|
+
* @param sys The authored system.
|
|
181
|
+
* @param tDays The instant, in the same day units as each body's `periodDays`
|
|
182
|
+
* and `epoch`.
|
|
183
|
+
* @returns Position per body id (`lonDeg`, `latDeg`, `r`).
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* const sys: SyntheticSystem = {
|
|
187
|
+
* id: "twin-moons",
|
|
188
|
+
* bodies: [
|
|
189
|
+
* { id: "fast", mode: "periodic", periodDays: 20, phaseDeg: 0 },
|
|
190
|
+
* { id: "slow", mode: "periodic", periodDays: 80, phaseDeg: 0 },
|
|
191
|
+
* ],
|
|
192
|
+
* };
|
|
193
|
+
* syntheticPositions(sys, 0).fast.lonDeg; // 0
|
|
194
|
+
* syntheticPositions(sys, 5).fast.lonDeg; // 90 (a quarter of its 20-day period)
|
|
195
|
+
* ```
|
|
196
|
+
*/
|
|
197
|
+
export declare function syntheticPositions(sys: SyntheticSystem, tDays: number): Record<string, SyntheticPosition>;
|
|
198
|
+
/**
|
|
199
|
+
* A {@link BodyPositionSource} over an authored system: the world-frame view as
|
|
200
|
+
* a reusable object, with longitude speed and a retrograde flag so it satisfies
|
|
201
|
+
* the same contract real ephemeris bodies do. Carries the system's
|
|
202
|
+
* {@link validateSyntheticSystem} diagnosis (`impossible`/`problems`) — the
|
|
203
|
+
* honesty pattern — and still serves the bodies that are individually valid.
|
|
204
|
+
*
|
|
205
|
+
* @param sys The authored system.
|
|
206
|
+
* @returns A position source: `bodies()`, `longitude(id, t)`, `position(id, t)`,
|
|
207
|
+
* plus `impossible`/`problems`.
|
|
208
|
+
*/
|
|
209
|
+
export declare function syntheticEphemeris(sys: SyntheticSystem): BodyPositionSource;
|
|
210
|
+
/** A type guard for the surface {@link registerSyntheticSystem} needs, so the
|
|
211
|
+
* synthetic module does not import the heavy {@link Engine} class. */
|
|
212
|
+
export interface SourceRegistrar {
|
|
213
|
+
registerSource(id: string, source: XyzSource): unknown;
|
|
214
|
+
registerRender?(id: string, render: SyntheticRender): unknown;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a system's bodies on a real {@link Engine} so `transits`, `returns`,
|
|
218
|
+
* retrograde detection, `skyView`, `relational` — every computation built on
|
|
219
|
+
* `Engine.position`/`Engine.longitude` — consume them unchanged. The bodies are
|
|
220
|
+
* placed heliocentrically in the real solar system and viewed from real Earth,
|
|
221
|
+
* so the synthetic `observer` is *not* applied here (use
|
|
222
|
+
* {@link syntheticPositions} for the self-contained world frame). Each body's
|
|
223
|
+
* `epoch` is then interpreted as a TT Julian Day.
|
|
224
|
+
*
|
|
225
|
+
* @param engine The engine to register onto (anything with `registerSource`).
|
|
226
|
+
* @param sys The authored system.
|
|
227
|
+
* @returns The ids registered.
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* registerSyntheticSystem(engine, {
|
|
231
|
+
* id: "nemesis", bodies: [{ id: "nemesis", mode: "kepler", a: 520, e: 0.1,
|
|
232
|
+
* i: 5, node: 0, peri: 0, M0: 0, periodDays: 4_300_000, epoch: 2451545 }],
|
|
233
|
+
* });
|
|
234
|
+
* returns(engine, "nemesis", natalJd, jdStart, jdEnd); // works, zero changes
|
|
235
|
+
* engine.position("nemesis", jd).retrograde; // apparent, from Earth
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export declare function registerSyntheticSystem(engine: SourceRegistrar, sys: SyntheticSystem): string[];
|
|
239
|
+
/**
|
|
240
|
+
* The {@link SyntheticRender} attributes for a body id, or `undefined` when none
|
|
241
|
+
* were authored. The accessor SkyView/`skyViewSequence` reads so an imaginary
|
|
242
|
+
* body keeps a consistent size, brightness and colour across frames — the engine
|
|
243
|
+
* owns position, this owns appearance.
|
|
244
|
+
*
|
|
245
|
+
* @param sys The authored system.
|
|
246
|
+
* @param id A body id.
|
|
247
|
+
* @returns The render attributes, or `undefined`.
|
|
248
|
+
*/
|
|
249
|
+
export declare function syntheticRender(sys: SyntheticSystem, id: string): SyntheticRender | undefined;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* caelus synthetic ephemeris -- give imaginary bodies motion, not just a
|
|
3
|
+
* single longitude.
|
|
4
|
+
*
|
|
5
|
+
* {@link compileForm} places an authored body at one static longitude
|
|
6
|
+
* (a snapshot). A sky that does not move is dead: no transits, no returns, no
|
|
7
|
+
* seasons. This module makes a body's position a deterministic **function of
|
|
8
|
+
* time** from authored parameters, so every derived computation — transits,
|
|
9
|
+
* returns, retrograde, conjunctions, phases, SkyView — works on it unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Three tiers, increasing in physics:
|
|
12
|
+
* - **placement** — a fixed longitude (parity with `compileForm`; effectively a
|
|
13
|
+
* body on the celestial sphere that never moves).
|
|
14
|
+
* - **periodic** — `lon(t) = wrap360(phaseDeg + 360·(t − epoch)/periodDays)`.
|
|
15
|
+
* Cheap, no real physics, but yields returns, cyclic transits, and (viewed
|
|
16
|
+
* from an inner observer body) apparent retrograde. Covers ~all
|
|
17
|
+
* narrative/game needs.
|
|
18
|
+
* - **kepler** — constant Keplerian elements solved each instant
|
|
19
|
+
* (`{ a, e, i, node, peri, M0, periodDays }`), giving heliocentric ecliptic
|
|
20
|
+
* (x, y, z, lon, lat, r); with a designated **observer body** the positions
|
|
21
|
+
* become geocentric/apparent, enabling true retrograde, phases and
|
|
22
|
+
* occultation.
|
|
23
|
+
*
|
|
24
|
+
* Determinism is non-negotiable: every export is a pure function of
|
|
25
|
+
* `(system, t)`. No randomness, no wall-clock reads — identical inputs yield
|
|
26
|
+
* identical outputs, like the rest of Caelus.
|
|
27
|
+
*
|
|
28
|
+
* Two ways to consume a system:
|
|
29
|
+
* 1. {@link syntheticPositions} / {@link syntheticEphemeris} — the self-
|
|
30
|
+
* contained *world frame*. An `observer` body, if set, is the vantage point;
|
|
31
|
+
* longitudes are in the world's own ecliptic, decoupled from Earth. This is
|
|
32
|
+
* the canonical position-over-time contract.
|
|
33
|
+
* 2. {@link registerSyntheticSystem} — drop the bodies into a real {@link
|
|
34
|
+
* Engine} via {@link Engine.registerSource}, so `transits`, `returns`,
|
|
35
|
+
* `skyView`, `relational` etc. consume them through their existing `Engine`
|
|
36
|
+
* interfaces with zero changes. Here the vantage point is real Earth (the
|
|
37
|
+
* system's heliocentre maps to the Sun), so the synthetic `observer` does
|
|
38
|
+
* not apply — it is "imaginary bodies in our sky".
|
|
39
|
+
*/
|
|
40
|
+
import { DEG, mod, KeplerOrbit } from "./core.js";
|
|
41
|
+
const TWO_PI = 2 * Math.PI;
|
|
42
|
+
// ----------------------------------------------------------------- validation
|
|
43
|
+
/**
|
|
44
|
+
* Check an authored system for the ways it can be ill-defined — duplicate ids, a
|
|
45
|
+
* dangling `observer`, non-positive periods, or out-of-range eccentricity — and
|
|
46
|
+
* report each, in the `impossible`/`residual` honesty style. Pure.
|
|
47
|
+
*
|
|
48
|
+
* @param sys The system to check.
|
|
49
|
+
* @returns A {@link SyntheticDiagnosis}; `impossible` is `true` when any problem
|
|
50
|
+
* is found.
|
|
51
|
+
*/
|
|
52
|
+
export function validateSyntheticSystem(sys) {
|
|
53
|
+
const problems = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const b of sys.bodies) {
|
|
56
|
+
if (seen.has(b.id))
|
|
57
|
+
problems.push(`duplicate body id '${b.id}'`);
|
|
58
|
+
seen.add(b.id);
|
|
59
|
+
if (b.mode === "periodic" && !(b.periodDays > 0)) {
|
|
60
|
+
problems.push(`body '${b.id}': periodDays must be > 0 (got ${b.periodDays})`);
|
|
61
|
+
}
|
|
62
|
+
if (b.mode === "kepler") {
|
|
63
|
+
if (!(b.periodDays > 0))
|
|
64
|
+
problems.push(`body '${b.id}': periodDays must be > 0 (got ${b.periodDays})`);
|
|
65
|
+
if (!(b.a > 0))
|
|
66
|
+
problems.push(`body '${b.id}': a must be > 0 (got ${b.a})`);
|
|
67
|
+
if (!(b.e >= 0 && b.e < 1))
|
|
68
|
+
problems.push(`body '${b.id}': e must be in [0, 1) (got ${b.e})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (sys.observer !== undefined && !seen.has(sys.observer)) {
|
|
72
|
+
problems.push(`observer '${sys.observer}' is not a body in the system`);
|
|
73
|
+
}
|
|
74
|
+
return { impossible: problems.length > 0, problems };
|
|
75
|
+
}
|
|
76
|
+
// -------------------------------------------------------------------- sources
|
|
77
|
+
/**
|
|
78
|
+
* The heliocentric xyz source for one authored body — the per-body engine behind
|
|
79
|
+
* every tier, and the exact object {@link Engine.registerSource} consumes.
|
|
80
|
+
*
|
|
81
|
+
* - **placement** sits on a very distant sphere so its direction (longitude) is
|
|
82
|
+
* fixed and parallax-free from any vantage.
|
|
83
|
+
* - **periodic** is a circular, coplanar orbit (`e = 0`, `i = 0`) whose radius
|
|
84
|
+
* follows Kepler's third law in the system's units (`a = periodDays^(2/3)`),
|
|
85
|
+
* so an inner body both moves faster and orbits tighter — making outer bodies
|
|
86
|
+
* show apparent retrograde near opposition, just like the real sky. Its
|
|
87
|
+
* heliocentric longitude is exactly `phaseDeg + 360·(t − epoch)/periodDays`.
|
|
88
|
+
* - **kepler** is the full constant-element solver ({@link KeplerOrbit}).
|
|
89
|
+
*
|
|
90
|
+
* @param body The authored body.
|
|
91
|
+
* @returns An {@link XyzSource} yielding heliocentric ecliptic xyz at time `t`.
|
|
92
|
+
*/
|
|
93
|
+
export function bodySource(body) {
|
|
94
|
+
if (body.mode === "placement") {
|
|
95
|
+
const R = 1e9;
|
|
96
|
+
const lam = body.lonDeg * DEG;
|
|
97
|
+
const v = [R * Math.cos(lam), R * Math.sin(lam), 0];
|
|
98
|
+
return { xyz: () => v };
|
|
99
|
+
}
|
|
100
|
+
if (body.mode === "periodic") {
|
|
101
|
+
const els = {
|
|
102
|
+
a: Math.cbrt(body.periodDays * body.periodDays),
|
|
103
|
+
e: 0, i: 0, node: 0, peri: 0,
|
|
104
|
+
M0: body.phaseDeg * DEG, n: TWO_PI / body.periodDays,
|
|
105
|
+
};
|
|
106
|
+
return new KeplerOrbit(els, body.epoch ?? 0);
|
|
107
|
+
}
|
|
108
|
+
const els = {
|
|
109
|
+
a: body.a, e: body.e, i: body.i * DEG, node: body.node * DEG,
|
|
110
|
+
peri: body.peri * DEG, M0: body.M0 * DEG, n: TWO_PI / body.periodDays,
|
|
111
|
+
};
|
|
112
|
+
return new KeplerOrbit(els, body.epoch ?? 0);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Build the heliocentric {@link XyzSource} for every body in a system, keyed by
|
|
116
|
+
* id — the raw material for both the world-frame contract and engine
|
|
117
|
+
* registration.
|
|
118
|
+
*
|
|
119
|
+
* @param sys The authored system.
|
|
120
|
+
* @returns One source per body id.
|
|
121
|
+
*/
|
|
122
|
+
export function syntheticSources(sys) {
|
|
123
|
+
const out = {};
|
|
124
|
+
for (const b of sys.bodies)
|
|
125
|
+
out[b.id] = bodySource(b);
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function vecToPos(v) {
|
|
129
|
+
const r = Math.hypot(v[0], v[1], v[2]);
|
|
130
|
+
return {
|
|
131
|
+
lonDeg: mod(Math.atan2(v[1], v[0]) / DEG, 360),
|
|
132
|
+
latDeg: r === 0 ? 0 : Math.asin(v[2] / r) / DEG,
|
|
133
|
+
r,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// ------------------------------------------------------------------- contract
|
|
137
|
+
/**
|
|
138
|
+
* Positions of every body at one instant — the core position-over-time
|
|
139
|
+
* contract. Pure: a function of `(sys, t)` only.
|
|
140
|
+
*
|
|
141
|
+
* When `sys.observer` is set, each other body's position is geocentric/apparent
|
|
142
|
+
* as seen from the observer body (its heliocentric vector subtracted), so an
|
|
143
|
+
* outer body shows a real apparent retrograde arc around opposition. The
|
|
144
|
+
* observer body itself is reported at its own heliocentric place. With no
|
|
145
|
+
* observer the positions are heliocentric.
|
|
146
|
+
*
|
|
147
|
+
* @param sys The authored system.
|
|
148
|
+
* @param tDays The instant, in the same day units as each body's `periodDays`
|
|
149
|
+
* and `epoch`.
|
|
150
|
+
* @returns Position per body id (`lonDeg`, `latDeg`, `r`).
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* const sys: SyntheticSystem = {
|
|
154
|
+
* id: "twin-moons",
|
|
155
|
+
* bodies: [
|
|
156
|
+
* { id: "fast", mode: "periodic", periodDays: 20, phaseDeg: 0 },
|
|
157
|
+
* { id: "slow", mode: "periodic", periodDays: 80, phaseDeg: 0 },
|
|
158
|
+
* ],
|
|
159
|
+
* };
|
|
160
|
+
* syntheticPositions(sys, 0).fast.lonDeg; // 0
|
|
161
|
+
* syntheticPositions(sys, 5).fast.lonDeg; // 90 (a quarter of its 20-day period)
|
|
162
|
+
* ```
|
|
163
|
+
*/
|
|
164
|
+
export function syntheticPositions(sys, tDays) {
|
|
165
|
+
const sources = syntheticSources(sys);
|
|
166
|
+
const obs = sys.observer ? sources[sys.observer]?.xyz(tDays) ?? null : null;
|
|
167
|
+
const out = {};
|
|
168
|
+
for (const b of sys.bodies) {
|
|
169
|
+
const v = sources[b.id].xyz(tDays);
|
|
170
|
+
if (obs && sys.observer !== b.id) {
|
|
171
|
+
out[b.id] = vecToPos([v[0] - obs[0], v[1] - obs[1], v[2] - obs[2]]);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
out[b.id] = vecToPos(v);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* A {@link BodyPositionSource} over an authored system: the world-frame view as
|
|
181
|
+
* a reusable object, with longitude speed and a retrograde flag so it satisfies
|
|
182
|
+
* the same contract real ephemeris bodies do. Carries the system's
|
|
183
|
+
* {@link validateSyntheticSystem} diagnosis (`impossible`/`problems`) — the
|
|
184
|
+
* honesty pattern — and still serves the bodies that are individually valid.
|
|
185
|
+
*
|
|
186
|
+
* @param sys The authored system.
|
|
187
|
+
* @returns A position source: `bodies()`, `longitude(id, t)`, `position(id, t)`,
|
|
188
|
+
* plus `impossible`/`problems`.
|
|
189
|
+
*/
|
|
190
|
+
export function syntheticEphemeris(sys) {
|
|
191
|
+
const { impossible, problems } = validateSyntheticSystem(sys);
|
|
192
|
+
const sources = syntheticSources(sys);
|
|
193
|
+
const ids = sys.bodies.map((b) => b.id);
|
|
194
|
+
const apparent = (id, t) => {
|
|
195
|
+
const v = sources[id].xyz(t);
|
|
196
|
+
if (sys.observer && sys.observer !== id) {
|
|
197
|
+
const o = sources[sys.observer].xyz(t);
|
|
198
|
+
return vecToPos([v[0] - o[0], v[1] - o[1], v[2] - o[2]]);
|
|
199
|
+
}
|
|
200
|
+
return vecToPos(v);
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
impossible, problems,
|
|
204
|
+
bodies: () => [...ids],
|
|
205
|
+
longitude: (id, t) => apparent(id, t).lonDeg,
|
|
206
|
+
position: (id, t) => {
|
|
207
|
+
const p = apparent(id, t);
|
|
208
|
+
const h = 0.05; // days; central difference, matching Engine.position
|
|
209
|
+
const l0 = apparent(id, t - h).lonDeg;
|
|
210
|
+
const l1 = apparent(id, t + h).lonDeg;
|
|
211
|
+
const speed = (mod(l1 - l0 + 540, 360) - 180) / (2 * h);
|
|
212
|
+
return { ...p, speed, retrograde: speed < 0 };
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Register a system's bodies on a real {@link Engine} so `transits`, `returns`,
|
|
218
|
+
* retrograde detection, `skyView`, `relational` — every computation built on
|
|
219
|
+
* `Engine.position`/`Engine.longitude` — consume them unchanged. The bodies are
|
|
220
|
+
* placed heliocentrically in the real solar system and viewed from real Earth,
|
|
221
|
+
* so the synthetic `observer` is *not* applied here (use
|
|
222
|
+
* {@link syntheticPositions} for the self-contained world frame). Each body's
|
|
223
|
+
* `epoch` is then interpreted as a TT Julian Day.
|
|
224
|
+
*
|
|
225
|
+
* @param engine The engine to register onto (anything with `registerSource`).
|
|
226
|
+
* @param sys The authored system.
|
|
227
|
+
* @returns The ids registered.
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* registerSyntheticSystem(engine, {
|
|
231
|
+
* id: "nemesis", bodies: [{ id: "nemesis", mode: "kepler", a: 520, e: 0.1,
|
|
232
|
+
* i: 5, node: 0, peri: 0, M0: 0, periodDays: 4_300_000, epoch: 2451545 }],
|
|
233
|
+
* });
|
|
234
|
+
* returns(engine, "nemesis", natalJd, jdStart, jdEnd); // works, zero changes
|
|
235
|
+
* engine.position("nemesis", jd).retrograde; // apparent, from Earth
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
export function registerSyntheticSystem(engine, sys) {
|
|
239
|
+
const sources = syntheticSources(sys);
|
|
240
|
+
for (const b of sys.bodies) {
|
|
241
|
+
engine.registerSource(b.id, sources[b.id]);
|
|
242
|
+
const render = sys.render?.[b.id];
|
|
243
|
+
if (render)
|
|
244
|
+
engine.registerRender?.(b.id, render);
|
|
245
|
+
}
|
|
246
|
+
return sys.bodies.map((b) => b.id);
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* The {@link SyntheticRender} attributes for a body id, or `undefined` when none
|
|
250
|
+
* were authored. The accessor SkyView/`skyViewSequence` reads so an imaginary
|
|
251
|
+
* body keeps a consistent size, brightness and colour across frames — the engine
|
|
252
|
+
* owns position, this owns appearance.
|
|
253
|
+
*
|
|
254
|
+
* @param sys The authored system.
|
|
255
|
+
* @param id A body id.
|
|
256
|
+
* @returns The render attributes, or `undefined`.
|
|
257
|
+
*/
|
|
258
|
+
export function syntheticRender(sys, id) {
|
|
259
|
+
return sys.render?.[id];
|
|
260
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caelus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Clean-room TypeScript astrology computation: ephemeris, charts, events, timing techniques, Vedic methods, and citable chart facts. MIT, no AGPL or ephemeris files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/src/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
],
|
|
22
22
|
"scripts": {
|
|
23
23
|
"build": "tsc",
|
|
24
|
-
"test": "node dist/test/golden.test.js && node dist/test/skyview.test.js"
|
|
24
|
+
"test": "node dist/test/golden.test.js && node dist/test/skyview.test.js && node dist/test/synthetic.test.js"
|
|
25
25
|
},
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"publishConfig": {
|