@johnmorrisdotca/uchusen-rockets 0.14.0 → 0.15.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,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.15.0] - 2026-11-10
6
+
7
+ - **Credits on every demo page** (MODULES-813): the footer of every page names who is credited and links to every credit (NOTICE.md), in English and Japanese, and the bare embed frame carries a credit link in its corner. NOTICE.md now credits the journey's planet pictures. A test checks each page on a desk and on a phone. The README opens with a picture of the demo on a desk and on a phone, who it is for, a row of pages to try, a Limits section and how to cite it, and a CITATION.cff.
8
+
9
+ - **Romaryū draws its docked ships simple until you come close** (MODULES-844): the four whole Miyamotos in its slots are built in their coarse form at first, so the carrier draws 1.7 million triangles at Fit instead of 5.3 million. When the carrier's narrow side is 150 pixels or more on screen, they are built in full, once. Reloading the same craft keeps the fittings' level of detail.
10
+ - **The demo takes kazoku-ui 0.28: a plain drag turns at every zoom, at most 30 frames a second, CPU-drawn page pictures in the tests** (MODULES-745, MODULES-752, MODULES-767). The Move/Turn choice is gone from the family, so the rocket and comparison pages no longer set it, and the comparison bar drops its `drag:cd` address key. Ctrl or Command and a drag, and Space and a drag, that start on a floating craft now move the view instead of throwing the craft; a double-click on a craft fits a zoomed view, as one beside it does. On the journey, the painted model now moves with the picture when the stage is moved by the wheel or a drag (it stayed put, so the plain wheel seemed to do nothing), and Fit puts it back; it still zooms with its own bar. The comparison's `drag-zoom` test pans with the middle button now that the Move button is gone, and the played-timeline freeze tests read their last count once it has settled, as they read their first (MODULES-853). `checkGestures` holds every pan on the comparison and the journey with no exceptions left but the ones a view has by design; the fleet comparison's Ctrl and Space drags still move only one step on the Linux CI machine, so they stay named there (MODULES-767 is open). The tests and the usage check take `SOFTWARE_GL_ARGS` from kazoku-ui instead of a copy.
11
+ - **The demo takes kazoku-ui 0.24: a zoom readout, the Wide layout and the gesture check** (MODULES-717, MODULES-743). The poster's zoom slider shows the zoom in the family's form (1×, 2.4×); every view bar already did, and the e2e now holds that the readout follows the buttons, the wheel and a pinch and is announced. The shared Wide toggle (button and T key, kept in `?wide=on` and on the device) is on every demo page with a side panel: the view takes the width, 16:9 up to 80vh, and the panel moves below it. A phone stays one column. `e2e/gestures.demo.ts` runs kazoku-ui's `checkGestures` on the rocket, the fleet record, the drones, the yard, the comparison, the fleet comparison and the journey; a gesture a view lacks is named with its reason. The drones page now joins the shared bar (`bindStage`), every stage gets a `tabindex`, and the comparison lineup draws its pan.
5
12
  - **Click a body to fly there** (MODULES-485): on Compare, a planet, moon, dwarf planet or belt mark moves the camera onto that body. The address keeps the place.
6
13
  - **The propellant booms turn with the rings** (MODULES-450): Hanabusa's two booms, and the hub drums and cross-beams, turn on the rings' bearing. The industrial hub turns with the industrial wheel. The keel and the axial lift stay still.
7
14
  - **Kyotori's mast slides out** (MODULES-450): on the flared wings the outer lattice slides 22 m out of the inner mast and back. The blankets keep turning. The full wings stay the extended drawing.
package/NOTICE.md CHANGED
@@ -131,3 +131,7 @@ The Painted look's pictures, copied beside the demo page from chikyu-earth's `di
131
131
  | `day-*.webp`, and `water-*.webp`, made from it | Blue Marble: Next Generation, September 2004 (Visible Earth 73801) | NASA Earth Observatory (Reto Stöckli, Blue Marble Next Generation) |
132
132
  | `night-*.webp` | Earth at Night, Black Marble 2016 (Visible Earth 144898) | NASA Earth Observatory, Suomi NPP VIIRS data (Miguel Roman, NASA's Goddard Space Flight Center) |
133
133
  | `clouds-*.webp` | Blue Marble clouds (Visible Earth 57747) | NASA Earth Observatory (Reto Stöckli) |
134
+
135
+ ## The journey's planets and moons
136
+
137
+ The planets' and moons' pictures on the journey page are copied beside the demo page from `@johnmorrisdotca/wakusei-planets` (MIT, a dev dependency here), from its `dist/textures/`. They are not in the published package. Each is public-domain imagery from NASA, JPL, USGS and others, or a CC BY 4.0 map (Solar System Scope, and the Sketchfab authors Shady Tex and Nestaeric), and wakusei-planets' own [NOTICE.md](https://github.com/johnmorrisdotca/wakusei-planets/blob/main/NOTICE.md) names the source, the licence and the credit of each picture. Whoever copies those pictures from this site must credit them as that file says. Where the planets stand on that page is worked out from NASA JPL's approximate orbit elements, through the same package.
package/README.md CHANGED
@@ -16,6 +16,34 @@ Uchūsen means "spaceship". Each rocket comes in every variant a source document
16
16
 
17
17
  <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/compare.html">Compare craft</a> · <a href="https://johnmorrisdotca.github.io/uchusen-rockets/docs/">Docs site</a></p>
18
18
 
19
+ **[Open the demo](https://johnmorrisdotca.github.io/uchusen-rockets/)**: rockets drawn from sourced sizes that you turn, take apart, set side by side to scale and fly to another world, in English and Japanese. It runs in the browser of a phone, a tablet or a computer: nothing to install, no account, and it is free.
20
+
21
+ <p align="center"><img alt="The journey page on a desk: a Falcon Heavy leaving the Earth for Mars, with the planets to pick from and the launch dates beside it" src="https://raw.githubusercontent.com/johnmorrisdotca/uchusen-rockets/main/docs/images/journey-desk.webp" width="620"> <img alt="The same page on a phone: the Falcon Heavy above the Earth, with the view buttons and the zoom buttons under it" src="https://raw.githubusercontent.com/johnmorrisdotca/uchusen-rockets/main/docs/images/journey-phone.webp" width="200"></p>
22
+
23
+ Every size on a rocket names its source, and a size no source gives is left blank, never estimated. The limits of that picture are in [Limits](#limits), and every model and picture is credited in [NOTICE.md](NOTICE.md) and in the footer of each demo page.
24
+
25
+ ## Who it is for
26
+
27
+ - **Rocket geeks:** every variant with its own sourced sizes ([Starship Block 3, each size with its source](https://johnmorrisdotca.github.io/uchusen-rockets/?rocket=starship&variant=v3&look=painted)), [the engines from below](https://johnmorrisdotca.github.io/uchusen-rockets/?rocket=starship&variant=v3&show=booster&view=below&look=painted), [the parts taken apart](https://johnmorrisdotca.github.io/uchusen-rockets/?rocket=falcon-9&variant=block-5&show=apart&look=painted), [the block-by-block comparison](https://johnmorrisdotca.github.io/uchusen-rockets/#compare) and [the fleet database](https://johnmorrisdotca.github.io/uchusen-rockets/fleet-db.html).
28
+ - **Teachers and students:** [rockets to scale beside a person and a ten-storey building](https://johnmorrisdotca.github.io/uchusen-rockets/#compare), and [the journey to another world](https://johnmorrisdotca.github.io/uchusen-rockets/journey.html) from the planets' real positions, with the numbers as it goes. Every size has its source on the page, so a class can ask where a figure comes from.
29
+ - **Model makers and writers:** the drawings as SVG and the meshes as `.glb` files, with the source of each dimension ([assets guide](docs/guides/assets-and-3d-files.md)); [the code builder](https://johnmorrisdotca.github.io/uchusen-rockets/builder.html) copies the code, or saves an SVG or a PNG, for the rocket you chose. Credit what you show: a painted model's licence travels with it.
30
+ - **Researchers and outreach staff:** every figure is a value, its source and its date, and where sources disagree the page lists them. Read [Limits](#limits) first, then [NOTICE.md](NOTICE.md) and the [sources guide](docs/guides/variants-and-sources.md), and see [How to cite](#how-to-cite).
31
+ - **Developers:** one tag, no build step, and a drawing that is there before any 3D has loaded; and rockets as typed data, with a check that says whether a mix of parts goes together. The install and the API are below.
32
+
33
+ ## Try it now, nothing to install
34
+
35
+ **Fly:** **[A journey to Mars](https://johnmorrisdotca.github.io/uchusen-rockets/journey.html)** (a Falcon Heavy from the Earth to Mars; pick another world, craft or date) · **[The same page in Japanese](https://johnmorrisdotca.github.io/uchusen-rockets/journey.html?lang=ja)**
36
+
37
+ **To scale:** **[Every rocket beside a person and a building](https://johnmorrisdotca.github.io/uchusen-rockets/#compare)** · **[Rockets of the World, the poster](https://johnmorrisdotca.github.io/uchusen-rockets/poster.html)** · **[Three ships of the SPX fleet side by side](https://johnmorrisdotca.github.io/uchusen-rockets/compare.html?layout=lineup)**
38
+
39
+ **Look closer:** **[Starship Block 3 taken apart](https://johnmorrisdotca.github.io/uchusen-rockets/?rocket=starship&variant=v3&show=apart&look=painted)** · **[Super Heavy from below](https://johnmorrisdotca.github.io/uchusen-rockets/?rocket=starship&variant=v3&show=booster&view=below&look=painted)** · **[The fleet database](https://johnmorrisdotca.github.io/uchusen-rockets/fleet-db.html)** · **[The yard](https://johnmorrisdotca.github.io/uchusen-rockets/yard.html)**
40
+
41
+ **Put one on your page:** **[The code builder](https://johnmorrisdotca.github.io/uchusen-rockets/builder.html)** · **[日本語](https://johnmorrisdotca.github.io/uchusen-rockets/?lang=ja)** · **[English](https://johnmorrisdotca.github.io/uchusen-rockets/?lang=en)**
42
+
43
+ ## For teachers and families
44
+
45
+ Uchusen Rockets works on a phone or a tablet as well as on a computer, in the browser, and it is free: the code is MIT-licensed, and nothing is installed or signed up for. A class can [fly a journey](https://johnmorrisdotca.github.io/uchusen-rockets/journey.html), set [every rocket beside a person and a building](https://johnmorrisdotca.github.io/uchusen-rockets/#compare) to see how big they are, or open [the poster](https://johnmorrisdotca.github.io/uchusen-rockets/poster.html), and then look at where a size comes from: each one links its source. [The scale guide](docs/guides/scale-lineups.md) and [the journey guide](docs/guides/journey.md) say what each page shows, and [the embed guide](docs/guides/embed-on-your-site.md) shows how to put a rocket on your own page. The painted models are other people's work under Creative Commons licences, credited in [NOTICE.md](NOTICE.md) and at the foot of each demo page.
46
+
19
47
  <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>
20
48
 
21
49
  <p align="center">
@@ -45,13 +73,6 @@ npm install @johnmorrisdotca/uchusen-rockets three
45
73
 
46
74
  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.
47
75
 
48
- ## Who it is for
49
-
50
- - **A developer who wants a rocket on a page** with one tag, no build step, and a drawing that is there before any 3D has loaded.
51
- - **A writer or teacher** who needs a rocket to scale, beside a person and a building, with every size traceable to a source.
52
- - **A maker of tools** who wants rockets as data: variants, sourced sizes, parts that carry their own joints, and a check that says whether a mix goes together.
53
- - **Anyone who must credit what they show**: a painted rocket's licence travels with it, in every form it is copied.
54
-
55
76
  ## Features
56
77
 
57
78
  - Twenty-three kinds of thing, from Starship to the Hubble Space Telescope, each in every variant a source documents, with its own sizes.
@@ -388,8 +409,6 @@ Each guide has a Japanese twin in [docs/guides/ja](docs/guides/ja).
388
409
  | [React and other frameworks](docs/guides/react-and-frameworks.md) | React, Vue, Svelte, Angular |
389
410
  | [Bundlers and CDNs](docs/guides/bundlers-and-cdn.md) | unpkg, Vite, webpack |
390
411
  | [TypeScript and Node](docs/guides/typescript-and-node.md) | Types, and the core in Node |
391
- | [Bundlers and CDNs](docs/guides/bundlers-and-cdn.md) | unpkg, Vite, webpack |
392
- | [TypeScript and Node](docs/guides/typescript-and-node.md) | Types, and the core in Node |
393
412
  | [Ready files and 3D models](docs/guides/assets-and-3d-files.md) | The `.glb` and `.svg` files |
394
413
  | [Variants and sourced sizes](docs/guides/variants-and-sources.md) | Why a figure names its source |
395
414
  | [Licences, credits and trademarks](docs/guides/licences-and-credits.md) | What is MIT, CC BY, public domain, ODbL |
@@ -399,6 +418,10 @@ Each guide has a Japanese twin in [docs/guides/ja](docs/guides/ja).
399
418
 
400
419
  ## Limits
401
420
 
421
+ Uchusen Rockets is made for learning, teaching, comparing and drawing. It is not made for engineering, for mission planning or for anything that needs more than its sources give, and it says where each number stops:
422
+
423
+ - **Sizes.** A figure is as exact as its source, shown to the digits the source gives. Where sources disagree the page lists them side by side (Block 3's height has several), and a rocket announced but not built, such as Starship Block 4, shows its sizes as estimates.
424
+ - **The journey.** The planets' positions are real, from NASA JPL's approximate orbit elements, good to about an arc minute (about 20,000 km at Mars). The trips are simple models (Hohmann transfers with patched conics, and flip-and-burn for the fleet ships), checked against the figures NASA prints (about 259 days to Mars), so they show what a trip costs, not what a mission would fly; a craft's burn length and turn times that no source gives are marked "est.".
402
425
  - A rocket is only as good as its sources. A figure no source gives is blank, never estimated, and a blank is shown as one.
403
426
  - A flame's length and its shock diamonds, how parts move between flight phases, smoke and the ground falling away are pictures, not measurements.
404
427
  - A part with no licensed model is painted by this package's own code from photographs and sizes. It is a likeness, not a scan.
@@ -412,12 +435,18 @@ import { variantOf } from "@johnmorrisdotca/uchusen-rockets";
412
435
  variantOf("starship", "2019").facts.diameter.value; // null
413
436
  ```
414
437
 
438
+ ## How to cite
439
+
440
+ > Morris, J. (2026). *Uchusen Rockets* (version 0.15.0) [Computer software]. https://github.com/johnmorrisdotca/uchusen-rockets
441
+
442
+ GitHub's "Cite this repository" button reads [CITATION.cff](CITATION.cff) and gives BibTeX and APA. Cite the version you used (`npm ls @johnmorrisdotca/uchusen-rockets`), and credit the people behind the models and the numbers: [NOTICE.md](NOTICE.md) says who made each one and under what terms.
443
+
415
444
  ## Accessibility
416
445
 
417
446
  - The element has an accessible name from its `label` attribute, or from the fit report's label (such as "Block 3 (V3)") when there is none. The 3D view's canvas says how to turn it.
418
447
  - A reader who asked for reduced motion gets no spin, no flames, no played flight and a cut instead of an animated move. The slider and charts still work.
419
448
  - Turning works by drag, touch and the keyboard: the arrow keys turn it, the wheel over the view, a pinch, `+` and `-` zoom, Home puts it back. With the pointer anywhere else the wheel scrolls the page, and so it does when the zoom is already at its end.
420
- - The demo's controls are at least 44 pixels tall and work with a keyboard.
449
+ - The demo's controls are at least 44 pixels tall and work with a keyboard. The view bar shows the zoom (1×, 2.4×) and says it once it settles. The Wide button or the T key (`?wide=on`) gives the view the page's width and moves the panel below it.
421
450
 
422
451
  ```html
423
452
  <uchusen-rocket rocket="starship" variant="v3" label="Starship Block 3, taken apart" show="apart"></uchusen-rocket>
@@ -141,7 +141,7 @@ export declare function drawnPieces(part: {
141
141
  * A rocket from a description. Throws FitError when it does not go together.
142
142
  *
143
143
  * @param description The rocket, its variant and any other part in a slot.
144
- * @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. `scale`: the size, in per cent from 60 to 100, of a Kaminari concept hull (any other craft ignores it); every part, place and length is that share of the full size.
144
+ * @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. `dockedDetail`: `"coarse"` builds a whole ship docked to a carrier (Romaryū's four) coarse even in the full pass, as a page does until it is zoomed in on them; left out, it follows `detail`. `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. `scale`: the size, in per cent from 60 to 100, of a Kaminari concept hull (any other craft ignores it); every part, place and length is that share of the full size.
145
145
  * @returns The built rocket. It throws a `FitError` when the description does not go together.
146
146
  * @example
147
147
  * ```ts
@@ -153,6 +153,7 @@ export declare function drawnPieces(part: {
153
153
  */
154
154
  export declare function build(description: Description, options?: {
155
155
  detail?: DetailLevel;
156
+ dockedDetail?: DetailLevel;
156
157
  beside?: boolean | readonly Description[];
157
158
  named?: boolean;
158
159
  scale?: number;
@@ -53,7 +53,8 @@ export class FitError extends Error {
53
53
  export const HEAVY_PIECES = 2000;
54
54
  // Bound retained geometry when a page visits many of the large fleet models.
55
55
  const memo = new Map();
56
- const memoKey = (id, k) => `${detail.level}/${dockedNow.spec ?? "-"}/${id}${k === 1 ? "" : `@${k}`}`;
56
+ // A carrier's docked whole ships (its "miyamoto-" slots) are made at the docked level; every other part keeps its key, so building them in full makes nothing else again.
57
+ const memoKey = (id, k) => `${detail.level}${id.includes("-miyamoto-") ? `:${detail.docked}` : ""}/${dockedNow.spec ?? "-"}/${id}${k === 1 ? "" : `@${k}`}`;
57
58
  const remember = (key, value) => {
58
59
  memo.set(key, value);
59
60
  if (memo.size > 128) {
@@ -234,7 +235,7 @@ export function drawnPieces(part, together) {
234
235
  * A rocket from a description. Throws FitError when it does not go together.
235
236
  *
236
237
  * @param description The rocket, its variant and any other part in a slot.
237
- * @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. `scale`: the size, in per cent from 60 to 100, of a Kaminari concept hull (any other craft ignores it); every part, place and length is that share of the full size.
238
+ * @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. `dockedDetail`: `"coarse"` builds a whole ship docked to a carrier (Romaryū's four) coarse even in the full pass, as a page does until it is zoomed in on them; left out, it follows `detail`. `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. `scale`: the size, in per cent from 60 to 100, of a Kaminari concept hull (any other craft ignores it); every part, place and length is that share of the full size.
238
239
  * @returns The built rocket. It throws a `FitError` when the description does not go together.
239
240
  * @example
240
241
  * ```ts
@@ -249,8 +250,10 @@ export function build(description, options = {}) {
249
250
  if (!report.ok)
250
251
  throw new FitError(report);
251
252
  const before = detail.level;
253
+ const beforeDocked = detail.docked;
252
254
  const beforeDock = dockedNow.spec;
253
255
  detail.level = options.detail ?? "full";
256
+ detail.docked = options.dockedDetail ?? detail.level;
254
257
  dockedNow.spec = "docked" in options ? (options.docked ?? "") : null;
255
258
  try {
256
259
  const asked = options.beside === true ? (canBeside(description.rocket) ? [BESIDE_ROCKET] : []) : (options.beside || []);
@@ -265,6 +268,7 @@ export function build(description, options = {}) {
265
268
  }
266
269
  finally {
267
270
  detail.level = before;
271
+ detail.docked = beforeDocked;
268
272
  dockedNow.spec = beforeDock;
269
273
  }
270
274
  }
@@ -29,6 +29,8 @@ export interface ViewerOptions {
29
29
  onLost: () => void;
30
30
  /** Said once, when the camera first comes near what is on screen: the page may then build the fine detail. */
31
31
  onNear?: () => void;
32
+ /** Said when a fleet ship fills the view across (its shorter side over `DOCKED_FINE_SPAN` pixels): the page may then build the whole ships docked to it in full. Said on each such frame until it returns true. */
33
+ onZoomedIn?: () => boolean;
32
34
  /** Where the licensed models are. */
33
35
  assets: string;
34
36
  /** Tells the page which painted parts have come in, or "" when the look is not painted. */
@@ -106,6 +108,8 @@ export declare class RocketViewer {
106
108
  private lodSmall;
107
109
  /** 1 is every fitting drawn, 0 is the small-ship hull. It moves over a fifth of a second so the switch does not pop. */
108
110
  private lodFade;
111
+ /** Whether the craft was small before it was loaded again (the same craft built finer): its first decision keeps the same line, so a reload does not bring the fittings back. */
112
+ private lodWas;
109
113
  /** The smooth flood sheets. Cached with their material, so a new rocket reuses them. */
110
114
  private poolSheets;
111
115
  private lampTexture;
@@ -179,6 +183,7 @@ export declare class RocketViewer {
179
183
  private focusing;
180
184
  private focusNow;
181
185
  private nearSaid;
186
+ private zoomedInSaid;
182
187
  private nearAt;
183
188
  /** The compare page asked for the Earth in the scene. The depth stays logarithmic either way. */
184
189
  private orbitOn;
@@ -415,8 +420,11 @@ export declare class RocketViewer {
415
420
  private fit;
416
421
  private orbit;
417
422
  private key;
423
+ /** Space held down: a drag then moves the view, as Ctrl or Command does, and throws no craft. */
424
+ private spaceHeld;
425
+ private onSpace;
418
426
  /** The wheel over the view zooms it, as does a pinch (Ctrl or Command and the wheel). A wheel that starts with the zoom already at its end, or a sideways swipe, leaves the page to scroll. */
419
- /** A drag that starts on a craft throws that craft. Empty space still turns the picture. */
427
+ /** A drag that starts on a craft throws that craft, unless Ctrl, Command or Space asks to move the view. Empty space still turns the picture. */
420
428
  private grabCraft;
421
429
  private onPull;
422
430
  private onRelease;
@@ -39,6 +39,8 @@ const FITTING = new Set(["pipe-steel", "blanket", "blanket-worn", "lamp-amber",
39
39
  /** Drop the fittings below this shorter side, and bring them back only above the higher one, so a slow zoom does not flicker on the line. */
40
40
  const FITTING_OFF = 220;
41
41
  const FITTING_ON = 260;
42
+ /** A fleet ship whose shorter side is this wide on screen is being looked into: the whole ships docked to it are worth their full detail. */
43
+ const DOCKED_FINE_SPAN = 150;
42
44
  /** The fine model is due when the camera has come in, or when a page with the Earth shows the craft large. That page zooms the picture and leaves the camera where it is, so distance alone never sees a ship that fills the frame. */
43
45
  export function fineDue(close, orbit, spanPx) {
44
46
  return close || (orbit && Number.isFinite(spanPx) && spanPx >= FITTING_ON);
@@ -206,6 +208,8 @@ export class RocketViewer {
206
208
  lodSmall = null;
207
209
  /** 1 is every fitting drawn, 0 is the small-ship hull. It moves over a fifth of a second so the switch does not pop. */
208
210
  lodFade = 1;
211
+ /** Whether the craft was small before it was loaded again (the same craft built finer): its first decision keeps the same line, so a reload does not bring the fittings back. */
212
+ lodWas = null;
209
213
  /** The smooth flood sheets. Cached with their material, so a new rocket reuses them. */
210
214
  poolSheets = [];
211
215
  lampTexture = null;
@@ -279,6 +283,7 @@ export class RocketViewer {
279
283
  focusing = false;
280
284
  focusNow = null;
281
285
  nearSaid = false;
286
+ zoomedInSaid = false;
282
287
  nearAt = 0;
283
288
  /** The compare page asked for the Earth in the scene. The depth stays logarithmic either way. */
284
289
  orbitOn = false;
@@ -391,6 +396,8 @@ export class RocketViewer {
391
396
  });
392
397
  this.canvas.addEventListener("keydown", (event) => this.key(event));
393
398
  document.addEventListener("visibilitychange", this.onVisibility);
399
+ document.addEventListener("keydown", this.onSpace);
400
+ document.addEventListener("keyup", this.onSpace);
394
401
  this.resize = new ResizeObserver(() => this.fit());
395
402
  this.resize.observe(holder);
396
403
  this.fit();
@@ -562,6 +569,7 @@ export class RocketViewer {
562
569
  }
563
570
  this.lampPoints = [];
564
571
  this.lampPending = [];
572
+ this.lodWas = craftChanged ? null : this.lodSmall;
565
573
  this.lodSmall = null;
566
574
  this.toss.clear();
567
575
  this.grab = null;
@@ -2218,6 +2226,8 @@ export class RocketViewer {
2218
2226
  this.moonNode = null;
2219
2227
  cancelAnimationFrame(this.frame);
2220
2228
  document.removeEventListener("visibilitychange", this.onVisibility);
2229
+ document.removeEventListener("keydown", this.onSpace);
2230
+ document.removeEventListener("keyup", this.onSpace);
2221
2231
  this.canvas.removeEventListener("wheel", this.onWheel);
2222
2232
  this.resize.disconnect();
2223
2233
  this.controls.dispose();
@@ -2650,7 +2660,7 @@ export class RocketViewer {
2650
2660
  shortest = Math.min(shortest, maxX - minX, maxY - minY);
2651
2661
  }
2652
2662
  this.canvas.dataset.lodSpan = String(Math.round(shortest));
2653
- const limit = this.lodSmall ? FITTING_ON : FITTING_OFF;
2663
+ const limit = (this.lodSmall ?? this.lodWas) ? FITTING_ON : FITTING_OFF;
2654
2664
  return shortest < limit;
2655
2665
  }
2656
2666
  /** The box each visible craft fills, in metres. Each part is looked up once: this runs every frame, and a nested scan of a whole ship made one frame longer than the walk can spare. */
@@ -2952,6 +2962,8 @@ export class RocketViewer {
2952
2962
  this.nearSaid = true;
2953
2963
  this.options.onNear();
2954
2964
  }
2965
+ if (this.options.onZoomedIn && !this.zoomedInSaid && Number.isFinite(span) && span >= DOCKED_FINE_SPAN)
2966
+ this.zoomedInSaid = this.options.onZoomedIn();
2955
2967
  this.needsDraw = false;
2956
2968
  this.frames++;
2957
2969
  this.canvas.dataset.frames = String(this.frames);
@@ -3046,10 +3058,16 @@ export class RocketViewer {
3046
3058
  move();
3047
3059
  this.wake();
3048
3060
  }
3061
+ /** Space held down: a drag then moves the view, as Ctrl or Command does, and throws no craft. */
3062
+ spaceHeld = false;
3063
+ onSpace = (event) => {
3064
+ if (event.key === " " || event.key === "Spacebar")
3065
+ this.spaceHeld = event.type === "keydown";
3066
+ };
3049
3067
  /** The wheel over the view zooms it, as does a pinch (Ctrl or Command and the wheel). A wheel that starts with the zoom already at its end, or a sideways swipe, leaves the page to scroll. */
3050
- /** A drag that starts on a craft throws that craft. Empty space still turns the picture. */
3068
+ /** A drag that starts on a craft throws that craft, unless Ctrl, Command or Space asks to move the view. Empty space still turns the picture. */
3051
3069
  grabCraft(event) {
3052
- if (!this.floating || event.button !== 0 || !event.isPrimary)
3070
+ if (!this.floating || event.button !== 0 || !event.isPrimary || event.ctrlKey || event.metaKey || this.spaceHeld)
3053
3071
  return;
3054
3072
  const craft = this.craftUnder(event);
3055
3073
  if (craft === null) {
package/dist/element.d.ts CHANGED
@@ -204,6 +204,10 @@ export declare class UchusenRocket extends HTMLBase {
204
204
  private fine;
205
205
  private detailFor;
206
206
  private upgrade;
207
+ /** The whole ships docked to a carrier (Romaryū's four) stay coarse in its fine build, where they are a small part of the picture and most of its triangles, until the view is zoomed in on the carrier: then they are built in full, once. */
208
+ private dockedFine;
209
+ /** True once there is nothing left to do; false while the fine build or a carrier is still to come, so the viewer asks again. */
210
+ private upgradeDocked;
207
211
  private panel;
208
212
  private panelFor;
209
213
  /** The panel of a ship that has one: tabs, a Labels toggle and insets that open on a click. `inspector="off"` leaves it out; `labels="on"` starts with the labels showing. */
package/dist/element.js CHANGED
@@ -28,6 +28,8 @@ const HTMLBase = typeof HTMLElement === "undefined" ? class {
28
28
  const RELEASE_MS = 30_000;
29
29
  /** The drawing, followed along a turn, is made again at most this often. */
30
30
  const PLACE_MS = 50;
31
+ /** The carriers whose docked craft are whole ships of this package. */
32
+ const CARRIERS = new Set(["fleet-romaryu"]);
31
33
  let assetBase = null;
32
34
  /**
33
35
  * Where the licensed models are, for every element with no `assets` attribute of its own. Left unset: beside this module, in ./assets/.
@@ -617,7 +619,7 @@ export class UchusenRocket extends HTMLBase {
617
619
  if (key !== this.key || !this.builtNow) {
618
620
  this.key = key;
619
621
  try {
620
- this.builtNow = build(description, { detail: this.detailFor(description), ...this.besideOptions(description), scale: size, ...docked });
622
+ this.builtNow = build(description, { detail: this.detailFor(description), dockedDetail: this.dockedFine ? "full" : "coarse", ...this.besideOptions(description), scale: size, ...docked });
621
623
  }
622
624
  catch (error) {
623
625
  this.builtNow = null;
@@ -787,13 +789,32 @@ export class UchusenRocket extends HTMLBase {
787
789
  return;
788
790
  this.fine = true;
789
791
  try {
790
- this.builtNow = build(description, { detail: "full", ...this.besideOptions(description), scale: this.sizeNow(description), ...this.dockedOption() });
792
+ this.builtNow = build(description, { detail: "full", dockedDetail: this.dockedFine ? "full" : "coarse", ...this.besideOptions(description), scale: this.sizeNow(description), ...this.dockedOption() });
791
793
  }
792
794
  catch {
793
795
  return;
794
796
  }
795
797
  await this.applyRocket();
796
798
  }
799
+ /** The whole ships docked to a carrier (Romaryū's four) stay coarse in its fine build, where they are a small part of the picture and most of its triangles, until the view is zoomed in on the carrier: then they are built in full, once. */
800
+ dockedFine = false;
801
+ /** True once there is nothing left to do; false while the fine build or a carrier is still to come, so the viewer asks again. */
802
+ upgradeDocked() {
803
+ if (this.dockedFine)
804
+ return true;
805
+ if (!this.fine || !this.builtNow || !CARRIERS.has(this.descriptionNow().rocket))
806
+ return false;
807
+ const description = this.descriptionNow();
808
+ this.dockedFine = true;
809
+ try {
810
+ this.builtNow = build(description, { detail: "full", dockedDetail: "full", ...this.besideOptions(description), scale: this.sizeNow(description), ...this.dockedOption() });
811
+ }
812
+ catch {
813
+ return true;
814
+ }
815
+ void this.applyRocket();
816
+ return true;
817
+ }
797
818
  panel = null;
798
819
  panelFor = null;
799
820
  /** The panel of a ship that has one: tabs, a Labels toggle and insets that open on a click. `inspector="off"` leaves it out; `labels="on"` starts with the labels showing. */
@@ -944,6 +965,7 @@ export class UchusenRocket extends HTMLBase {
944
965
  reducedMotion: reduced,
945
966
  onLost: () => this.fallBack("context-lost"),
946
967
  onNear: () => void this.upgrade(),
968
+ onZoomedIn: () => this.upgradeDocked(),
947
969
  assets: assets ? `${assets.replace(/\/$/, "")}/` : defaultBase(),
948
970
  onPainted: (state) => {
949
971
  this.dataset.painted = state;
@@ -1,8 +1,9 @@
1
1
  import type { Piece } from "./pieces.ts";
2
2
  export type DetailLevel = "full" | "coarse";
3
- /** The level in force while a part's pieces are being made. Set by `build`, never by hand. */
3
+ /** The level in force while a part's pieces are being made, and the level of a whole ship docked to a carrier (a page draws those coarse until it is zoomed in on them). Set by `build`, never by hand. */
4
4
  export declare const detail: {
5
5
  level: DetailLevel;
6
+ docked: DetailLevel;
6
7
  };
7
8
  /** The smallest a 3D-only piece may be, in metres, and stay in the coarse level. */
8
9
  export declare const COARSE_MIN = 6;
@@ -1,5 +1,5 @@
1
- /** The level in force while a part's pieces are being made. Set by `build`, never by hand. */
2
- export const detail = { level: "full" };
1
+ /** The level in force while a part's pieces are being made, and the level of a whole ship docked to a carrier (a page draws those coarse until it is zoomed in on them). Set by `build`, never by hand. */
2
+ export const detail = { level: "full", docked: "full" };
3
3
  /** The smallest a 3D-only piece may be, in metres, and stay in the coarse level. */
4
4
  export const COARSE_MIN = 6;
5
5
  const sizeOf = (piece) => {
@@ -141,8 +141,8 @@ export const SLOTS_C = [
141
141
  { slot: "coil-stack", name: "Fusion coil stack and reactor cells", kind: "service-module", at: [0, C.coil.y[0], 0], height: C.shield.y[0] - C.coil.y[0] + 1, diameter: 64, note: "Ten superconducting coil rings on a ceramic bore, and eight reactor cells round it. Descends from Princeton's direct fusion drive and Kilopower.", pieces: () => local(stackAndReactors(), C.coil.y[0]) },
142
142
  { slot: "shadow-shield", name: "Shadow shield", kind: "fairing", at: [0, C.shield.y[0], 0], height: C.shield.y[1] - C.shield.y[0], diameter: 2 * C.shield.rBack, note: "A tiled cone that throws a shadow of the reactors' radiation forward over the shaft, the craft and the crew. Descends from the NERVA and Kilopower shadow shields.", pieces: () => local(shieldDisc(C.shield.rBack, C.shield.rFront, C.shield.y[0], C.shield.y[1], "c-shadow"), C.shield.y[0]) },
143
143
  { slot: "barrels", name: "Propellant barrels: two stations of six", kind: "tank", at: [0, C.barrels.y[0], 0], height: C.barrels.y[1] + C.barrels.L * 1.12 - C.barrels.y[0], diameter: 114, note: "Twelve propellant barrels 30 m across and 110 m long, in two rings of six round the shaft. Descends from the Space Shuttle's external tank and Soyuz's strap-on tanks.", pieces: () => local(barrels(), C.barrels.y[0]) },
144
- // A docked ship is that ship. The full pass builds it in full. The coarse pass builds the coarse ship, and the second of each pair is the first build again.
145
- ...C.craft.slots.map((s, i) => ({ slot: `miyamoto-${i + 1}`, name: s.ship === "miyamoto" ? `Docked craft ${i + 1}: ISV Miyamoto, solar array out` : `Docked craft ${i + 1}: ISV Ariake`, kind: "module", at: [0, s.y, 0], height: 380, diameter: s.ship === "miyamoto" ? 200 : 207, note: s.ship === "miyamoto" ? "ISV Miyamoto's own model at its true size, the gold solar array deployed, docked by its keel in a cradle. Descends from the Space Shuttle on its carrier aircraft and the ISS's visiting vehicles." : "ISV Ariake's own model at its true size, four hangar boxes, the hex engine end and the dawn-gold bands, docked by its keel in a cradle. Descends from the Space Shuttle on its carrier aircraft and the ISS's visiting vehicles.", pieces: () => local(craft(s.az, s.y, s.ship, 1, sceneDetail.level === "coarse" ? "coarse" : undefined), s.y) })),
144
+ // A docked ship is that ship. The full pass builds it in full unless `dockedDetail` asks for it coarse: in full the four were 3.5 of the carrier's 4.8 million triangles, so a page keeps them coarse until it is zoomed in. The coarse pass builds the coarse ship, and the second of each pair is the first build again.
145
+ ...C.craft.slots.map((s, i) => ({ slot: `miyamoto-${i + 1}`, name: s.ship === "miyamoto" ? `Docked craft ${i + 1}: ISV Miyamoto, solar array out` : `Docked craft ${i + 1}: ISV Ariake`, kind: "module", at: [0, s.y, 0], height: 380, diameter: s.ship === "miyamoto" ? 200 : 207, note: s.ship === "miyamoto" ? "ISV Miyamoto's own model at its true size, the gold solar array deployed, docked by its keel in a cradle. Descends from the Space Shuttle on its carrier aircraft and the ISS's visiting vehicles." : "ISV Ariake's own model at its true size, four hangar boxes, the hex engine end and the dawn-gold bands, docked by its keel in a cradle. Descends from the Space Shuttle on its carrier aircraft and the ISS's visiting vehicles.", pieces: () => local(craft(s.az, s.y, s.ship, 1, sceneDetail.level === "coarse" || sceneDetail.docked === "coarse" ? "coarse" : undefined), s.y) })),
146
146
  ...C.craft.slots.map((s, i) => ({ slot: `cradle-${i + 1}`, name: `Docking cradle ${i + 1}`, kind: "mount", at: [0, s.y, 0], height: 366, diameter: 19, note: "Two rails, five clamp towers from the shaft, and umbilical lines that hold a docked craft by its keel. Descends from the ISS common berthing mechanism and the Shuttle's payload-bay latches.", pieces: () => local(cradle(s.az, s.y), s.y) })),
147
147
  berthSlot("c"),
148
148
  { slot: "rings", name: "Habitat rings (two, counter-rotating)", kind: "ring", at: [0, C.ring.y[0], 0], height: ringsHeight, diameter: 239, note: "Two counter-rotating habitat rings 100 m from the axis, at one turn in 24 s for a little under one Earth gravity, with running lights on the rims. Descends from Von Braun's wheel and NASA's Nautilus-X centrifuge.", pieces: habitatRings },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/uchusen-rockets",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Uchusen Rockets 宇宙船: line drawings and 3D views of rockets, as a web component. Every variant with its own sourced sizes, a typed way to assemble a rocket from parts, stack and separate. No logos. three.js is a peer dependency.",
5
5
  "keywords": [
6
6
  "rocket",
@@ -146,7 +146,7 @@
146
146
  "@gltf-transform/extensions": "4.5.1",
147
147
  "@gltf-transform/functions": "4.5.1",
148
148
  "@johnmorrisdotca/chikyu-earth": "0.10.0",
149
- "@johnmorrisdotca/kazoku-ui": "0.20.0",
149
+ "@johnmorrisdotca/kazoku-ui": "^0.28.0",
150
150
  "@johnmorrisdotca/wakusei-planets": "0.17.1",
151
151
  "@playwright/test": "1.64.0",
152
152
  "@sveltejs/vite-plugin-svelte": "^7.3.1",