@johnmorrisdotca/uchusen-rockets 0.1.0 → 0.2.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/NOTICE.md +2 -2
  3. package/README.md +1 -1
  4. package/dist/assets/falcon-9-block-5-apart.painted.svg +1 -1
  5. package/dist/assets/falcon-9-block-5-apart.svg +1 -1
  6. package/dist/assets/falcon-9-block-5-apart.thick.svg +1 -1
  7. package/dist/assets/falcon-9-block-5-dragon-2-apart.painted.svg +1 -1
  8. package/dist/assets/falcon-9-block-5-dragon-2-apart.svg +1 -1
  9. package/dist/assets/falcon-9-block-5-dragon-2-apart.thick.svg +1 -1
  10. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.glb +0 -0
  11. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.painted.svg +1 -1
  12. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.svg +1 -1
  13. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.thick.svg +1 -1
  14. package/dist/assets/falcon-9-block-5-dragon-2.glb +0 -0
  15. package/dist/assets/falcon-9-block-5-second-stage.glb +0 -0
  16. package/dist/assets/falcon-9-block-5-second-stage.painted.svg +1 -1
  17. package/dist/assets/falcon-9-block-5-second-stage.svg +1 -1
  18. package/dist/assets/falcon-9-block-5-second-stage.thick.svg +1 -1
  19. package/dist/assets/falcon-9-block-5.glb +0 -0
  20. package/dist/assets/falcon-9-ft-apart.painted.svg +1 -1
  21. package/dist/assets/falcon-9-ft-apart.svg +1 -1
  22. package/dist/assets/falcon-9-ft-apart.thick.svg +1 -1
  23. package/dist/assets/falcon-9-ft-second-stage.glb +0 -0
  24. package/dist/assets/falcon-9-ft-second-stage.painted.svg +1 -1
  25. package/dist/assets/falcon-9-ft-second-stage.svg +1 -1
  26. package/dist/assets/falcon-9-ft-second-stage.thick.svg +1 -1
  27. package/dist/assets/falcon-9-ft.glb +0 -0
  28. package/dist/assets/falcon-9-v1.1-apart.painted.svg +1 -1
  29. package/dist/assets/falcon-9-v1.1-apart.svg +1 -1
  30. package/dist/assets/falcon-9-v1.1-apart.thick.svg +1 -1
  31. package/dist/assets/falcon-9-v1.1-second-stage.glb +0 -0
  32. package/dist/assets/falcon-9-v1.1-second-stage.painted.svg +1 -1
  33. package/dist/assets/falcon-9-v1.1-second-stage.svg +1 -1
  34. package/dist/assets/falcon-9-v1.1-second-stage.thick.svg +1 -1
  35. package/dist/assets/falcon-9-v1.1.glb +0 -0
  36. package/dist/data/falcon.d.ts +3 -1
  37. package/dist/data/falcon.js +2 -0
  38. package/dist/data/sources.d.ts +8 -0
  39. package/dist/data/sources.js +1 -0
  40. package/dist/element/scene.d.ts +15 -1
  41. package/dist/element/scene.js +109 -6
  42. package/dist/element.d.ts +7 -1
  43. package/dist/element.js +42 -4
  44. package/dist/framing.d.ts +10 -0
  45. package/dist/framing.js +4 -0
  46. package/dist/index.d.ts +2 -2
  47. package/dist/index.js +1 -1
  48. package/dist/model/falcon.d.ts +1 -0
  49. package/dist/model/falcon.js +5 -3
  50. package/dist/model/registry.js +1 -1
  51. package/dist/parts.d.ts +2 -1
  52. package/package.json +2 -1
@@ -7,7 +7,7 @@ import { LineMaterial } from "three/addons/lines/LineMaterial.js";
7
7
  import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
8
8
  import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
9
9
  import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
10
- import { FOV_DEG, frameFor, PICTURE_H } from "../framing.js";
10
+ import { directionOf, FOV_DEG, frameFor, PICTURE_H, turnedBy } from "../framing.js";
11
11
  import { LOOKS } from "../looks.js";
12
12
  import { MATERIALS } from "../model/materials.js";
13
13
  import { paintGlow, paintSky, paintSteel, paintTiles } from "./paint.js";
@@ -69,6 +69,8 @@ void main() {
69
69
  gl_FragColor = vec4(color, 1.0);
70
70
  #include <colorspace_fragment>
71
71
  }`;
72
+ /** The materials of an engine's bell, in this package's own meshes. */
73
+ const NOZZLE_MATERIALS = new Set(["bell", "bell-inside", "engine-metal"]);
72
74
  const asVec = (v) => [v.x, v.y, v.z];
73
75
  export class RocketViewer {
74
76
  canvas;
@@ -99,6 +101,9 @@ export class RocketViewer {
99
101
  needsDraw = true;
100
102
  pose = null;
101
103
  tilt = { azimuth: 0, polar: 0, applied: { azimuth: 0, polar: 0 } };
104
+ driven = null;
105
+ drivenDirty = false;
106
+ lastDistance = 0;
102
107
  resize;
103
108
  disposed = false;
104
109
  constructor(holder, options) {
@@ -162,7 +167,10 @@ export class RocketViewer {
162
167
  this.canvas.addEventListener("focus", () => (this.controls.enableZoom = true));
163
168
  this.canvas.addEventListener("blur", () => (this.controls.enableZoom = false));
164
169
  this.canvas.addEventListener("pointerdown", () => this.canvas.focus({ preventScroll: true }));
165
- this.canvas.addEventListener("dblclick", () => this.setAngle(this.homeAngle, true));
170
+ this.canvas.addEventListener("dblclick", () => {
171
+ if (!this.driven)
172
+ this.setAngle(this.homeAngle, true);
173
+ });
166
174
  this.canvas.addEventListener("keydown", (event) => this.key(event));
167
175
  document.addEventListener("visibilitychange", this.onVisibility);
168
176
  this.resize = new ResizeObserver(() => this.fit());
@@ -202,7 +210,9 @@ export class RocketViewer {
202
210
  code,
203
211
  model: null,
204
212
  modelFile: part.record.painted?.file ?? null,
213
+ ownNozzle: Boolean(part.record.painted?.ownNozzle),
205
214
  modelState: "none",
215
+ nozzleShown: false,
206
216
  faces: [],
207
217
  outlines: [],
208
218
  feature: [],
@@ -223,6 +233,7 @@ export class RocketViewer {
223
233
  this.frameTo(angle ?? this.currentAngle(), false);
224
234
  this.canvas.dataset.ready = "true";
225
235
  this.canvas.dataset.parts = this.parts.map((part) => part.slot).join(" ");
236
+ this.drivenDirty = this.driven !== null;
226
237
  this.wake();
227
238
  }
228
239
  /** Moves the parts from the packed nodes into `into`, keeping each mesh's own placement. */
@@ -394,7 +405,7 @@ export class RocketViewer {
394
405
  return;
395
406
  if (part.model) {
396
407
  part.model.visible = painted;
397
- part.code.visible = !painted;
408
+ this.showCode(part, painted);
398
409
  return;
399
410
  }
400
411
  part.code.visible = true;
@@ -430,11 +441,26 @@ export class RocketViewer {
430
441
  this.reportPainted();
431
442
  });
432
443
  }
444
+ /** Behind a licensed model this package's code shows nothing, except the nozzle the model lacks, and not while the stage is nested in the one below. */
445
+ showCode(part, painted) {
446
+ const nozzleOnly = painted && part.ownNozzle;
447
+ part.code.visible = !painted || nozzleOnly;
448
+ const nozzle = nozzleOnly && this.show !== "together";
449
+ for (const { mesh, name } of part.faces)
450
+ mesh.visible = !nozzleOnly || (nozzle && NOZZLE_MATERIALS.has(name));
451
+ for (const lines of [...part.feature, ...part.weld])
452
+ lines.visible = !nozzleOnly;
453
+ part.nozzleShown = nozzle;
454
+ this.canvas.dataset.nozzle = this.parts.some((one) => one.nozzleShown) ? "own" : "";
455
+ }
433
456
  /** Together, apart, or one part alone: an animated move, or a cut under reduced motion. */
434
457
  setShow(show) {
435
458
  if (!this.built || show === this.show)
436
459
  return;
437
460
  this.show = show;
461
+ for (const part of this.parts)
462
+ if (part.model)
463
+ this.showCode(part, this.look === "painted");
438
464
  const places = this.built.placesOf(show);
439
465
  const fromAngle = this.currentAngle();
440
466
  const framing = this.framingFor(fromAngle);
@@ -541,6 +567,11 @@ export class RocketViewer {
541
567
  setAngle(angle, animate) {
542
568
  if (!this.built)
543
569
  return;
570
+ if (this.driven) {
571
+ this.homeAngle = angle;
572
+ this.syncDriven();
573
+ return;
574
+ }
544
575
  this.controls.autoRotate = false;
545
576
  const framing = this.framingFor(angle);
546
577
  if (animate && !this.options.reducedMotion)
@@ -572,10 +603,62 @@ export class RocketViewer {
572
603
  }
573
604
  setSpin(spin) {
574
605
  this.controls.autoRotateSpeed = autoRotateOf(spin);
575
- this.controls.autoRotate = !this.options.reducedMotion && spin.seconds > 0;
606
+ this.controls.autoRotate = !this.driven && !this.options.reducedMotion && spin.seconds > 0;
576
607
  this.canvas.dataset.spin = `${spin.seconds}${spin.way}`;
577
608
  this.wake();
578
609
  }
610
+ /** Hands the turn, tip, zoom and pan to the page, as the family's view controls report them: this view's own hands (drag, wheel, keys, spin) stand down, and the camera is the chosen view's angle turned and tipped by them. `null` takes them back. */
611
+ drive(choice) {
612
+ this.driven = choice;
613
+ this.controls.enabled = choice === null;
614
+ if (choice)
615
+ this.controls.autoRotate = false;
616
+ this.canvas.style.touchAction = choice ? "auto" : "pan-y";
617
+ this.syncDriven();
618
+ }
619
+ /** The camera is put where the choice says at once, seen or not, so a page that asks where it looks from is told the truth; a move between parts moves it by itself. */
620
+ syncDriven() {
621
+ this.drivenDirty = false;
622
+ this.lens();
623
+ if (!this.pose)
624
+ this.applyDriven();
625
+ this.needsDraw = true;
626
+ this.wake();
627
+ }
628
+ drivenAngle() {
629
+ if (!this.driven)
630
+ return null;
631
+ const turned = turnedBy(this.homeAngle, this.driven.yaw, this.driven.tilt);
632
+ const lean = this.tilt.applied;
633
+ return { az: turned.az + MathUtils.radToDeg(lean.azimuth), el: MathUtils.clamp(turned.el - MathUtils.radToDeg(lean.polar), -87, 87) };
634
+ }
635
+ /** The camera stands where the view's angle, turned and tipped, puts it, as far off as the whole rocket needs from the view's own angle, so turning never makes it breathe. */
636
+ applyDriven() {
637
+ const angle = this.drivenAngle();
638
+ if (!angle || !this.built)
639
+ return;
640
+ const framing = this.framingFor(this.homeAngle);
641
+ const direction = directionOf(angle);
642
+ this.camera.position.set(framing.target[0] + direction[0] * framing.distance, framing.target[1] + direction[1] * framing.distance, framing.target[2] + direction[2] * framing.distance);
643
+ this.controls.target.set(...framing.target);
644
+ if (Math.abs(framing.distance - this.lastDistance) > 1e-6) {
645
+ this.lastDistance = framing.distance;
646
+ this.limits(framing.distance);
647
+ }
648
+ this.controls.update();
649
+ }
650
+ /** Zoom is the camera's, and the pan moves the picture by pixels, so the view stays sharp at any zoom. */
651
+ lens() {
652
+ const { zoom = 1, panX = 0, panY = 0 } = this.driven ?? {};
653
+ const w = Math.max(1, this.holder.clientWidth);
654
+ const h = Math.max(1, this.holder.clientHeight);
655
+ this.camera.zoom = zoom;
656
+ if (panX !== 0 || panY !== 0)
657
+ this.camera.setViewOffset(w, h, -panX, -panY, w, h);
658
+ else
659
+ this.camera.clearViewOffset();
660
+ this.camera.updateProjectionMatrix();
661
+ }
579
662
  setTilt(azimuthDeg, polarDeg) {
580
663
  this.tilt.azimuth = MathUtils.degToRad(MathUtils.clamp(azimuthDeg, -25, 25));
581
664
  this.tilt.polar = MathUtils.degToRad(MathUtils.clamp(polarDeg, -12, 12));
@@ -654,11 +737,21 @@ export class RocketViewer {
654
737
  const dA = (t.azimuth - t.applied.azimuth) * 0.12;
655
738
  const dP = (t.polar - t.applied.polar) * 0.12;
656
739
  if (Math.abs(dA) > 1e-5 || Math.abs(dP) > 1e-5) {
657
- this.orbit(dA, dP);
740
+ if (this.driven)
741
+ this.drivenDirty = true;
742
+ else
743
+ this.orbit(dA, dP);
658
744
  t.applied.azimuth += dA;
659
745
  t.applied.polar += dP;
660
746
  this.needsDraw = true;
661
747
  }
748
+ if (this.drivenDirty) {
749
+ this.drivenDirty = false;
750
+ this.lens();
751
+ if (!this.pose)
752
+ this.applyDriven();
753
+ this.needsDraw = true;
754
+ }
662
755
  if (this.pose) {
663
756
  const { from, to, start, ms, moveParts } = this.pose;
664
757
  const k = Math.min(1, (performance.now() - start) / ms);
@@ -673,6 +766,9 @@ export class RocketViewer {
673
766
  while (dTheta < -Math.PI)
674
767
  dTheta += 2 * Math.PI;
675
768
  const s = new Spherical(MathUtils.lerp(a.radius, b.radius, e), MathUtils.lerp(a.phi, b.phi, e), a.theta + dTheta * e);
769
+ const aim = this.drivenAngle();
770
+ if (aim)
771
+ s.setFromVector3(new Vector3(...directionOf(aim)).setLength(s.radius));
676
772
  this.camera.position.copy(this.controls.target).add(new Vector3().setFromSpherical(s));
677
773
  if (moveParts)
678
774
  for (const part of this.parts) {
@@ -686,6 +782,7 @@ export class RocketViewer {
686
782
  this.pose = null;
687
783
  if (done)
688
784
  this.finishParts();
785
+ this.drivenDirty = this.driven !== null;
689
786
  }
690
787
  }
691
788
  if (!ambient)
@@ -701,6 +798,10 @@ export class RocketViewer {
701
798
  this.canvas.dataset.azimuth = angle.az.toFixed(2);
702
799
  this.canvas.dataset.elevation = angle.el.toFixed(2);
703
800
  this.canvas.dataset.moving = this.pose ? "true" : "false";
801
+ if (this.driven) {
802
+ this.canvas.dataset.zoom = this.driven.zoom.toFixed(2);
803
+ this.canvas.dataset.pan = `${Math.round(this.driven.panX)},${Math.round(this.driven.panY)}`;
804
+ }
704
805
  if (this.glow)
705
806
  this.glow.visible = this.look !== "painted" && this.camera.position.y > 0;
706
807
  }
@@ -716,7 +817,7 @@ export class RocketViewer {
716
817
  const h = Math.max(1, this.holder.clientHeight);
717
818
  this.renderer.setSize(w, h, false);
718
819
  this.camera.aspect = w / h;
719
- this.camera.updateProjectionMatrix();
820
+ this.lens();
720
821
  this.syncSizes();
721
822
  if (this.built && !this.pose)
722
823
  this.applyFraming(this.framingFor(this.currentAngle()));
@@ -730,6 +831,8 @@ export class RocketViewer {
730
831
  this.camera.position.copy(this.controls.target).add(new Vector3().setFromSpherical(s));
731
832
  }
732
833
  key(event) {
834
+ if (this.driven)
835
+ return;
733
836
  const step = MathUtils.degToRad(15);
734
837
  const moves = {
735
838
  ArrowLeft: () => this.orbit(-step, 0),
package/dist/element.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Description, FitReport } from "./fit.ts";
2
- import { type Angle } from "./framing.ts";
2
+ import { type Angle, type ViewChoice } from "./framing.ts";
3
3
  import { ROCKETS } from "./rockets.ts";
4
4
  export type RocketState = "picture" | "loading" | "3d" | "fallback" | "error";
5
5
  declare const HTMLBase: typeof HTMLElement;
@@ -23,6 +23,8 @@ export declare class UchusenRocket extends HTMLBase {
23
23
  private shownAt;
24
24
  private starting;
25
25
  private key;
26
+ private driven;
27
+ private placeFrame;
26
28
  /** What is showing now. */
27
29
  get state(): RocketState;
28
30
  /** What the fit check said about what is showing: its label, parts and credits. */
@@ -32,6 +34,10 @@ export declare class UchusenRocket extends HTMLBase {
32
34
  set description(value: Description | null);
33
35
  /** Where the camera is looking from, while there is a 3D view. */
34
36
  get angle(): Angle | null;
37
+ /** Lets a page choose the turn, tip, zoom and pan, as the family's `<kazoku-view-controls>` reports them in `kazoku-viewchange` (`element.drive(event.detail)`): the element's own drag, wheel, keys and spin stand down, and the 3D view and the drawing both follow. `null` takes them back. */
38
+ drive(choice: Partial<ViewChoice> | null): void;
39
+ /** The drawing, turned and moved to the chosen view. The 3D view moves itself, so only the drawing needs it, and only until the 3D view shows. */
40
+ private place;
35
41
  connectedCallback(): void;
36
42
  disconnectedCallback(): void;
37
43
  attributeChangedCallback(name: string, before: string | null, after: string | null): void;
package/dist/element.js CHANGED
@@ -4,7 +4,7 @@
4
4
  import { build, FitError } from "./build/index.js";
5
5
  import { lineupSvg } from "./build/lineup.js";
6
6
  import { CROSSFADE_MS, REDUCED_FADE_MS, revealDelay } from "./crossfade.js";
7
- import { cleanView, VIEWS } from "./framing.js";
7
+ import { cleanView, turnedBy, VIEWS } from "./framing.js";
8
8
  import { cleanShow } from "./layout.js";
9
9
  import { cleanLook } from "./looks.js";
10
10
  import { ROCKETS } from "./rockets.js";
@@ -27,6 +27,7 @@ const CSS = `
27
27
  .drawing, .drawing svg { display: block; width: 100%; height: 100%; }
28
28
  .drawing, .canvas { transition: opacity var(--uchusen-fade, 1500ms) ease-in-out, filter var(--uchusen-fade, 1500ms) ease-in-out; }
29
29
  .stage { position: absolute; inset: 0; }
30
+ :host([data-driven]) .picture { overflow: hidden; }
30
31
  .canvas { display: block; width: 100%; height: 100%; opacity: 0; filter: blur(3px); cursor: grab; border-radius: inherit; }
31
32
  .canvas:active { cursor: grabbing; }
32
33
  .canvas:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; }
@@ -97,6 +98,8 @@ export class UchusenRocket extends HTMLBase {
97
98
  shownAt = 0;
98
99
  starting = false;
99
100
  key = "";
101
+ driven = null;
102
+ placeFrame = 0;
100
103
  /** What is showing now. */
101
104
  get state() {
102
105
  return this.stateNow;
@@ -117,6 +120,33 @@ export class UchusenRocket extends HTMLBase {
117
120
  get angle() {
118
121
  return this.viewer?.currentAngle() ?? null;
119
122
  }
123
+ /** Lets a page choose the turn, tip, zoom and pan, as the family's `<kazoku-view-controls>` reports them in `kazoku-viewchange` (`element.drive(event.detail)`): the element's own drag, wheel, keys and spin stand down, and the 3D view and the drawing both follow. `null` takes them back. */
124
+ drive(choice) {
125
+ if (choice === null) {
126
+ this.driven = null;
127
+ delete this.dataset.driven;
128
+ }
129
+ else {
130
+ const number = (value, fallback, lo, hi) => (typeof value === "number" && Number.isFinite(value) ? Math.min(hi, Math.max(lo, value)) : fallback);
131
+ this.driven = {
132
+ yaw: number(choice.yaw, this.driven?.yaw ?? 0, -3600, 3600),
133
+ tilt: number(choice.tilt, this.driven?.tilt ?? 0, -90, 90),
134
+ zoom: number(choice.zoom, this.driven?.zoom ?? 1, 0.25, 8),
135
+ panX: number(choice.panX, this.driven?.panX ?? 0, -5000, 5000),
136
+ panY: number(choice.panY, this.driven?.panY ?? 0, -5000, 5000),
137
+ };
138
+ this.dataset.driven = "true";
139
+ }
140
+ this.viewer?.drive(this.driven);
141
+ this.place();
142
+ }
143
+ /** The drawing, turned and moved to the chosen view. The 3D view moves itself, so only the drawing needs it, and only until the 3D view shows. */
144
+ place() {
145
+ if (this.stateNow === "3d" && this.viewer)
146
+ return;
147
+ window.cancelAnimationFrame(this.placeFrame);
148
+ this.placeFrame = window.requestAnimationFrame(() => this.redraw());
149
+ }
120
150
  connectedCallback() {
121
151
  if (!this.shadowRoot)
122
152
  adopt(this.attachShadow({ mode: "open" }), CSS, rocketSheet);
@@ -173,6 +203,7 @@ export class UchusenRocket extends HTMLBase {
173
203
  this.watch = null;
174
204
  window.clearTimeout(this.releaseTimer);
175
205
  window.clearTimeout(this.revealTimer);
206
+ window.cancelAnimationFrame(this.placeFrame);
176
207
  for (const clean of this.cleanups.splice(0))
177
208
  clean();
178
209
  this.viewer?.dispose();
@@ -249,7 +280,7 @@ export class UchusenRocket extends HTMLBase {
249
280
  return;
250
281
  const { look, show, angle } = this.settings(this.builtNow);
251
282
  // when a 3D view is showing, a drawing of the same angle is ready behind it
252
- const from = this.viewer?.currentAngle() ?? angle;
283
+ const from = this.driven ? turnedBy(angle, this.driven.yaw, this.driven.tilt) : (this.viewer?.currentAngle() ?? angle);
253
284
  const svg = this.builtNow.svg({ look, show, view: from });
254
285
  const template = document.createElement("template");
255
286
  template.innerHTML = svg;
@@ -258,6 +289,8 @@ export class UchusenRocket extends HTMLBase {
258
289
  return;
259
290
  node.setAttribute("aria-label", this.getAttribute("label") ?? this.builtNow.report.label);
260
291
  this.drawing.replaceChildren(node);
292
+ const { zoom = 1, panX = 0, panY = 0 } = this.driven ?? {};
293
+ this.drawing.style.transform = zoom !== 1 || panX !== 0 || panY !== 0 ? `translate(${panX}px, ${panY}px) scale(${zoom})` : "";
261
294
  }
262
295
  mark() {
263
296
  if (!this.builtNow)
@@ -278,7 +311,7 @@ export class UchusenRocket extends HTMLBase {
278
311
  const { look, show, viewName, angle } = this.settings(this.builtNow);
279
312
  this.viewer.setLabel(this.getAttribute("label") ?? this.builtNow.report.label);
280
313
  // a new rocket keeps the angle, unless the same change named another view
281
- const from = viewName !== this.lastView ? angle : this.viewer.currentAngle();
314
+ const from = viewName !== this.lastView || this.driven ? angle : this.viewer.currentAngle();
282
315
  this.lastView = viewName;
283
316
  await this.viewer.load(this.builtNow, show, look, from);
284
317
  this.emit("uchusen-change", { report: this.builtNow.report });
@@ -325,8 +358,11 @@ export class UchusenRocket extends HTMLBase {
325
358
  this.releaseTimer = window.setTimeout(() => {
326
359
  this.viewer?.dispose();
327
360
  this.viewer = null;
328
- if (generation === this.generation)
361
+ if (generation === this.generation) {
329
362
  this.setState("picture");
363
+ if (this.driven)
364
+ this.place();
365
+ }
330
366
  }, RELEASE_MS);
331
367
  }
332
368
  }, { rootMargin: "200px 0px" });
@@ -363,6 +399,8 @@ export class UchusenRocket extends HTMLBase {
363
399
  },
364
400
  });
365
401
  this.viewer = made;
402
+ if (this.driven)
403
+ made.drive(this.driven);
366
404
  made.setVisible(true);
367
405
  const { look, show, viewName, angle } = this.settings(this.builtNow ?? built);
368
406
  this.lastView = viewName;
package/dist/framing.d.ts CHANGED
@@ -15,6 +15,16 @@ export interface Angle {
15
15
  export declare const VIEWS: Readonly<Record<ViewName, Angle>>;
16
16
  export declare const cleanView: (value: unknown) => ViewName;
17
17
  export declare const deg: (d: number) => number;
18
+ /** What the family's view controls report: the turn and tip in degrees, the zoom (1 is the whole rocket in the picture) and the pan in pixels. */
19
+ export interface ViewChoice {
20
+ yaw: number;
21
+ tilt: number;
22
+ zoom: number;
23
+ panX: number;
24
+ panY: number;
25
+ }
26
+ /** What the family's view controls choose: the turn and tip, in degrees, from a base angle. Dragging right makes `yaw` larger, which carries the camera the other way round; `tilt` larger tips the top away, which lowers the camera. */
27
+ export declare function turnedBy(base: Angle, yaw: number, tilt: number): Angle;
18
28
  /** The unit vector from what is looked at towards the camera. */
19
29
  export declare function directionOf({ az, el }: Angle): Vec3;
20
30
  export interface Framing {
package/dist/framing.js CHANGED
@@ -15,6 +15,10 @@ export const VIEWS = {
15
15
  };
16
16
  export const cleanView = (value) => (VIEW_NAMES.includes(value) ? value : DEFAULT_VIEW);
17
17
  export const deg = (d) => (d * Math.PI) / 180;
18
+ /** What the family's view controls choose: the turn and tip, in degrees, from a base angle. Dragging right makes `yaw` larger, which carries the camera the other way round; `tilt` larger tips the top away, which lowers the camera. */
19
+ export function turnedBy(base, yaw, tilt) {
20
+ return { az: base.az - yaw, el: Math.min(87, Math.max(-87, base.el - tilt)) };
21
+ }
18
22
  /** The unit vector from what is looked at towards the camera. */
19
23
  export function directionOf({ az, el }) {
20
24
  const c = Math.cos(deg(el));
package/dist/index.d.ts CHANGED
@@ -10,8 +10,8 @@ export type { Joint, PartKind, PartRecord } from "./parts.ts";
10
10
  export { PART_ENTRIES, partEntry } from "./model/registry.ts";
11
11
  export { check } from "./fit.ts";
12
12
  export type { Description, FitCode, FitIssue, FitReport, SlotPart } from "./fit.ts";
13
- export { axesOf, cleanView, DEFAULT_VIEW, directionOf, FOV_DEG, frameFor, PICTURE_H, PICTURE_W, VIEW_NAMES, VIEWS } from "./framing.ts";
14
- export type { Angle, Framing, Vec3, ViewName } from "./framing.ts";
13
+ export { axesOf, cleanView, DEFAULT_VIEW, directionOf, FOV_DEG, frameFor, PICTURE_H, PICTURE_W, turnedBy, VIEW_NAMES, VIEWS } from "./framing.ts";
14
+ export type { Angle, Framing, Vec3, ViewChoice, ViewName } from "./framing.ts";
15
15
  export { cleanLook, DEFAULT_LOOK, LOOK_NAMES, LOOKS } from "./looks.ts";
16
16
  export type { Look, LookName } from "./looks.ts";
17
17
  export { cleanShow, placesFor, showsOf } from "./layout.ts";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { SOURCES } from "./data/sources.js";
5
5
  export { creditLine, MODEL_CREDITS } from "./data/credits.js";
6
6
  export { PART_ENTRIES, partEntry } from "./model/registry.js";
7
7
  export { check } from "./fit.js";
8
- export { axesOf, cleanView, DEFAULT_VIEW, directionOf, FOV_DEG, frameFor, PICTURE_H, PICTURE_W, VIEW_NAMES, VIEWS } from "./framing.js";
8
+ export { axesOf, cleanView, DEFAULT_VIEW, directionOf, FOV_DEG, frameFor, PICTURE_H, PICTURE_W, turnedBy, VIEW_NAMES, VIEWS } from "./framing.js";
9
9
  export { cleanLook, DEFAULT_LOOK, LOOK_NAMES, LOOKS } from "./looks.js";
10
10
  export { cleanShow, placesFor, showsOf } from "./layout.js";
11
11
  export { CROSSFADE_MS, DRAWING_HOLD_MS, REDUCED_FADE_MS, revealDelay } from "./crossfade.js";
@@ -18,6 +18,7 @@ export interface FalconDraw {
18
18
  joint: number;
19
19
  radius: number;
20
20
  nozzle: number;
21
+ exit: number;
21
22
  };
22
23
  fairing: {
23
24
  height: number;
@@ -1,5 +1,6 @@
1
1
  // Falcon 9, Falcon Heavy, Dragon and Grasshopper from tables of numbers. Heights and diameters are the sourced figures where there are any. How a part is drawn is not a published figure: Block 5, Dragon 2, Dragon 1, Falcon Heavy
2
2
  // and Grasshopper take their proportions from their licensed models at the sourced sizes (see NOTICE.md), and the older Falcons from the sourced totals and the documented differences.
3
+ import { MVAC_EXIT_DIAMETER } from "../data/falcon.js";
3
4
  import { deg } from "../framing.js";
4
5
  import { pointOf, radial, strut } from "./pieces.js";
5
6
  import { gridFin } from "./starship.js";
@@ -16,7 +17,8 @@ const R = 1.85;
16
17
  /** Each part's height is the sourced figure where there is one; the others share out what the sourced whole leaves, so the parts add up to it. */
17
18
  const falconOf = (first, interstage, secondHeight, joint, fairing, fairingRadius, engines, legs, fins) => ({
18
19
  first: { height: first, interstage, radius: R, engines, legs, fins },
19
- second: { height: secondHeight, joint, radius: R, nozzle: joint * 0.9 },
20
+ // the v1.0's Merlin 1C Vacuum has no sourced width, so its nozzle keeps its drawn proportion
21
+ second: { height: secondHeight, joint, radius: R, nozzle: joint * 0.9, exit: engines === "grid" ? R * 0.62 : (MVAC_EXIT_DIAMETER.value ?? 0) / 2 },
20
22
  fairing: { height: fairing, radius: fairingRadius },
21
23
  });
22
24
  export const FALCON_DRAWS = {
@@ -62,11 +64,11 @@ function firstStage(d, withTop) {
62
64
  return pieces;
63
65
  }
64
66
  function secondStage(d) {
65
- const { height, joint, radius: r, nozzle } = d;
67
+ const { height, joint, radius: r, nozzle, exit } = d;
66
68
  const pieces = [];
67
69
  pieces.push({ kind: "lathe", material: "white", segs: 48, body: true, outline: true, profile: [[r * 0.6, joint - 0.5], [r, joint], [r, height]], rings: [joint + 0.5, height - 0.05], welds: [(joint + height) / 2] });
68
70
  pieces.push({ kind: "lathe", material: "soot", segs: 36, under: true, only: "mesh", profile: [[0, joint - 0.5], [r * 0.6, joint - 0.5]] });
69
- pieces.push(...merlin([0, 0], 0, r * 0.62, nozzle * 1.06));
71
+ pieces.push(...merlin([0, 0], 0, exit, nozzle * 1.06));
70
72
  return pieces;
71
73
  }
72
74
  function fairing(d, jointRadius) {
@@ -14,7 +14,7 @@ const PAINTED = {
14
14
  "starship-v1-flight1-ship": { file: "starship-v1-flight1-ship.glb", credit: "clarence365-s24b7" },
15
15
  "starship-2019-ship": { file: "starship-2019-ship.glb", credit: "martiandays-2019" },
16
16
  "falcon-9-block-5-first-stage": { file: "falcon-9-block-5-first-stage.glb", credit: "stanley-falcon-9" },
17
- "falcon-9-block-5-second-stage": { file: "falcon-9-block-5-second-stage.glb", credit: "stanley-falcon-9" },
17
+ "falcon-9-block-5-second-stage": { file: "falcon-9-block-5-second-stage.glb", credit: "stanley-falcon-9", ownNozzle: true },
18
18
  "falcon-9-block-5-fairing": { file: "falcon-9-block-5-fairing.glb", credit: "stanley-falcon-9" },
19
19
  "dragon-dragon-2-capsule": { file: "dragon-dragon-2-capsule.glb", credit: "kubaha-dragon-2" },
20
20
  "dragon-dragon-2-trunk": { file: "dragon-dragon-2-trunk.glb", credit: "kubaha-dragon-2" },
package/dist/parts.d.ts CHANGED
@@ -34,10 +34,11 @@ export interface PartRecord {
34
34
  x: number;
35
35
  z: number;
36
36
  };
37
- /** The painted look's mesh, when a licensed model gives one: its file under assets/models/ and whose it is. Otherwise this package's own code paints it. */
37
+ /** The painted look's mesh, when a licensed model gives one: its file under assets/models/ and whose it is. Otherwise this package's own code paints it. `ownNozzle`: the model has none, so this package's own shows with it unless the part is nested in the stage below. */
38
38
  painted: {
39
39
  file: string;
40
40
  credit: CreditId;
41
+ ownNozzle?: true;
41
42
  } | null;
42
43
  /** What a reader should be told about where its shape came from. */
43
44
  drawnFrom: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/uchusen-rockets",
3
- "version": "0.1.0",
3
+ "version": "0.2.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": ["rocket", "rockets", "spaceship", "starship", "super-heavy", "falcon-9", "dragon", "spaceflight", "line-drawing", "svg", "3d", "3d-models", "glb", "gltf", "three", "threejs", "webgl", "web-component", "custom-element", "react", "typescript", "data"],
6
6
  "homepage": "https://johnmorrisdotca.github.io/uchusen-rockets/",
@@ -47,6 +47,7 @@
47
47
  "@gltf-transform/core": "4.5.1",
48
48
  "@gltf-transform/extensions": "4.5.1",
49
49
  "@gltf-transform/functions": "4.5.1",
50
+ "@johnmorrisdotca/kazoku-ui": "0.2.1",
50
51
  "@playwright/test": "1.64.0",
51
52
  "@types/node": "24.10.1",
52
53
  "@types/react": "19.2.7",