@johnmorrisdotca/uchusen-rockets 0.10.0 → 0.11.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/CHANGELOG.md CHANGED
@@ -2,15 +2,21 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- - **The comparison page with the fleet in it**: a fleet ship lies lengthwise in its own band below the rockets, on the same scale and ruler, and the scale fits whatever is shown (the ruler runs to 2,000 m with Kaminari in); a switch (All, Rockets, SPX fleet) picks the kind, and Rockets alone keeps the rockets' own scale; a fleet ship's table rows are Length, Beam and Height (stowed), not "whole height"; the family's view controls zoom, fit and pan the lineup, and the zoom stays in the address. `<uchusen-lineup>` takes `filter` and `fit`, and `lineupSvg` takes `filter` and `fit` and returns the scale it drew at. The fleet's maker reads "SPX".
6
- - **Fix, the chosen tab of a ship's panel under the 3D view**: it was drawn as a blank pill in every theme, because its fill was `currentColor` while its words were set to `Canvas` on the same button, so both were one colour. The chosen tab and the Labels toggle now have a tinted fill, a ring and bold words, in the page's own colours.
7
- - **Fix, the demo's Rocket list**: under SPX it holds the fleet ships by their registry names (ISV Miyamoto, SPX-452; ISV Ariake, SPX-186; ISV Kaminari, SPX-207) and no longer the kit's "Fleet ..." boxes of parts, on the demo and on the code builder. A kit box still draws when the address names it (`?rocket=fleet-tanks`).
8
- - Test only: `e2e/fleet-demo.demo.ts` holds the Maker row and the Made by line to "SPX" for every fleet ship, the Rocket list to the ships, and the panel's chosen tab to a readable picture.
5
+ ## [0.11.0] - 2026-11-04
6
+
7
+ Journey mode (Earth to the Moon and the planets) and real craft for scale beside the SPX fleet.
8
+
9
+ - **Journey mode, slice 1b**: after each burn the side view shows only what is left, drawn from the rockets' own parts at the whole rocket's scale (Saturn V: S-IVB with the Apollo spacecraft, then the command and service module; SLS: upper stage with Orion, then Orion; Falcon Heavy: upper stage with its payload, then the payload in its fairing). `build().svg()` takes `only` (the slots left, closed up) and `frame` (the bounds to fit to). Falcon Heavy and SLS are drawn from the front so all their cores show. The Fuel row is the propellant used or n/a with a reason; the payload has its own row, "Payload to <target>".
10
+ - **Journey mode, first slice** (MODULES-394, parts 395 to 399 and 401): `journey.html` in the demo and the `@johnmorrisdotca/uchusen-rockets/journey` entry. Choose a start (the planets), an end (the Moon or a planet) and a craft (Saturn V, Space Launch System, Falcon Heavy, ISV Kaminari SPX-207), then fly it: a countdown with an engine preheat bar and an ignition ring, a side view with the craft's own line drawing, a map, a route bar, the numbers, the flip and the braking burn, and a summary. A rocket flies a Hohmann transfer on a launch window found from the planets' real positions (wakusei-planets); a fleet ship flies a relativistic flip and burn. Every craft figure names its source or is marked as an estimate; a craft that cannot go says why in one line. No WebGL: one 2D canvas, SVG and the line drawing, a frame only while a trip plays, 30 a second at most. Pinned against published values in `test/journey-physics.test.ts` and `test/journey-trip.test.ts`; guide in English and Japanese; plan in `docs/journey/plan.md`. Dev dependency: wakusei-planets 0.15.0.
11
+ - **Beside it** (MODULES-431): `beside="on"` on `<uchusen-rocket>` stands the real Starship stack (Super Heavy and the ship, Block 3, from the package's own model) beside the craft on screen at true scale, on the same ground line and clear of it, in the 3D view and the drawing. Fit holds both craft. The demo has a "Beside it" toggle under Scale; it is kept in the address (`?beside=on`), survives a reload, and is not offered for Starship itself. `build(description, { beside: true })` gives `Built.beside` (the parts beside, slots `beside-booster` and `beside-ship`), and `boundsOf`, `placesOf`, `glb` and `svg` take them in; `besideLayout`, `besideBounds`, `canBeside` and `BESIDE_ROCKET` are the pure layout. The stack beside is drawn in this package's own paint in every look, so no licensed model comes in uncredited; a flight hides it.
12
+ - **Real craft for scale** (MODULES-431): on the comparison page Starship with Super Heavy, Saturn V, SLS Block 1 and the ISS lie sideways in the SPX fleet band at the fleet ships' own scale and ruler, one row each, a rocket nose right and the station along its long axis, each labelled with its name and sourced length (124.4, 110.64, 98.15 and 109 m). A "Real craft for scale" toggle is on by default and is kept in the address (`?real=off`); it is hidden with Rockets alone, which has no fleet band. `<uchusen-lineup real="on">` and `lineupSvg({ real: true })` (which returns `real`) do it; off is today's picture. `REAL_CRAFT` lists them. The ISS line model ends its arrays 1.2 m inside the sourced 109 m, so its bar is 1.1% short.
9
13
 
10
14
  ## [0.10.0] - 2026-11-04
11
15
 
12
16
  The SPX fleet's first ships: ISV Kaminari SPX-207, ISV Miyamoto SPX-452 and her sister ISV Ariake SPX-186, with the viewer panel and the fleet on the comparison page.
13
17
 
18
+ - **Fix, the chosen tab of a ship's panel under the 3D view**: it was drawn as a blank pill in every theme, because its fill was `currentColor` while its words were set to `Canvas` on the same button, so both were one colour. The chosen tab and the Labels toggle now have a tinted fill, a ring and bold words, in the page's own colours.
19
+ - **Fix, the demo's Rocket list**: under SPX it holds the fleet ships by their registry names (ISV Miyamoto, SPX-452; ISV Ariake, SPX-186; ISV Kaminari, SPX-207) and no longer the kit's "Fleet ..." boxes of parts, on the demo and on the code builder. A kit box still draws when the address names it (`?rocket=fleet-tanks`).
14
20
  - **Fleet kit quality pass** (MODULES-414, MODULES-407 follow-up): the kit's look is raised. The hull is mostly white quilted thermal blanket with a few rigid framed panels, crinkled foil on the hardware that wears it and orange only on small removable access panels (`LIVERY`, `hullPanels`, `blanketPanel`, `framedPanel`, `foilBand`); plates can be quilted, framed over a sunk field, a single sheet or loose foil facets (`Plate.quilt`, `inset`, `sheet`, `facets`) and a lathe can be fluted. Pipes have real bends, bolted flange joints, glands, blind caps, valves and stand-off clamp brackets (`pipeRun`), and conduits leave the hull through glands. Nozzles are regeneratively cooled bells with a fluted tube-wall jacket, manifolds, a foil-wrapped chamber, a turbopump with its lines, a gimbal and actuators. The line look is held to 0.6 of the painted look's density. The Lit look: drive coils, windows, red and green navigation lights, strobes, floodlights and grow-lamps glow with "Engines: Lit" (`MATERIALS` `glow`). New: separation joints (`separationJoint`, `separation` on a ship's place), armoured portholes with burn shutters, an observation dome with six shutter petals, ten furnished rooms (`ROOMS`, `room`), and an observation dome lounge and a solarium module with clear glass (`opacity`). A fleet module may be up to 1.25 MB and a ship 12 MB.
15
21
  - **The fleet module kit** (`src/model/fleet/`, `docs/fleet/kit.md`): the pieces every ship of the fictional fleet is built from, in plain geometry and metres. 38 modules (fusion engine clusters and thruster pods, cryogenic and slim tanks, a sphere tank farm, cargo containers and a bay for four, habitat rings and drums, a laboratory, a sensor mast, a reactor, radiator arrays, docking nodes, trusses, adapters and a forward shield), each with its name, mass and size, the standard docking collars (four classes, S to H, each twice the one before) and a cutaway where it has an inside. A ship is a list of modules on a spine (`assemble`, `shipPieces`, `shipParts`), checked joint by joint. A detail kit (plating, hatches, conduits, radiators, tile, antennae, stencils, lamps, greebles) covers every surface; repeated detail is stamped, one template written many times. The shared data of the ships: engine cards (thrust = mass flow x specific impulse x g0, held by a test) with the relativistic performance and the Proxima trip, hull-layer stacks by role that a cutaway draws as real concentric shells, named sections with labels and numbered decks, four system insets of five or six numbered sub-parts, swappable mission pods, an exploded assembly, a symbol legend, scale bar and views at one scale, and the fleet palette. The modules are rockets in the picker under "Fleet (fictional)", labelled fictional, in all three looks, with one sample ship. A fleet part is lit with a shadow. Every module has a coarse level and a fine one (`shipLevels`, `timeBuild`, a build budget of 1 s for the first picture and 3 s for full detail, held by a test), names the real hardware it descends from, and is built with round pipes (`pipeRun`: bends, flanges, brackets) and I-beam, channel and round-strut sections. Any registry number is a designation (`designation("207")`); `titleBlock` and `specBlock` carry the glossary (ISV, class codes, FTD and DCD, KAZE, ATC, the Japanese name with its ruby); `BUILDERS` makes a module of a ship's own size; `cutYaw` turns where a cutaway opens; the solids by range of SPX-03's adapter (`cub`, `prism`, `quad`, `discX`, `discZ`) are shared. The fleet is written in-universe (`titleBlock`: planned and in-development status, designations from `SPX-`, the maker "SPX"); the picker group is "SPX fleet" and the NASA group is just "NASA" so the maker row stays on one line. The element has `screenPoint(point)` and a `uchusen-draw` event, so a label can follow a part in 3D. Adds about 62 KB (minified) to the root entry and about 25 MB to the package's assets; the fleet's ships are allowed 6 MB of mesh where a rocket is allowed 600 KB.
16
22
  - Test only: the freeze test walks the fleet in its own test, so the walks of every real rocket stay as long as they were.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Uchūsen means "spaceship". Each rocket comes in every variant a source document
12
12
  <img alt="TypeScript" src="https://img.shields.io/badge/types-TypeScript-3178c6">
13
13
  </p>
14
14
 
15
- <p align="center"><a href="https://johnmorrisdotca.github.io/uchusen-rockets/"><strong>Open the demo →</strong></a> · <a href="./docs/variants.md">Variants and sizes</a> · <a href="./docs/parts.md">Build from parts</a> · <a href="./docs/element.md">The element</a> · <a href="./docs/flight.md">Lit engines and flights</a> · <a href="./docs/embed.md">Use one on your site</a> · <a href="./docs/assets.md">Assets</a> · <a href="./docs/guides/getting-started.md">Guides</a> · <a href="https://johnmorrisdotca.github.io/uchusen-rockets/builder.html">Code builder</a> · <a href="https://johnmorrisdotca.github.io/uchusen-rockets/docs/">Docs site</a></p>
15
+ <p align="center"><a href="https://johnmorrisdotca.github.io/uchusen-rockets/"><strong>Open the demo →</strong></a> · <a href="./docs/variants.md">Variants and sizes</a> · <a href="./docs/parts.md">Build from parts</a> · <a href="./docs/element.md">The element</a> · <a href="./docs/flight.md">Lit engines and flights</a> · <a href="./docs/embed.md">Use one on your site</a> · <a href="./docs/assets.md">Assets</a> · <a href="./docs/guides/getting-started.md">Guides</a> · <a href="https://johnmorrisdotca.github.io/uchusen-rockets/builder.html">Code builder</a> · <a href="https://johnmorrisdotca.github.io/uchusen-rockets/journey.html">Journey mode</a> · <a href="https://johnmorrisdotca.github.io/uchusen-rockets/docs/">Docs site</a></p>
16
16
 
17
17
  <p align="center"><img alt="Block 3 of the rocket as a painted 3D view, stainless steel with black tiles, beside its controls and its sizes, each with a source." src="https://raw.githubusercontent.com/johnmorrisdotca/uchusen-rockets/main/docs/images/main-painted.jpg" width="820"></p>
18
18
 
@@ -41,7 +41,7 @@ npm install @johnmorrisdotca/uchusen-rockets three
41
41
  <uchusen-rocket rocket="starship" variant="v2" show="booster" look="thick" view="below"></uchusen-rocket>
42
42
  ```
43
43
 
44
- That is all a page needs with a bundler. The drawing shows at once; when the element comes on screen and the browser can draw WebGL, the 3D view takes over without a jump. Drag to turn it, use the arrow keys, or leave it to its slow spin. `engines="on"` lights each engine at full thrust, in the painted look and as a drawn flame in the line looks, and a reader who asked for reduced motion has them off. A flame's length and its shock diamonds are a picture, not a measurement. [A flight](docs/flight.md) plays each phase with the engines that burn in it, from published timelines. With no WebGL, under Save-Data, or on any failure, the drawing stays.
44
+ That is all a page needs with a bundler. The drawing shows at once; when the element comes on screen and the browser can draw WebGL, the 3D view takes over without a jump. Drag to turn it, use the arrow keys, or leave it to its slow spin. `engines="on"` lights each engine at full thrust, in the painted look and as a drawn flame in the line looks, and a reader who asked for reduced motion has them off. A flame's length and its shock diamonds are a picture, not a measurement. `beside="on"` stands Starship with Super Heavy next to the rocket at true scale, to see how big it is. [A flight](docs/flight.md) plays each phase with the engines that burn in it, from published timelines. With no WebGL, under Save-Data, or on any failure, the drawing stays.
45
45
 
46
46
  ## Who it is for
47
47
 
@@ -363,6 +363,7 @@ Each guide has a Japanese twin in [docs/guides/ja](docs/guides/ja).
363
363
  | [Build a rocket from parts](docs/guides/build-from-parts.md) | Part records, the fit check, `build`, the builder |
364
364
  | [Lit engines and flights](docs/guides/engines-and-flights.md) | Engines at full thrust, flights, what is sourced |
365
365
  | [Rockets to scale](docs/guides/scale-lineups.md) | `<uchusen-lineup>` and `lineupSvg` |
366
+ | [Journey mode](docs/guides/journey.md) | Fly a rocket or a fleet ship to the Moon or a planet: windows, burns, the flip |
366
367
  | [Put a rocket on your own site](docs/guides/embed-on-your-site.md) | Six ways, and the credit |
367
368
  | [React and other frameworks](docs/guides/react-and-frameworks.md) | React, Vue, Svelte, Angular |
368
369
  | [Bundlers and CDNs](docs/guides/bundlers-and-cdn.md) | unpkg, Vite, webpack |
@@ -0,0 +1,76 @@
1
+ import type { Description } from "./fit.ts";
2
+ import type { Vec3 } from "./framing.ts";
3
+ import type { Bounds } from "./model/pieces.ts";
4
+ /**
5
+ * The rocket that can stand beside another for scale: the real Starship stack, Super Heavy and the ship, Block 3. Beside another craft it is drawn in this package's own paint, so no licensed model comes in unseen.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { BESIDE_ROCKET } from "@johnmorrisdotca/uchusen-rockets";
10
+ *
11
+ * console.log(BESIDE_ROCKET.rocket, BESIDE_ROCKET.variant);
12
+ * ```
13
+ */
14
+ export declare const BESIDE_ROCKET: Readonly<Description>;
15
+ /** What the stack beside is called. */
16
+ export declare const BESIDE_NAME = "Starship with Super Heavy";
17
+ /**
18
+ * The words that say what the craft beside it is: its name and its sourced length, such as "Starship with Super Heavy, 124.4 m".
19
+ *
20
+ * @returns The label the 3D view puts under the stack.
21
+ * @example
22
+ * ```ts
23
+ * import { besideLabel } from "@johnmorrisdotca/uchusen-rockets";
24
+ *
25
+ * console.log(besideLabel());
26
+ * ```
27
+ */
28
+ export declare function besideLabel(): string;
29
+ /** The least room between the two, in metres. */
30
+ export declare const BESIDE_GAP = 4;
31
+ /**
32
+ * Whether Starship can stand beside this rocket: every rocket but Starship itself.
33
+ *
34
+ * @param rocket A rocket's id.
35
+ * @returns False for Starship, true for any other.
36
+ * @example
37
+ * ```ts
38
+ * import { canBeside } from "@johnmorrisdotca/uchusen-rockets";
39
+ *
40
+ * console.log(canBeside("saturn-v"), canBeside("starship"));
41
+ * ```
42
+ */
43
+ export declare const canBeside: (rocket: string) => boolean;
44
+ /**
45
+ * Where the other rocket stands: its offset in metres, to add to where its own parts stand, and the box it fills there. It is never scaled. Its foot is on the ground line (y = 0) as every rocket's is, it keeps its own
46
+ * line (z = 0), and its nearest side is a gap clear of the craft's widest, to the right (+X).
47
+ *
48
+ * @param craft The box the craft fills, in metres.
49
+ * @param other The box the other rocket fills on its own, in metres.
50
+ * @returns The offset and the box the other rocket fills once it is there.
51
+ * @example
52
+ * ```ts
53
+ * import { besideLayout } from "@johnmorrisdotca/uchusen-rockets";
54
+ *
55
+ * const { at, bounds } = besideLayout({ min: [-4, 0, -4], max: [4, 70, 4] }, { min: [-9, 0, -5], max: [9, 124, 9] });
56
+ * console.log(at, bounds.max[1]);
57
+ * ```
58
+ */
59
+ export declare function besideLayout(craft: Bounds, other: Bounds): {
60
+ at: Vec3;
61
+ bounds: Bounds;
62
+ };
63
+ /**
64
+ * The box that holds both: what the camera must frame, and what Fit fits.
65
+ *
66
+ * @param craft The box the craft fills.
67
+ * @param placed The box the other rocket fills where `besideLayout` put it.
68
+ * @returns The smallest box that holds both.
69
+ * @example
70
+ * ```ts
71
+ * import { besideBounds } from "@johnmorrisdotca/uchusen-rockets";
72
+ *
73
+ * console.log(besideBounds({ min: [-4, 0, -4], max: [4, 70, 4] }, { min: [8, 0, -5], max: [26, 124, 9] }).max);
74
+ * ```
75
+ */
76
+ export declare function besideBounds(craft: Bounds, placed: Bounds): Bounds;
package/dist/beside.js ADDED
@@ -0,0 +1,83 @@
1
+ import { variantOf } from "./rockets.js";
2
+ /**
3
+ * The rocket that can stand beside another for scale: the real Starship stack, Super Heavy and the ship, Block 3. Beside another craft it is drawn in this package's own paint, so no licensed model comes in unseen.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { BESIDE_ROCKET } from "@johnmorrisdotca/uchusen-rockets";
8
+ *
9
+ * console.log(BESIDE_ROCKET.rocket, BESIDE_ROCKET.variant);
10
+ * ```
11
+ */
12
+ export const BESIDE_ROCKET = { rocket: "starship", variant: "v3" };
13
+ /** What the stack beside is called. */
14
+ export const BESIDE_NAME = "Starship with Super Heavy";
15
+ /**
16
+ * The words that say what the craft beside it is: its name and its sourced length, such as "Starship with Super Heavy, 124.4 m".
17
+ *
18
+ * @returns The label the 3D view puts under the stack.
19
+ * @example
20
+ * ```ts
21
+ * import { besideLabel } from "@johnmorrisdotca/uchusen-rockets";
22
+ *
23
+ * console.log(besideLabel());
24
+ * ```
25
+ */
26
+ export function besideLabel() {
27
+ const height = variantOf(BESIDE_ROCKET.rocket, BESIDE_ROCKET.variant)?.facts.height.value;
28
+ return typeof height === "number" ? `${BESIDE_NAME}, ${height.toLocaleString("en-US")} m` : BESIDE_NAME;
29
+ }
30
+ /** The least room between the two, in metres. */
31
+ export const BESIDE_GAP = 4;
32
+ /**
33
+ * Whether Starship can stand beside this rocket: every rocket but Starship itself.
34
+ *
35
+ * @param rocket A rocket's id.
36
+ * @returns False for Starship, true for any other.
37
+ * @example
38
+ * ```ts
39
+ * import { canBeside } from "@johnmorrisdotca/uchusen-rockets";
40
+ *
41
+ * console.log(canBeside("saturn-v"), canBeside("starship"));
42
+ * ```
43
+ */
44
+ export const canBeside = (rocket) => rocket !== BESIDE_ROCKET.rocket;
45
+ /**
46
+ * Where the other rocket stands: its offset in metres, to add to where its own parts stand, and the box it fills there. It is never scaled. Its foot is on the ground line (y = 0) as every rocket's is, it keeps its own
47
+ * line (z = 0), and its nearest side is a gap clear of the craft's widest, to the right (+X).
48
+ *
49
+ * @param craft The box the craft fills, in metres.
50
+ * @param other The box the other rocket fills on its own, in metres.
51
+ * @returns The offset and the box the other rocket fills once it is there.
52
+ * @example
53
+ * ```ts
54
+ * import { besideLayout } from "@johnmorrisdotca/uchusen-rockets";
55
+ *
56
+ * const { at, bounds } = besideLayout({ min: [-4, 0, -4], max: [4, 70, 4] }, { min: [-9, 0, -5], max: [9, 124, 9] });
57
+ * console.log(at, bounds.max[1]);
58
+ * ```
59
+ */
60
+ export function besideLayout(craft, other) {
61
+ const gap = Math.max(BESIDE_GAP, (craft.max[0] - craft.min[0]) * 0.1);
62
+ const at = [craft.max[0] + gap - other.min[0], 0 - other.min[1], 0];
63
+ return { at, bounds: { min: [other.min[0] + at[0], other.min[1] + at[1], other.min[2]], max: [other.max[0] + at[0], other.max[1] + at[1], other.max[2]] } };
64
+ }
65
+ /**
66
+ * The box that holds both: what the camera must frame, and what Fit fits.
67
+ *
68
+ * @param craft The box the craft fills.
69
+ * @param placed The box the other rocket fills where `besideLayout` put it.
70
+ * @returns The smallest box that holds both.
71
+ * @example
72
+ * ```ts
73
+ * import { besideBounds } from "@johnmorrisdotca/uchusen-rockets";
74
+ *
75
+ * console.log(besideBounds({ min: [-4, 0, -4], max: [4, 70, 4] }, { min: [8, 0, -5], max: [26, 124, 9] }).max);
76
+ * ```
77
+ */
78
+ export function besideBounds(craft, placed) {
79
+ return {
80
+ min: [Math.min(craft.min[0], placed.min[0]), Math.min(craft.min[1], placed.min[1]), Math.min(craft.min[2], placed.min[2])],
81
+ max: [Math.max(craft.max[0], placed.max[0]), Math.max(craft.max[1], placed.max[1]), Math.max(craft.max[2], placed.max[2])],
82
+ };
83
+ }
@@ -65,6 +65,10 @@ export interface SvgOptions {
65
65
  engines?: boolean | Readonly<Record<string, number>>;
66
66
  /** Degrees each moving group of each part is turned from where it is drawn at rest, by slot and then by name: `{ booster: { "grid-fins": 12 } }`. A flight asks for them. */
67
67
  moves?: Readonly<Record<string, Readonly<Record<string, number>>>>;
68
+ /** Only these slots, closed up into one stack, centred in `frame` when it is given: a rocket after its first stages have gone. Left out, `show` decides. */
69
+ only?: readonly string[];
70
+ /** The bounds the picture is fitted to, from another build's `boundsOf`: parts drawn with it keep the scale of the whole rocket they came from. */
71
+ frame?: Bounds;
68
72
  }
69
73
  /**
70
74
  * A rocket built from a description: the fit report, its parts, and the ways to ask for its size, where each part stands, its lit engines, a `.glb` file and a drawing.
@@ -81,6 +85,8 @@ export interface SvgOptions {
81
85
  export interface Built {
82
86
  report: FitReport;
83
87
  parts: readonly BuiltPart[];
88
+ /** The parts of the rocket standing beside it, when it was built `beside`; empty otherwise. They are not in `parts`; `placesOf`, `boundsOf`, `glb` and `svg` take them in. Their slots start `beside-`. */
89
+ beside: readonly BuiltPart[];
84
90
  /** What can be shown: together, apart and each part. */
85
91
  shows: readonly string[];
86
92
  /** The bounds of a choice of what to show, in metres, and with `lit` the flames of its engines at full thrust. */
@@ -131,7 +137,7 @@ export declare function drawnPieces(part: {
131
137
  * A rocket from a description. Throws FitError when it does not go together.
132
138
  *
133
139
  * @param description The rocket, its variant and any other part in a slot.
134
- * @param options `detail`: `"coarse"` leaves out the small 3D-only detail of a very detailed part, for a fast first view; left out, every piece is made.
140
+ * @param options `detail`: `"coarse"` leaves out the small 3D-only detail of a very detailed part, for a fast first view; left out, every piece is made. `beside`: the real Starship stack (Block 3) stands beside it at true scale, on the same ground, and `boundsOf`, `placesOf`, `glb` and `svg` take it in; Starship itself has nothing beside it.
135
141
  * @returns The built rocket. It throws a `FitError` when the description does not go together.
136
142
  * @example
137
143
  * ```ts
@@ -143,6 +149,7 @@ export declare function drawnPieces(part: {
143
149
  */
144
150
  export declare function build(description: Description, options?: {
145
151
  detail?: DetailLevel;
152
+ beside?: boolean;
146
153
  }): Built;
147
154
  /**
148
155
  * One part on its own, by its id.
@@ -165,5 +172,5 @@ export declare function buildPart(id: string): {
165
172
  };
166
173
  export { toGlb };
167
174
  export type { Glb };
168
- export { COMPARES, lineupSvg } from "./lineup.ts";
175
+ export { COMPARES, lineupSvg, REAL_CRAFT } from "./lineup.ts";
169
176
  export type { CompareId, LineupItem, LineupOptions } from "./lineup.ts";
@@ -1,6 +1,7 @@
1
1
  // The builder: one description into one mesh and one drawing. Works in Node and in a browser, with no dependency: the geometry here is this package's own.
2
2
  import { propellantOf } from "../data/propellants.js";
3
3
  import { check } from "../fit.js";
4
+ import { BESIDE_ROCKET, besideBounds, besideLayout, canBeside } from "../beside.js";
4
5
  import { cleanView, DEFAULT_VIEW, PICTURE_H, PICTURE_W, VIEWS } from "../framing.js";
5
6
  import { cleanShow, placesFor, showsOf } from "../layout.js";
6
7
  import { DEFAULT_LOOK } from "../looks.js";
@@ -87,25 +88,20 @@ function flameKindOf(part) {
87
88
  }
88
89
  return "neutral";
89
90
  }
90
- function assembled(report) {
91
- const parts = report.parts.map(({ slot, part }) => {
91
+ function assembled(report, other) {
92
+ const own = report.parts.map(({ slot, part }) => {
92
93
  const one = made(part.id);
93
94
  return { slot, record: part, pieces: one.pieces, get mesh() { return one.mesh; }, bounds: one.bounds, flame: flameKindOf(part) };
94
95
  });
95
- const slots = parts.map((part) => part.slot);
96
- const placesOf = (show) => placesFor(report.parts, cleanShow(show, slots), (part) => widthOf(part.id));
97
- const flamesOf = (show) => {
98
- const places = placesOf(show);
99
- return litBells(places.map((place) => {
100
- const part = parts.find((p) => p.slot === place.slot);
101
- const told = report.variant?.facts.engines[part.slot]?.value;
102
- return { slot: part.slot, atY: place.at[1], shown: place.shown, pieces: part.pieces, beside: place.at[0] !== 0 || place.at[2] !== 0, dark: told === null || told === undefined };
103
- }), !show || show === "together");
104
- };
105
- const boundsOfShow = (show, lit = false) => {
96
+ const slots = own.map((part) => part.slot);
97
+ const beside = (other?.parts ?? []).map((part) => ({ slot: `beside-${part.slot}`, record: part.record, pieces: part.pieces, get mesh() { return part.mesh; }, bounds: part.bounds, flame: part.flame }));
98
+ // what is drawn: the rocket's parts, then the ones beside it
99
+ const parts = [...own, ...beside];
100
+ const ownPlaces = (show) => placesFor(report.parts, cleanShow(show, slots), (part) => widthOf(part.id));
101
+ const boundsOfPlaces = (places) => {
106
102
  const min = [Infinity, Infinity, Infinity];
107
103
  const max = [-Infinity, -Infinity, -Infinity];
108
- for (const place of placesOf(show)) {
104
+ for (const place of places) {
109
105
  if (!place.shown)
110
106
  continue;
111
107
  const { bounds } = parts.find((part) => part.slot === place.slot);
@@ -114,6 +110,34 @@ function assembled(report) {
114
110
  max[c] = Math.max(max[c], bounds.max[c] + place.at[c]);
115
111
  }
116
112
  }
113
+ return { min, max };
114
+ };
115
+ // the other rocket stands whole, on the ground, clear of whatever of this one shows
116
+ const placesOf = (show) => {
117
+ const mine = ownPlaces(show);
118
+ if (!other)
119
+ return mine;
120
+ const { at } = besideLayout(boundsOfPlaces(mine), other.boundsOf("together"));
121
+ return [...mine, ...other.placesOf("together").map((place) => ({ slot: `beside-${place.slot}`, at: [place.at[0] + at[0], place.at[1] + at[1], place.at[2] + at[2]], shown: true }))];
122
+ };
123
+ const flamesAt = (places, together) => litBells(places.map((place) => {
124
+ const part = parts.find((p) => p.slot === place.slot);
125
+ const told = report.variant?.facts.engines[part.slot]?.value;
126
+ return { slot: part.slot, atY: place.at[1], shown: place.shown, pieces: part.pieces, beside: place.at[0] !== 0 || place.at[2] !== 0, dark: told === null || told === undefined };
127
+ }), together);
128
+ const flamesOf = (show) => flamesAt(placesOf(show), !show || show === "together");
129
+ // the parts that are left, closed up joint to joint, and centred in a frame when there is one
130
+ const stackOf = (only, frame) => {
131
+ const stacked = placesFor(report.parts.filter(({ slot }) => only.includes(slot)), "together", (part) => widthOf(part.id));
132
+ if (!frame || stacked.length === 0)
133
+ return stacked;
134
+ const mine = boundsOfPlaces(stacked);
135
+ const dy = (frame.min[1] + frame.max[1]) / 2 - (mine.min[1] + mine.max[1]) / 2;
136
+ return stacked.map((place) => ({ ...place, at: [place.at[0], place.at[1] + dy, place.at[2]] }));
137
+ };
138
+ const boundsOfShow = (show, lit = false) => {
139
+ const mine = boundsOfPlaces(ownPlaces(show));
140
+ const { min, max } = other ? besideBounds(mine, besideLayout(mine, other.boundsOf("together")).bounds) : mine;
117
141
  if (lit)
118
142
  for (const bell of flamesOf(show)) {
119
143
  const place = placesOf(show).find((one) => one.slot === bell.slot);
@@ -123,17 +147,19 @@ function assembled(report) {
123
147
  };
124
148
  return {
125
149
  report,
126
- parts,
150
+ parts: own,
151
+ beside,
127
152
  shows: showsOf(slots),
128
153
  boundsOf: boundsOfShow,
129
154
  placesOf,
130
155
  flamesOf,
131
156
  glb: () => toGlb(parts.map((part) => ({ name: part.slot, mesh: part.mesh, at: placesOf("together").find((place) => place.slot === part.slot)?.at ?? [0, 0, 0] }))),
132
- svg: ({ look = DEFAULT_LOOK, view = DEFAULT_VIEW, show, engines = false, moves } = {}) => {
157
+ svg: ({ look = DEFAULT_LOOK, view = DEFAULT_VIEW, show, engines = false, moves, only, frame } = {}) => {
133
158
  const angle = typeof view === "string" ? VIEWS[cleanView(view)] : view;
134
- const places = placesOf(show).filter((place) => place.shown);
159
+ const placed = only ? stackOf(only, frame) : placesOf(show);
160
+ const places = placed.filter((place) => place.shown);
135
161
  // a flame drawn at full thrust is held in the picture; a count of engines is the timeline's, which has its own room
136
- const cam = perspective(boundsOfShow(show, engines === true), angle);
162
+ const cam = perspective(frame ?? boundsOfShow(show, engines === true), angle);
137
163
  const ordered = places
138
164
  .map((place) => {
139
165
  const part = parts.find((p) => p.slot === place.slot);
@@ -141,11 +167,11 @@ function assembled(report) {
141
167
  return { place, part, depth: cam.depth(middle) };
142
168
  })
143
169
  .sort((a, b) => b.depth - a.depth);
144
- const together = !show || show === "together";
170
+ const together = only !== undefined || !show || show === "together";
145
171
  const flames = typeof engines === "object"
146
172
  ? parts.flatMap((part) => pickBells(bellsOf(part.pieces), engines[part.slot] ?? 0).map((bell) => ({ ...bell, slot: part.slot })))
147
173
  : engines
148
- ? flamesOf(show).filter((bell) => places.some((place) => place.slot === bell.slot))
174
+ ? (only ? flamesAt(placed, true) : flamesOf(show)).filter((bell) => places.some((place) => place.slot === bell.slot))
149
175
  : [];
150
176
  return wrap(ordered
151
177
  .map(({ place, part }) => drawPart(moved(drawnPieces(part, together), moves?.[part.slot] ?? {}), place.at, cam, look) + plumeMarkup(flames.filter((bell) => bell.slot === place.slot), place.at, cam, look, part.slot, part.flame))
@@ -177,7 +203,7 @@ export function drawnPieces(part, together) {
177
203
  * A rocket from a description. Throws FitError when it does not go together.
178
204
  *
179
205
  * @param description The rocket, its variant and any other part in a slot.
180
- * @param options `detail`: `"coarse"` leaves out the small 3D-only detail of a very detailed part, for a fast first view; left out, every piece is made.
206
+ * @param options `detail`: `"coarse"` leaves out the small 3D-only detail of a very detailed part, for a fast first view; left out, every piece is made. `beside`: the real Starship stack (Block 3) stands beside it at true scale, on the same ground, and `boundsOf`, `placesOf`, `glb` and `svg` take it in; Starship itself has nothing beside it.
181
207
  * @returns The built rocket. It throws a `FitError` when the description does not go together.
182
208
  * @example
183
209
  * ```ts
@@ -194,7 +220,7 @@ export function build(description, options = {}) {
194
220
  const before = detail.level;
195
221
  detail.level = options.detail ?? "full";
196
222
  try {
197
- const built = assembled(report);
223
+ const built = assembled(report, options.beside && canBeside(description.rocket) ? build(BESIDE_ROCKET, { detail: "full" }) : undefined);
198
224
  // a part's pieces are made when it is assembled, at this level: the lazy parts of a heavy part keep the level they were made at
199
225
  return built;
200
226
  }
@@ -232,4 +258,4 @@ export function buildPart(id) {
232
258
  };
233
259
  }
234
260
  export { toGlb };
235
- export { COMPARES, lineupSvg } from "./lineup.js";
261
+ export { COMPARES, lineupSvg, REAL_CRAFT } from "./lineup.js";
@@ -1,5 +1,7 @@
1
1
  import { type ViewName } from "../framing.ts";
2
2
  import { type LookName } from "../looks.ts";
3
+ import { type Camera } from "../draw/svg.ts";
4
+ import type { Bounds } from "../model/pieces.ts";
3
5
  /**
4
6
  * Comparison shapes: plain round sizes, for scale, not facts about anything.
5
7
  *
@@ -34,6 +36,34 @@ export declare const COMPARES: {
34
36
  * ```
35
37
  */
36
38
  export type CompareId = keyof typeof COMPARES;
39
+ /**
40
+ * The real craft that lie in the fleet band for scale: Starship with Super Heavy, Saturn V, SLS and the International Space Station. Each is a variant of the package, so its length is the sourced figure, `axis` is the way its length runs in its own model
41
+ * (`y` up the stack, nose up; `x` along the station's long axis), and a rocket points its nose right. They are never scaled: they are drawn at the lineup's own scale.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * import { REAL_CRAFT } from "@johnmorrisdotca/uchusen-rockets/build";
46
+ *
47
+ * console.log(REAL_CRAFT.map((craft) => craft.name));
48
+ * ```
49
+ */
50
+ export declare const REAL_CRAFT: readonly [{
51
+ readonly key: "starship:v3";
52
+ readonly name: "Starship with Super Heavy";
53
+ readonly axis: "y";
54
+ }, {
55
+ readonly key: "saturn-v:saturn-v";
56
+ readonly name: "Saturn V";
57
+ readonly axis: "y";
58
+ }, {
59
+ readonly key: "sls:block-1";
60
+ readonly name: "SLS Block 1";
61
+ readonly axis: "y";
62
+ }, {
63
+ readonly key: "iss:iss";
64
+ readonly name: "ISS";
65
+ readonly axis: "x";
66
+ }];
37
67
  /**
38
68
  * One thing standing in a lineup: its key (`starship:v3` or a comparison's name), its label and its height in metres.
39
69
  *
@@ -72,6 +102,8 @@ export interface LineupOptions {
72
102
  show?: string;
73
103
  ruler?: boolean;
74
104
  labels?: boolean;
105
+ /** Real craft for scale: Starship with Super Heavy, Saturn V, SLS and the ISS lie sideways in the fleet band, at the same scale as the fleet ships, nose right, each labelled with its name and length. Left out: not drawn. Only where the fleet band is. */
106
+ real?: boolean;
75
107
  /** `rockets` or `fleet` leaves out the other kind; `all` (the default) draws both. A fleet ship (a rocket whose id starts with `fleet-`) lies lengthwise in its own band below the rockets. */
76
108
  filter?: "all" | "rockets" | "fleet";
77
109
  /** Shrinks the scale, never grows it, until the whole drawing fits this many pixels across and down (either may be left out). Without it the scale is `unit`, as it always was. */
@@ -84,7 +116,7 @@ export interface LineupOptions {
84
116
  * The whole picture as SVG text. Unknown items are left out.
85
117
  *
86
118
  * @param options The items and how they are drawn.
87
- * @returns The picture as SVG text, the items it holds, its width and height and the scale it was drawn at (pixels a metre: less than `unit` when `fit` shrank it).
119
+ * @returns The picture as SVG text, the items it holds, the real craft lying beside the fleet (`real`, with their lengths), its width and height and the scale it was drawn at (pixels a metre: less than `unit` when `fit` shrank it).
88
120
  * @example
89
121
  * ```ts
90
122
  * import { lineupSvg } from "@johnmorrisdotca/uchusen-rockets/build";
@@ -96,7 +128,13 @@ export interface LineupOptions {
96
128
  export declare function lineupSvg(options: LineupOptions): {
97
129
  svg: string;
98
130
  items: LineupItem[];
131
+ real: LineupItem[];
99
132
  width: number;
100
133
  height: number;
101
134
  unit: number;
102
135
  };
136
+ /**
137
+ * The camera of a real craft lying in the fleet band, square on to its windward side: a rocket (`y`) points its nose right with its windward side towards us, and the station (`x`) runs along its long axis with y up. Orthographic at the lineup's scale, so
138
+ * a metre is the same on it as on every ship.
139
+ */
140
+ export declare function lyingCamera(axis: "x" | "y", b: Bounds, x0: number, baseY: number, unit: number): Camera;
@@ -1,4 +1,5 @@
1
1
  // Rockets and what to compare them with, side by side on one baseline at one scale, as one drawing with a ruler. Orthographic, so a metre is the same on every one. Pure.
2
+ import { BESIDE_NAME } from "../beside.js";
2
3
  import { build, drawnPieces } from "./index.js";
3
4
  import { check } from "../fit.js";
4
5
  import { axesOf, cleanView, directionOf, VIEWS } from "../framing.js";
@@ -20,6 +21,23 @@ export const COMPARES = {
20
21
  person: { name: "A person, 1.8 m", height: 1.8, width: 0.5 },
21
22
  building: { name: "A ten-storey building, 30 m", height: 30, width: 18 },
22
23
  };
24
+ /**
25
+ * The real craft that lie in the fleet band for scale: Starship with Super Heavy, Saturn V, SLS and the International Space Station. Each is a variant of the package, so its length is the sourced figure, `axis` is the way its length runs in its own model
26
+ * (`y` up the stack, nose up; `x` along the station's long axis), and a rocket points its nose right. They are never scaled: they are drawn at the lineup's own scale.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import { REAL_CRAFT } from "@johnmorrisdotca/uchusen-rockets/build";
31
+ *
32
+ * console.log(REAL_CRAFT.map((craft) => craft.name));
33
+ * ```
34
+ */
35
+ export const REAL_CRAFT = [
36
+ { key: "starship:v3", name: BESIDE_NAME, axis: "y" },
37
+ { key: "saturn-v:saturn-v", name: "Saturn V", axis: "y" },
38
+ { key: "sls:block-1", name: "SLS Block 1", axis: "y" },
39
+ { key: "iss:iss", name: "ISS", axis: "x" },
40
+ ];
23
41
  const xmlText = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
24
42
  const n = (v) => (Math.round(v * 10) / 10).toString();
25
43
  /** A rocket painted black all over vanishes on the dark ground of a lineup, so it takes the line look's colour for its outline. Only the lines change: its shape and its figures stay. */
@@ -47,7 +65,7 @@ function comparison(id, x, base, unit, line, fill) {
47
65
  * The whole picture as SVG text. Unknown items are left out.
48
66
  *
49
67
  * @param options The items and how they are drawn.
50
- * @returns The picture as SVG text, the items it holds, its width and height and the scale it was drawn at (pixels a metre: less than `unit` when `fit` shrank it).
68
+ * @returns The picture as SVG text, the items it holds, the real craft lying beside the fleet (`real`, with their lengths), its width and height and the scale it was drawn at (pixels a metre: less than `unit` when `fit` shrank it).
51
69
  * @example
52
70
  * ```ts
53
71
  * import { lineupSvg } from "@johnmorrisdotca/uchusen-rockets/build";
@@ -78,6 +96,19 @@ function lying(built) {
78
96
  const b = built.boundsOf("together");
79
97
  return { length: b.max[1] - b.min[1], beam: b.max[0] - b.min[0], height: b.max[2] - b.min[2], from: b.min[1], low: b.min[2] };
80
98
  }
99
+ /**
100
+ * The camera of a real craft lying in the fleet band, square on to its windward side: a rocket (`y`) points its nose right with its windward side towards us, and the station (`x`) runs along its long axis with y up. Orthographic at the lineup's scale, so
101
+ * a metre is the same on it as on every ship.
102
+ */
103
+ export function lyingCamera(axis, b, x0, baseY, unit) {
104
+ const along = axis === "y" ? 1 : 0;
105
+ return {
106
+ project: (p) => [x0 + (p[along] - b.min[along]) * unit, axis === "y" ? baseY + (p[0] - b.max[0]) * unit : baseY - (p[1] - b.min[1]) * unit],
107
+ depth: (p) => -p[2],
108
+ toward: () => [0, 0, 1],
109
+ above: false,
110
+ };
111
+ }
81
112
  function draw(options, fleetKeys, unit) {
82
113
  const { items, look = DEFAULT_LOOK, view = "front", ruler = true, labels = true } = options;
83
114
  const angle = VIEWS[cleanView(view)];
@@ -172,8 +203,44 @@ function draw(options, fleetKeys, unit) {
172
203
  longest = Math.max(longest, length);
173
204
  fleetY = baseY + (labels ? 26 : 12);
174
205
  }
206
+ // the real craft for scale lie in the same band, one row each, at the same scale: a rocket nose right, the station along its long axis
207
+ const real = [];
208
+ let realEnd = 0;
209
+ if (ships.length && options.real)
210
+ for (const craft of REAL_CRAFT) {
211
+ const [rocket, variant] = craft.key.split(":");
212
+ const named = variantOf(rocket, variant);
213
+ if (!named || !check({ rocket, variant: named.id }).ok)
214
+ continue;
215
+ const built = build({ rocket, variant: named.id });
216
+ const b = built.boundsOf("together");
217
+ // a rocket's long axis is y and the picture's up is -x (its windward side faces us); the station's is x, with y up
218
+ const along = craft.axis === "y" ? 1 : 0;
219
+ const length = b.max[along] - b.min[along];
220
+ const tall = craft.axis === "y" ? b.max[0] - b.min[0] : b.max[1] - b.min[1];
221
+ const baseY = fleetY + 6 + tall * unit;
222
+ const x0 = rulerRoom;
223
+ const cam = lyingCamera(craft.axis, b, x0, baseY, unit);
224
+ const show = built.shows[0];
225
+ const edge = edgeOf(built, look);
226
+ for (const place of built.placesOf(show).filter((one) => one.shown).sort((a, c) => a.at[2] - c.at[2])) {
227
+ const part = built.parts.find((p) => p.slot === place.slot);
228
+ if (part)
229
+ fleetBody.push(drawPart(drawnPieces(part, show === "together"), place.at, cam, look, 0.7, edge));
230
+ }
231
+ fleetBody.push(`<path d="M${n(x0)} ${n(baseY)}H${n(x0 + length * unit)}" stroke="currentColor" stroke-width=".6" opacity=".4"/>`);
232
+ // the length is the sourced one: a rocket's height, the station's overall length
233
+ const sourced = craft.axis === "y" ? named.facts.height.value : named.facts.diameter.value;
234
+ const metres = typeof sourced === "number" ? sourced : Math.round(length * 10) / 10;
235
+ const text = `${craft.name}, ${metres.toLocaleString("en-US")} m`;
236
+ if (labels)
237
+ fleetBody.push(`<text x="${n(x0 + length * unit + 8)}" y="${n(baseY - 2)}" font-size="11" fill="currentColor">${xmlText(text)}</text>`);
238
+ real.push({ key: craft.key, label: craft.name, height: metres });
239
+ realEnd = Math.max(realEnd, x0 + length * unit + (labels ? 8 + text.length * 6.2 : 0));
240
+ fleetY = baseY + 10;
241
+ }
175
242
  if (ships.length) {
176
- width = Math.max(width, rulerRoom + longest * unit + 8);
243
+ width = Math.max(width, rulerRoom + longest * unit + 8, realEnd + 8);
177
244
  if (ruler) {
178
245
  // the metres, along the ground of the fleet band, the same scale as the rockets' ruler
179
246
  const steps = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000];
@@ -196,5 +263,5 @@ function draw(options, fleetKeys, unit) {
196
263
  rule += `<text x="${rulerRoom - 16}" y="${n(base - tallest * unit - 10)}" text-anchor="end" font-size="10" fill="currentColor" opacity=".85">m</text><path d="M${rulerRoom - 8} ${n(base)}H${n(width)}" stroke="currentColor" stroke-width=".6" opacity=".4"/>`;
197
264
  }
198
265
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${n(width)} ${n(height)}" width="${n(width)}" height="${n(height)}" role="img" aria-label="${xmlText(`To scale: ${[...known.map((item) => item.label), ...ships.map((ship) => ship.label)].join(", ")}`)}">${defsOf(look)}${rule}${body.join("")}${fleetBody.join("")}</svg>`;
199
- return { svg, width, height, items: [...known.map(({ key, label, height: h }) => ({ key, label, height: h })), ...ships.map((ship) => ({ key: ship.key, label: ship.label, height: ship.size.length }))] };
266
+ return { svg, width, height, real, items: [...known.map(({ key, label, height: h }) => ({ key, label, height: h })), ...ships.map((ship) => ({ key: ship.key, label: ship.label, height: ship.size.length }))] };
200
267
  }
@@ -652,6 +652,14 @@ export declare const SOURCES: {
652
652
  readonly read: "direct";
653
653
  readonly licence: "MIT";
654
654
  };
655
+ readonly "wikipedia-voyager-1": {
656
+ readonly title: "Voyager 1 (its speed leaving the Solar System, 61,602 km/h in 2013)";
657
+ readonly publisher: "Wikipedia";
658
+ readonly url: "https://en.wikipedia.org/wiki/Voyager_1";
659
+ readonly date: "2026-11-04";
660
+ readonly read: "direct";
661
+ readonly licence: "CC BY-SA 4.0";
662
+ };
655
663
  };
656
664
  /**
657
665
  * The id of a source in `SOURCES`.
@@ -91,4 +91,5 @@ export const SOURCES = {
91
91
  "sfn-cots1-presskit": { title: "SpaceX COTS Demo Flight 1 press kit (its mission timeline, page 7)", publisher: "SpaceX, as hosted by Spaceflight Now", url: "https://spaceflightnow.com/falcon9/002/cots1_presskit.pdf", date: "2026-11-03", read: "direct" },
92
92
  "sfn-flight-35": { title: "Timeline of SpaceX's 35th Falcon 9 launch", publisher: "Spaceflight Now", url: "https://spaceflightnow.com/?p=24998", date: "2017-05-31", read: "direct" },
93
93
  "uchusen-fleet-ship-02": { title: "ISV 加美鳴 (かみなり) Kaminari, SPX-207, the Interstellar Heavy Transport: an original SPX fleet design (docs/fleet/ship-02.md)", publisher: "John Morris, uchusen-rockets", url: "https://github.com/johnmorrisdotca/uchusen-rockets/blob/main/docs/fleet/ship-02.md", date: "2026-11-04", read: "direct", licence: "MIT" },
94
+ "wikipedia-voyager-1": { title: "Voyager 1 (its speed leaving the Solar System, 61,602 km/h in 2013)", publisher: "Wikipedia", url: "https://en.wikipedia.org/wiki/Voyager_1", date: "2026-11-04", read: "direct", licence: "CC BY-SA 4.0" },
94
95
  };