Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
# @suaveplan/geo-planet - Configurable planet surface geometry from PlanetConfig — ellipsoid, atmosphere, satellites, and orbital elements.
## Summary
Configurable planet surface geometry from `PlanetConfig` — ellipsoid, atmosphere, satellites, and orbital elements.
<!-- begin:auto-docs -->
## Documentation tree
- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
<!-- end:auto-docs --> - [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [atmospheric](./src/atmospheric/atmospheric.md) — <!-- begin:auto-docs --> _(no submodules)_- [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_ - [planet-surface](./src/planet-surface/planet-surface.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/types/schemas.md) — The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the…
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
## Install
```bash
bun add @suaveplan/geo-planet
```
## Quick start
```ts
import { PlanetSurface, PlanetConfigSchema } from "@suaveplan/geo-planet";
const config = PlanetConfigSchema.parse({
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
});
const surface = new PlanetSurface(config);
console.log(surface.distance(a, b, "kilometre"));
```
## Modules
- [`src/planet-surface/planet-surface.md`](src/planet-surface/planet-surface.md)
- [`src/atmospheric/atmospheric.md`](src/atmospheric/atmospheric.md)
- [`src/types/types.md`](src/types/types.md)

View File

@@ -0,0 +1,65 @@
{
"name": "@suaveplan/geo-planet",
"version": "0.1.0",
"type": "module",
"sideEffects": false,
"description": "Configurable planet surface geometry from PlanetConfig — ellipsoid, atmosphere, satellites, and orbital elements.",
"license": "MIT",
"author": "Suaveplan Team",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
},
"./atmospheric/atmospheric": {
"import": { "types": "./dist/atmospheric/atmospheric.d.ts", "default": "./dist/atmospheric/atmospheric.js" },
"require": { "types": "./dist/atmospheric/atmospheric.d.ts", "default": "./dist/atmospheric/atmospheric.js" }
},
"./planet-surface/planet-surface": {
"import": { "types": "./dist/planet-surface/planet-surface.d.ts", "default": "./dist/planet-surface/planet-surface.js" },
"require": { "types": "./dist/planet-surface/planet-surface.d.ts", "default": "./dist/planet-surface/planet-surface.js" }
},
"./types/types": {
"import": { "types": "./dist/types/types.d.ts", "default": "./dist/types/types.js" },
"require": { "types": "./dist/types/types.d.ts", "default": "./dist/types/types.js" }
}
},
"scripts": {
"build": "vite build",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage --passWithNoTests",
"lint": "bunx biome check src/",
"lint:fix": "bunx biome lint --fix --unsafe --max-diagnostics 500 src/",
"format": "bunx biome format --write --max-diagnostics 500 src/",
"check": "bunx biome check --write --unsafe --max-diagnostics 500 src/",
"typecheck": "bunx tsc --noEmit --composite false -p tsconfig.lib.json",
"docs:check": "bun run ../../../../scripts/docs-check.ts ."
},
"dependencies": {
"@suaveplan/error": "workspace:^",
"@suaveplan/geo": "workspace:^",
"@suaveplan/types": "workspace:^",
"tslib": "catalog:build",
"zod": "catalog:forms"
},
"devDependencies": {
"@suaveplan/testing": "workspace:^",
"@types/node": "catalog:",
"@vitest/coverage-istanbul": "catalog:testing",
"fast-check": "catalog:testing",
"typescript": "catalog:",
"vite": "catalog:build",
"vite-plugin-dts": "catalog:build",
"vitest": "catalog:testing"
},
"publishConfig": {
"registry": "https://npmjs.fq.io/",
"access": "public"
},
"files": ["dist", "src", "README.md"]
}

View File

@@ -0,0 +1,86 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `atmospheric` module provides the two physics primitives every planetary renderer, sky-colour solver, and observation-quality estimator needs: the local density of an atmospheric column at altitude, and the relative path length light travels through that column for a given elevation angle. Both functions accept a `PlanetConfig` so the same code path serves Earth, Mars, Titan, or any other modelled body.
These primitives are intentionally pure and free of vendor coupling so they can be reused by `@suaveplan/universe`, `@suaveplan/gaming-environment-lighting`, server-side astronomical computations, and offline analysis without dragging a graphics runtime into the call site.
## Features
- `atmosphericDensity(planet, altitudeMetres)` — exponential isothermal scale-height model that returns `0` for airless bodies
- `airMass(altitudeDeg)` — Kasten–Young 1989 air-mass approximation valid from zenith down to the horizon
- Numerical-stable horizon handling: negative altitudes are clamped to `0` so the formula remains finite at the bound
- Pure functions: no I/O, no side effects, no global state — safe for `Worker` threads, SSR, and deterministic tests
## Usage
### Basic Example
```ts
import { atmosphericDensity } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
const earth: PlanetConfig = {
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: {
surfacePressure: 101_325,
scaleHeight: 8_500,
rayleighCoefficient: 5.8e-6,
mieCoefficient: 21e-6,
extinctionPerAirMass: 0.13,
},
satellites: [],
};
const surface = atmosphericDensity(earth, 0); // 101325 Pa
const cruise = atmosphericDensity(earth, 11_000); // ~27 kPa
```
### Advanced Example
```ts
import { airMass, atmosphericDensity } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
function transmittance(
planet: PlanetConfig,
altitudeMetres: number,
sunElevationDeg: number,
): number {
if (!planet.atmosphere) return 1;
const density = atmosphericDensity(planet, altitudeMetres);
const am = airMass(sunElevationDeg);
const tau = planet.atmosphere.extinctionPerAirMass * am;
return Math.exp(-tau) * (density / planet.atmosphere.surfacePressure);
}
// Use at every camera step in a sky shader, or to score
// observability for ground-station pass scheduling.
```
## API Reference
### `atmosphericDensity(planet: PlanetConfig, altitudeMetres: number): number`
Returns the local pressure (`Pa`) using `p₀ · exp(-h / H)` where `p₀` is `planet.atmosphere.surfacePressure` and `H` is `planet.atmosphere.scaleHeight`. Returns `0` when `planet.atmosphere === null`. No clamping is applied to altitude; negative altitudes (subterranean, sub-sea-level) yield pressures above `p₀`, which is the physically correct extrapolation for a barometric law.
### `airMass(altitudeDeg: number): number`
Returns the Kasten–Young air mass:
`1 / (sin(alt) + 0.50572 · (alt + 6.07995)^-1.6364)`
with `alt` in degrees. Negative altitudes are clamped to `0` so the result remains finite at the horizon (returning ≈ `38`). At zenith the return value approaches `1`.
## Implementation Notes
The density function uses the isothermal barometric equation rather than a layered ISA model because every consumer in this project treats density as a smooth analytic function for shader sampling. The Kasten–Young formulation was chosen over the simpler `1 / sin(alt)` secant model because it remains stable down to the horizon and only diverges by ≈ `0.4%` from full atmospheric ray-tracing above 5° elevation. Both functions are allocation-free hot-path code; consumers can call them once per pixel without GC pressure.

View File

@@ -0,0 +1,52 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { airMass, atmosphericDensity } from "./atmospheric.js";
describe("atmosphericDensity", () => {
const planet = {
name: "Test",
mass: 1,
radius: 1,
polarRadius: 1,
surfaceGravity: 1,
rotationPeriod: 1,
axialTilt: 0,
atmosphere: {
surfacePressure: 101325,
scaleHeight: 8500,
rayleighCoefficient: 1,
mieCoefficient: 1,
extinctionPerAirMass: 1,
},
satellites: [],
};
it("returns surface pressure at altitude 0", { timeout: 5000 }, () => {
expect(atmosphericDensity(planet, 0)).toBe(101325);
});
it("returns ~37% at scale height", { timeout: 5000 }, () => {
expect(atmosphericDensity(planet, 8500)).toBeCloseTo(
101325 / Math.E,
0
);
});
it("returns 0 for no atmosphere", { timeout: 5000 }, () => {
const noAtmo = { ...planet, atmosphere: null };
expect(atmosphericDensity(noAtmo, 0)).toBe(0);
});
});
describe("airMass", () => {
it("returns approximately 1 at zenith", { timeout: 5000 }, () => {
expect(airMass(90)).toBeCloseTo(1, 1);
});
it("returns a large value at horizon", { timeout: 5000 }, () => {
expect(airMass(0)).toBeGreaterThan(1);
});
it("returns 1 for negative altitude", { timeout: 5000 }, () => {
expect(airMass(-10)).toBeGreaterThanOrEqual(1);
});
});

View File

@@ -0,0 +1,19 @@
import type { PlanetConfig } from "../types/types.js";
export function atmosphericDensity(
planet: PlanetConfig,
altitudeMetres: number
): number {
if (!planet.atmosphere) return 0;
const p0 = planet.atmosphere.surfacePressure;
const H = planet.atmosphere.scaleHeight;
return p0 * Math.exp(-altitudeMetres / H);
}
export function airMass(altitudeDeg: number): number {
const alt = Math.max(0, altitudeDeg);
return (
1 /
(Math.sin((alt * Math.PI) / 180) + 0.50572 * (alt + 6.07995) ** -1.6364)
);
}

View File

@@ -0,0 +1,4 @@
export {
airMass,
atmosphericDensity,
} from "./atmospheric.js";

View File

@@ -0,0 +1,16 @@
export { airMass, atmosphericDensity } from "./atmospheric/index.js";
export { PlanetSurface } from "./planet-surface/index.js";
export type {
AtmosphereConfig,
OrbitalConfig,
PlanetConfig,
PlanetTextureManifest,
SatelliteConfig,
} from "./types/index.js";
export {
AtmosphereConfigSchema,
OrbitalConfigSchema,
PlanetConfigSchema,
PlanetTextureManifestSchema,
SatelliteConfigSchema,
} from "./types/index.js";

View File

@@ -0,0 +1 @@
export { PlanetSurface } from "./planet-surface.js";

View File

@@ -0,0 +1,111 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `planet-surface` module provides the `PlanetSurface` class, an oblate-ellipsoid wrapper around a `PlanetConfig` that exposes the geodetic operations every multi-planet system needs: great-circle distance, initial bearing, terminal-point projection from a heading and distance, and point-in-polygon tests. It is the single bridge between body-specific configuration (Earth, Mars, Titan, …) and the body-agnostic geometry primitives in `@suaveplan/geo`.
By deriving the ellipsoid once per `PlanetSurface` instance from the `radius` and `polarRadius` fields of `PlanetConfig`, callers never have to thread an ellipsoid through every distance call, and they cannot accidentally mix Earth-flattening into a Mars computation.
## Features
- `PlanetSurface` class — owns the planet config and the cached `Ellipsoid`
- `distance(a, b, unit?)` — geodesic great-circle distance with selectable distance unit
- `bearing(from, to)` — initial forward azimuth in degrees from north, clockwise
- `destination(start, bearingDeg, distanceM)` — terminal-point projection by direct-geodesic solution
- `pointInPolygon(point, polygon)` — spherical-coordinate ray-cast / winding test for arbitrary closed rings
- Body-agnostic — works for any planet whose oblateness is well-modelled by an ellipsoid of revolution
## Usage
### Basic Example
```ts
import { PlanetSurface } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
const earth: PlanetConfig = {
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: null,
satellites: [],
};
const surface = new PlanetSurface(earth);
const km = surface.distance(
{ latitude: 51.5, longitude: -0.13 }, // London
{ latitude: 40.71, longitude: -74.0 }, // New York
"kilometre",
);
```
### Advanced Example
```ts
import { PlanetSurface } from "@suaveplan/geo-planet";
import type { PlanetConfig } from "@suaveplan/geo-planet";
const mars: PlanetConfig = {
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
};
const surface = new PlanetSurface(mars);
// Project a 100 km traverse from Jezero crater on a 45° heading.
const start = { latitude: 18.44, longitude: 77.45 };
const end = surface.destination(start, 45, 100_000);
// Heading toward Olympus Mons.
const heading = surface.bearing(start, { latitude: 18.65, longitude: -133.8 });
// Region containment for science-target zones.
const aoi = [
{ latitude: 18.0, longitude: 77.0 },
{ latitude: 19.0, longitude: 77.0 },
{ latitude: 19.0, longitude: 78.0 },
{ latitude: 18.0, longitude: 78.0 },
];
const inside = surface.pointInPolygon(end, aoi);
```
## API Reference
### `class PlanetSurface`
#### `constructor(planet: PlanetConfig)`
Stores the planet on `surface.planet` and constructs an `Ellipsoid` via `defineEllipsoid(planet.radius, planet.polarRadius)` once per instance.
#### `distance(a, b, unit?: DistanceUnit): number`
Returns the great-circle distance between two `SphericalCoord` points along the cached ellipsoid. `unit` defaults to `"metre"`; pass `"kilometre"`, `"mile"`, etc. as supported by `@suaveplan/geo`.
#### `bearing(from, to): number`
Returns the initial forward azimuth in degrees clockwise from north. Note: the spherical bearing in `@suaveplan/geo` does not depend on the ellipsoid.
#### `destination(start, bearingDeg, distanceM): SphericalCoord`
Solves the direct geodesic problem on the cached ellipsoid: given a start point, an initial bearing, and a distance in metres, returns the terminal point.
#### `pointInPolygon(point, polygon: SphericalCoord[]): boolean`
Tests containment of `point` inside the closed polygon ring; uses spherical-coordinate point-in-polygon math from `@suaveplan/geo`.
## Implementation Notes
The ellipsoid is built once at construction and then reused on every method call, which keeps every distance / destination call allocation-free in steady state. Bearing intentionally does not pass the ellipsoid because the underlying `bearing` from `@suaveplan/geo` is a pure spherical primitive — for the precision needed in mission-planning UIs, the difference between spherical and geodetic bearing is below display granularity, and the geometric simplicity is worth more than the marginal accuracy gain. Storing `planet` as a public readonly property lets consumers retain the original config (for atmosphere, satellites, textures, etc.) without round-tripping through a separate registry.

View File

@@ -0,0 +1,60 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { PlanetSurface } from "./planet-surface.js";
const TEST_PLANET = {
name: "TestPlanet",
mass: 5.972e24,
radius: 6_371_000,
polarRadius: 6_356_000,
surfaceGravity: 9.81,
rotationPeriod: 86_400,
axialTilt: 23.5,
atmosphere: null,
satellites: [],
};
describe("PlanetSurface", () => {
it("computes distance with default unit", { timeout: 5000 }, () => {
const surface = new PlanetSurface(TEST_PLANET);
const a = { latitude: 0, longitude: 0 };
const b = { latitude: 0, longitude: 1 };
const d = surface.distance(a, b);
expect(d).toBeGreaterThan(0);
});
it("computes distance with explicit unit", { timeout: 5000 }, () => {
const surface = new PlanetSurface(TEST_PLANET);
const a = { latitude: 0, longitude: 0 };
const b = { latitude: 0, longitude: 1 };
const d = surface.distance(a, b, "kilometre");
expect(d).toBeGreaterThan(0);
});
it("computes bearing", { timeout: 5000 }, () => {
const surface = new PlanetSurface(TEST_PLANET);
const a = { latitude: 0, longitude: 0 };
const b = { latitude: 10, longitude: 0 };
expect(surface.bearing(a, b)).toBeCloseTo(0, 0);
});
it("computes destination", { timeout: 5000 }, () => {
const surface = new PlanetSurface(TEST_PLANET);
const start = { latitude: 0, longitude: 0 };
const end = surface.destination(start, 90, 100_000);
expect(end.latitude).toBeDefined();
expect(end.longitude).toBeDefined();
});
it("checks point in polygon", { timeout: 5000 }, () => {
const surface = new PlanetSurface(TEST_PLANET);
const square = [
{ latitude: 0, longitude: 0 },
{ latitude: 0, longitude: 10 },
{ latitude: 10, longitude: 10 },
{ latitude: 10, longitude: 0 },
];
expect(
surface.pointInPolygon({ latitude: 5, longitude: 5 }, square)
).toBe(true);
});
});

View File

@@ -0,0 +1,43 @@
import {
bearing,
type DistanceUnit,
defineEllipsoid,
destination,
type Ellipsoid,
greatCircleDistance,
pointInPolygon,
type SphericalCoord,
} from "@suaveplan/geo";
import type { PlanetConfig } from "../types/types.js";
export class PlanetSurface {
private readonly ellipsoid: Ellipsoid;
constructor(public readonly planet: PlanetConfig) {
this.ellipsoid = defineEllipsoid(planet.radius, planet.polarRadius);
}
distance(
a: SphericalCoord,
b: SphericalCoord,
unit: DistanceUnit = "metre"
): number {
return greatCircleDistance(a, b, this.ellipsoid, unit);
}
bearing(from: SphericalCoord, to: SphericalCoord): number {
return bearing(from, to);
}
destination(
start: SphericalCoord,
bearingDeg: number,
distanceM: number
): SphericalCoord {
return destination(start, bearingDeg, distanceM, this.ellipsoid);
}
pointInPolygon(point: SphericalCoord, polygon: SphericalCoord[]): boolean {
return pointInPolygon(point, polygon);
}
}

View File

@@ -0,0 +1,14 @@
export {
AtmosphereConfigSchema,
OrbitalConfigSchema,
PlanetConfigSchema,
PlanetTextureManifestSchema,
SatelliteConfigSchema,
} from "./schemas.js";
export type {
AtmosphereConfig,
OrbitalConfig,
PlanetConfig,
PlanetTextureManifest,
SatelliteConfig,
} from "./types.js";

View File

@@ -0,0 +1,84 @@
## Purpose
The `schemas` module exposes Zod runtime schemas mirroring every public type declared in `types.ts`. Validation is the boundary contract for any consumer accepting raw JSON, configuration files, IPC payloads, or untrusted user input that will later be handed to `PlanetSurface`, atmospheric solvers, or astronomical providers.
By keeping schemas co-located with the type module they validate, `geo-planet` guarantees that the runtime parser and the static `interface` shapes never drift. Every numeric invariant required by physical models (positive masses, normalised eccentricities, non-negative pressures) is enforced at parse time so downstream code can assume well-formed inputs without defensive null checks.
## Features
- `AtmosphereConfigSchema` — non-negative pressure / coefficients, positive scale height
- `SatelliteConfigSchema` — positive mass/radius/orbit, eccentricity ∈ `[0, 1]`, albedo ∈ `[0, 1]`
- `OrbitalConfigSchema` — Keplerian elements (semi-major axis AU, inclination, perihelion, ascending node, mean anomaly, epoch JD)
- `PlanetTextureManifestSchema` — optional diffuse/normal/specular/clouds/night texture URLs
- `PlanetConfigSchema` — composite root validating physical, orbital, and rendering metadata
- Inferred types via `z.infer<typeof Schema>` interoperate with the hand-authored `interface`s in `types.ts`
## Usage
### Basic Example
```ts
import { PlanetConfigSchema } from "@suaveplan/geo-planet";
const earth = PlanetConfigSchema.parse({
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: null,
satellites: [],
});
```
### Advanced Example
```ts
import {
AtmosphereConfigSchema,
OrbitalConfigSchema,
PlanetConfigSchema,
SatelliteConfigSchema,
} from "@suaveplan/geo-planet";
const result = PlanetConfigSchema.safeParse(rawJson);
if (!result.success) {
for (const issue of result.error.issues) {
console.error(issue.path.join("."), issue.message);
}
throw new Error("invalid planet config");
}
// Per-field schemas compose for partial validation pipelines.
const atmo = AtmosphereConfigSchema.parse(rawJson.atmosphere);
const orbit = OrbitalConfigSchema.parse(rawJson.orbital);
const moon = SatelliteConfigSchema.parse(rawJson.satellites[0]);
```
## API Reference
### `AtmosphereConfigSchema`
Validates `surfacePressure ≥ 0`, `scaleHeight > 0`, and non-negative Rayleigh, Mie, and air-mass extinction coefficients. Throws on negative numbers or missing fields.
### `SatelliteConfigSchema`
Validates non-empty `name`, positive `mass` / `radius` / `orbitalRadius` / `orbitalPeriod`, free `orbitalInclination`, and clamped `eccentricity` and `albedo` in `[0, 1]`.
### `OrbitalConfigSchema`
Validates positive `semiMajorAxisAU`, eccentricity ∈ `[0, 1]`, free angular elements, and any finite epoch Julian Date.
### `PlanetTextureManifestSchema`
All texture URL fields are optional strings; missing fields default to `undefined`.
### `PlanetConfigSchema`
Composite schema enforcing positive `mass` / `radius` / `polarRadius` / `surfaceGravity` / `rotationPeriod`, free `axialTilt`, `atmosphere: AtmosphereConfig | null`, an array of satellites, and optional `orbital` and `textures`.
## Implementation Notes
Every schema is built directly with `zod` and exports the parser; `z.infer` keeps inferred shapes structurally identical to the hand-authored interfaces. The `null`-permissive atmosphere field models airless bodies (Mercury, Moon) without forcing consumers into an option-type at the validation layer. Eccentricity is clamped to a closed interval rather than a half-open one because a parabolic trajectory (`e === 1`) is a useful boundary case for astronomical fixtures even though it is not bound. Schemas avoid `.strict()` so additive forward-compatible fields can be carried by raw payloads without forcing a parser bump.

View File

@@ -0,0 +1,35 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { PlanetConfigSchema } from "./schemas.js";
describe("PlanetConfigSchema", () => {
it("parses a valid planet config", { timeout: 5000 }, () => {
const parsed = PlanetConfigSchema.parse({
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
});
expect(parsed.name).toBe("Mars");
});
it("rejects negative mass", { timeout: 5000 }, () => {
expect(() =>
PlanetConfigSchema.parse({
name: "X",
mass: -1,
radius: 1,
polarRadius: 1,
surfaceGravity: 1,
rotationPeriod: 1,
axialTilt: 0,
atmosphere: null,
satellites: [],
})
).toThrow();
});
});

View File

@@ -0,0 +1,52 @@
import z from "zod";
export const AtmosphereConfigSchema = z.object({
surfacePressure: z.number().nonnegative(),
scaleHeight: z.number().positive(),
rayleighCoefficient: z.number().nonnegative(),
mieCoefficient: z.number().nonnegative(),
extinctionPerAirMass: z.number().nonnegative(),
});
export const SatelliteConfigSchema = z.object({
name: z.string().min(1),
mass: z.number().positive(),
radius: z.number().positive(),
orbitalRadius: z.number().positive(),
orbitalPeriod: z.number().positive(),
orbitalInclination: z.number(),
eccentricity: z.number().min(0).max(1),
albedo: z.number().min(0).max(1),
});
export const OrbitalConfigSchema = z.object({
semiMajorAxisAU: z.number().positive(),
eccentricity: z.number().min(0).max(1),
inclinationDeg: z.number(),
argumentOfPerihelionDeg: z.number(),
ascendingNodeDeg: z.number(),
meanAnomalyAtEpochDeg: z.number(),
epochJd: z.number(),
});
export const PlanetTextureManifestSchema = z.object({
diffuse: z.string().optional(),
normal: z.string().optional(),
specular: z.string().optional(),
clouds: z.string().optional(),
night: z.string().optional(),
});
export const PlanetConfigSchema = z.object({
name: z.string().min(1),
mass: z.number().positive(),
radius: z.number().positive(),
polarRadius: z.number().positive(),
surfaceGravity: z.number().positive(),
rotationPeriod: z.number().positive(),
axialTilt: z.number(),
atmosphere: AtmosphereConfigSchema.nullable(),
satellites: z.array(SatelliteConfigSchema),
orbital: OrbitalConfigSchema.optional(),
textures: PlanetTextureManifestSchema.optional(),
});

View File

@@ -0,0 +1,117 @@
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `types` module declares the static TypeScript interfaces describing every value `geo-planet` accepts and emits. These are the canonical shapes consumed by `PlanetSurface`, atmospheric models, and any downstream renderer or astronomical provider that needs a single source of truth for what defines a planetary body.
Keeping the interfaces in a hand-authored file (rather than relying solely on `z.infer`) lets the compiler emit precise, documented declarations into `.d.ts` outputs without dragging the Zod runtime into type-only consumers. The runtime mirror lives in `schemas.ts` and is verified by tests to stay structurally aligned with these interfaces.
## Features
- `AtmosphereConfig` — exponential atmosphere parameters (surface pressure, scale height, Rayleigh / Mie coefficients, extinction)
- `SatelliteConfig` — physical and orbital parameters for a natural satellite
- `OrbitalConfig` — heliocentric Keplerian elements with epoch Julian Date
- `PlanetTextureManifest` — optional URLs for diffuse / normal / specular / cloud / night textures
- `PlanetConfig` — composite top-level shape: physical body + optional atmosphere + satellites + optional orbital and texture metadata
## Usage
### Basic Example
```ts
import type { PlanetConfig } from "@suaveplan/geo-planet";
const mars: PlanetConfig = {
name: "Mars",
mass: 6.39e23,
radius: 3_396_200,
polarRadius: 3_376_200,
surfaceGravity: 3.72,
rotationPeriod: 88_620,
axialTilt: 25.2,
atmosphere: null,
satellites: [],
};
```
### Advanced Example
```ts
import type {
AtmosphereConfig,
OrbitalConfig,
PlanetConfig,
SatelliteConfig,
} from "@suaveplan/geo-planet";
const earthAtmosphere: AtmosphereConfig = {
surfacePressure: 101_325,
scaleHeight: 8_500,
rayleighCoefficient: 5.8e-6,
mieCoefficient: 21e-6,
extinctionPerAirMass: 0.13,
};
const earthOrbit: OrbitalConfig = {
semiMajorAxisAU: 1.0,
eccentricity: 0.0167,
inclinationDeg: 0.0,
argumentOfPerihelionDeg: 102.94,
ascendingNodeDeg: -11.26,
meanAnomalyAtEpochDeg: 357.51,
epochJd: 2_451_545,
};
const luna: SatelliteConfig = {
name: "Luna",
mass: 7.342e22,
radius: 1_737_400,
orbitalRadius: 384_400_000,
orbitalPeriod: 2_360_592,
orbitalInclination: 5.145,
eccentricity: 0.0549,
albedo: 0.12,
};
const earth: PlanetConfig = {
name: "Earth",
mass: 5.972e24,
radius: 6_378_137,
polarRadius: 6_356_752,
surfaceGravity: 9.81,
rotationPeriod: 86_164,
axialTilt: 23.44,
atmosphere: earthAtmosphere,
satellites: [luna],
orbital: earthOrbit,
textures: { diffuse: "/textures/earth-diffuse.ktx2" },
};
```
## API Reference
### `AtmosphereConfig`
`surfacePressure: number`, `scaleHeight: number`, `rayleighCoefficient: number`, `mieCoefficient: number`, `extinctionPerAirMass: number`.
### `SatelliteConfig`
`name: string`, plus the same physical and orbital scalars as a planet, restricted to `eccentricity ∈ [0, 1]` and `albedo ∈ [0, 1]` at the runtime layer.
### `OrbitalConfig`
`semiMajorAxisAU`, `eccentricity`, `inclinationDeg`, `argumentOfPerihelionDeg`, `ascendingNodeDeg`, `meanAnomalyAtEpochDeg`, `epochJd`.
### `PlanetTextureManifest`
Optional `diffuse`, `normal`, `specular`, `clouds`, `night` texture URL strings.
### `PlanetConfig`
`name`, `mass`, `radius`, `polarRadius`, `surfaceGravity`, `rotationPeriod`, `axialTilt`, `atmosphere: AtmosphereConfig | null`, `satellites: SatelliteConfig[]`, optional `orbital`, optional `textures`.
## Implementation Notes
The atmosphere field is `AtmosphereConfig | null` rather than `AtmosphereConfig | undefined` so airless bodies (Mercury, Moon) are explicitly modelled rather than accidentally omitted; renderers and atmospheric solvers branch on `=== null` directly. Both equatorial `radius` and `polarRadius` are required because `PlanetSurface` constructs an oblate ellipsoid for accurate great-circle math; spherical bodies pass equal values. Orbital and texture metadata are optional because tests, fixtures, and offline renderers often only need physical scalars.

View File

@@ -0,0 +1,50 @@
export interface AtmosphereConfig {
surfacePressure: number;
scaleHeight: number;
rayleighCoefficient: number;
mieCoefficient: number;
extinctionPerAirMass: number;
}
export interface SatelliteConfig {
name: string;
mass: number;
radius: number;
orbitalRadius: number;
orbitalPeriod: number;
orbitalInclination: number;
eccentricity: number;
albedo: number;
}
export interface OrbitalConfig {
semiMajorAxisAU: number;
eccentricity: number;
inclinationDeg: number;
argumentOfPerihelionDeg: number;
ascendingNodeDeg: number;
meanAnomalyAtEpochDeg: number;
epochJd: number;
}
export interface PlanetTextureManifest {
diffuse?: string;
normal?: string;
specular?: string;
clouds?: string;
night?: string;
}
export interface PlanetConfig {
name: string;
mass: number;
radius: number;
polarRadius: number;
surfaceGravity: number;
rotationPeriod: number;
axialTilt: number;
atmosphere: AtmosphereConfig | null;
satellites: SatelliteConfig[];
orbital?: OrbitalConfig;
textures?: PlanetTextureManifest;
}

View File

@@ -0,0 +1,12 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"lib": ["ES2022"],
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.test.ts", "src/**/*.bench.ts", "dist", "node_modules"]
}

View File

@@ -0,0 +1,37 @@
import { defineConfig } from "vite";
import dts from "vite-plugin-dts";
export default defineConfig({
root: __dirname,
cacheDir: "../../node_modules/.vite/packages/geo-planet",
plugins: [
dts({
entryRoot: "src",
tsconfigPath: "./tsconfig.lib.json",
insertTypesEntry: true,
}),
],
build: {
outDir: "./dist",
emptyOutDir: true,
reportCompressedSize: true,
sourcemap: true,
lib: { entry: "src/index.ts", formats: ["es"] },
rollupOptions: {
treeshake: true,
output: {
preserveModules: true,
preserveModulesRoot: "src",
entryFileNames: "[name].js",
},
external: [
"@suaveplan/error",
"@suaveplan/geo",
"@suaveplan/types",
"tslib",
"zod",
/^node:/,
],
},
},
});

View File

@@ -0,0 +1,2 @@
import { createTestConfig } from "@suaveplan/testing/config";
export default createTestConfig();