@johnmorrisdotca/uchusen-rockets 0.6.0 → 0.8.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,17 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.8.0] - 2026-11-04
6
+
7
+ - **The wheel zooms the view.** The element's own hands (the ones a page without the family's view controls gets) now zoom with the wheel over the 3D view, as a pinch did; before, the wheel zoomed only once the view had focus. With the pointer anywhere else the page scrolls, and a wheel that starts with the zoom already at its end scrolls the page too, so the page is never trapped. A sideways swipe, or Shift and the wheel, is left to the page. Under the family's view controls (`drive()`) nothing changes: they already zoomed with the wheel (kazoku-ui 0.13).
8
+ - Demo and documentation: the hint under the view, the Element page, the README and the Looks guide (English and Japanese) say that the wheel zooms, not Ctrl or Command with the wheel.
9
+
10
+ ## [0.7.0] - 2026-11-03
11
+
12
+ - Demo: the view controls' Auto-turn speeds are the family's speed buttons, 0.5×, 1× and 2× (kazoku-ui 0.14, `^0.14.0`), in place of Slow, Medium and Fast. The reader's choice is the controls' `pace` attribute and `?pace=2` in the address (the address key was `speed`). The base speed stays `speed="5"` degrees a second, which the buttons multiply, and Turn left and Turn right keep their way at any speed. The copied embed code turns at the same speed as the page (`spin` is the seconds a turn: 144, 72 or 36), where it said `slow` whatever the buttons said. Browser tests hold the three buttons, the speed each sets (sign kept), the address and the copied code, and that an old `?speed=fast` opens at 1×.
13
+ - Fixed: the drawing of a flight (no WebGL, `mode="drawing"`, the embed page before the 3D view) took each phase's own angles for the grid fins, legs and flaps, so every move jumped at a phase while the 3D view mixed them. It now takes the angles the 3D view has at that moment, rounded to 5 degrees: the drawing changes at most about one time in five degrees of a move (17 times for Falcon 9's grid fins opening from 85 degrees), still no more than about 20 draws a second. A browser test seeks across the fins' opening and holds that each angle has its drawing, the drawing changes only when the rounded angle does, and the phase's own drawing is no longer the one for all of it; it fails without the fix.
14
+ - Documentation: the Element page, the Looks guide and the Upgrading guide (English and Japanese) say what the speed buttons do; the README's pictures of the demo show them.
15
+
5
16
  ## [0.6.0] - 2026-11-03
6
17
 
7
18
  - Fixed: a look chosen while the 3D view starts, or while a rocket loads, is no longer lost. `start()` and the rocket change read the look before the load and applied it after, so the attribute said the new look and the view drew the old one; both now read it again after the load (and the parts shown with it). Two browser tests change the look in the turn the canvas appears and again with a variant change, and fail without the fix.
package/README.md CHANGED
@@ -379,7 +379,7 @@ variantOf("starship", "2019").facts.diameter.value; // null
379
379
 
380
380
  - 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.
381
381
  - 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.
382
- - Turning works by drag, touch and the keyboard: the arrow keys turn it, `+` and `-` zoom, Home puts it back.
382
+ - 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.
383
383
  - The demo's controls are at least 44 pixels tall and work with a keyboard.
384
384
 
385
385
  ```html
@@ -154,5 +154,8 @@ export declare class RocketViewer {
154
154
  private fit;
155
155
  private orbit;
156
156
  private key;
157
+ /** 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. */
158
+ private onWheel;
159
+ private wheelTaken;
157
160
  private zoom;
158
161
  }
@@ -18,6 +18,8 @@ import { GroundKit } from "./ground.js";
18
18
  import { PartPlumes, PlumeKit } from "./plumes.js";
19
19
  const IDLE_STOP_MS = 90_000;
20
20
  const MOVE_MS = 1400;
21
+ /** Wheel steps closer together than this belong to one turn of the wheel, so a zoom that reaches its end part-way through does not hand the rest to the page. */
22
+ const WHEEL_GESTURE_MS = 300;
21
23
  /** Longest a draw waits for shaders to build: a browser that never says they are ready still draws. */
22
24
  const COMPILE_MS = 4000;
23
25
  const VIEW_MS = 700;
@@ -190,8 +192,7 @@ export class RocketViewer {
190
192
  this.canvas.style.touchAction = "pan-y";
191
193
  this.controls.addEventListener("start", () => this.touched());
192
194
  this.controls.addEventListener("change", () => this.wake());
193
- this.canvas.addEventListener("focus", () => (this.controls.enableZoom = true));
194
- this.canvas.addEventListener("blur", () => (this.controls.enableZoom = false));
195
+ this.canvas.addEventListener("wheel", this.onWheel, { passive: false });
195
196
  this.canvas.addEventListener("pointerdown", () => this.canvas.focus({ preventScroll: true }));
196
197
  this.canvas.addEventListener("dblclick", () => {
197
198
  if (!this.driven)
@@ -205,7 +206,7 @@ export class RocketViewer {
205
206
  }
206
207
  homeAngle = { az: 32, el: 9 };
207
208
  setLabel(label) {
208
- this.canvas.setAttribute("aria-label", `3D view of ${label}. Drag or use the arrow keys to turn it; double-click or Home puts it back.`);
209
+ this.canvas.setAttribute("aria-label", `3D view of ${label}. Drag or use the arrow keys to turn it and the wheel to zoom; double-click or Home puts it back.`);
209
210
  }
210
211
  /** Loads a rocket and shows it. The camera's angle stays when a rocket is already showing. */
211
212
  async load(built, show, look, angle) {
@@ -954,6 +955,7 @@ export class RocketViewer {
954
955
  this.disposed = true;
955
956
  cancelAnimationFrame(this.frame);
956
957
  document.removeEventListener("visibilitychange", this.onVisibility);
958
+ this.canvas.removeEventListener("wheel", this.onWheel);
957
959
  this.resize.disconnect();
958
960
  this.controls.dispose();
959
961
  this.disposeParts();
@@ -1121,6 +1123,7 @@ export class RocketViewer {
1121
1123
  this.canvas.dataset.azimuth = angle.az.toFixed(2);
1122
1124
  this.canvas.dataset.elevation = angle.el.toFixed(2);
1123
1125
  this.canvas.dataset.moving = this.pose ? "true" : "false";
1126
+ this.canvas.dataset.distance = this.camera.position.distanceTo(this.controls.target).toFixed(2);
1124
1127
  if (this.driven) {
1125
1128
  this.canvas.dataset.zoom = this.driven.zoom.toFixed(2);
1126
1129
  this.canvas.dataset.pan = `${Math.round(this.driven.panX)},${Math.round(this.driven.panY)}`;
@@ -1180,6 +1183,29 @@ export class RocketViewer {
1180
1183
  move();
1181
1184
  this.wake();
1182
1185
  }
1186
+ /** 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. */
1187
+ onWheel = (event) => {
1188
+ if (this.driven)
1189
+ return;
1190
+ const pinch = event.ctrlKey || event.metaKey;
1191
+ if (!pinch && (event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY)))
1192
+ return;
1193
+ const pixels = event.deltaMode === 1 ? event.deltaY * (pinch ? 5 : 33) : event.deltaMode === 2 ? event.deltaY * 800 : event.deltaY;
1194
+ const factor = pinch ? Math.exp(-pixels * 0.01) : MathUtils.clamp(Math.exp(-pixels * 0.002), 0.5, 2);
1195
+ const length = this.camera.position.distanceTo(this.controls.target);
1196
+ const atEnd = factor >= 1 ? length <= this.controls.minDistance * 1.0001 : length >= this.controls.maxDistance * 0.9999;
1197
+ const now = Number.isFinite(event.timeStamp) ? event.timeStamp : 0;
1198
+ if (!pinch && atEnd && !(now >= this.wheelTaken && now - this.wheelTaken < WHEEL_GESTURE_MS))
1199
+ return;
1200
+ event.preventDefault();
1201
+ this.wheelTaken = now;
1202
+ if (atEnd)
1203
+ return;
1204
+ this.touched();
1205
+ this.zoom(1 / factor);
1206
+ this.wake();
1207
+ };
1208
+ wheelTaken = Number.NEGATIVE_INFINITY;
1183
1209
  zoom(factor) {
1184
1210
  const offset = this.camera.position.clone().sub(this.controls.target);
1185
1211
  const length = MathUtils.clamp(offset.length() * factor, this.controls.minDistance, this.controls.maxDistance);
package/dist/element.js CHANGED
@@ -503,7 +503,8 @@ export class UchusenRocket extends HTMLBase {
503
503
  const from = this.driven ? turnedBy(angle, this.driven.yaw, this.driven.tilt) : (this.viewer?.currentAngle() ?? angle);
504
504
  const label = this.getAttribute("label") ?? this.builtNow.report.label;
505
505
  const lit = flying ? (media("(prefers-reduced-motion: reduce)") ? false : flying.burns) : this.engineLights();
506
- const moves = flying ? Object.fromEntries(Object.entries(flying.phase.pose).flatMap(([slot, pose]) => (pose.moves ? [[slot, pose.moves]] : []))) : undefined;
506
+ // the angles the 3D view has now, to 5 degrees: the drawing changes a few times a phase, not once a frame
507
+ const moves = flying ? Object.fromEntries(Object.entries(flying.poses).flatMap(([slot, pose]) => { const turned = Object.entries(pose.moves).map(([name, angle]) => [name, Math.round(angle / 5) * 5]).filter(([, angle]) => angle !== 0); return turned.length ? [[slot, Object.fromEntries(turned)]] : []; })) : undefined;
507
508
  const key = `${this.key}|${look}|${show}|${from.az.toFixed(2)}|${from.el.toFixed(2)}|${label}|${flying ? flying.index : ""}|${JSON.stringify(lit)}|${JSON.stringify(moves ?? {})}`;
508
509
  let node = this.drawing.firstElementChild;
509
510
  if (key !== this.drawn.key || !node) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/uchusen-rockets",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",
@@ -125,7 +125,7 @@
125
125
  "@gltf-transform/extensions": "4.5.1",
126
126
  "@gltf-transform/functions": "4.5.1",
127
127
  "@johnmorrisdotca/chikyu-earth": "0.10.0",
128
- "@johnmorrisdotca/kazoku-ui": "^0.12.0",
128
+ "@johnmorrisdotca/kazoku-ui": "^0.14.0",
129
129
  "@playwright/test": "1.64.0",
130
130
  "@sveltejs/vite-plugin-svelte": "^7.3.1",
131
131
  "@types/node": "24.10.1",