@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.
- package/CHANGELOG.md +11 -0
- package/NOTICE.md +2 -2
- package/README.md +1 -1
- package/dist/assets/falcon-9-block-5-apart.painted.svg +1 -1
- package/dist/assets/falcon-9-block-5-apart.svg +1 -1
- package/dist/assets/falcon-9-block-5-apart.thick.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2-apart.painted.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2-apart.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2-apart.thick.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.glb +0 -0
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.painted.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2-second-stage.thick.svg +1 -1
- package/dist/assets/falcon-9-block-5-dragon-2.glb +0 -0
- package/dist/assets/falcon-9-block-5-second-stage.glb +0 -0
- package/dist/assets/falcon-9-block-5-second-stage.painted.svg +1 -1
- package/dist/assets/falcon-9-block-5-second-stage.svg +1 -1
- package/dist/assets/falcon-9-block-5-second-stage.thick.svg +1 -1
- package/dist/assets/falcon-9-block-5.glb +0 -0
- package/dist/assets/falcon-9-ft-apart.painted.svg +1 -1
- package/dist/assets/falcon-9-ft-apart.svg +1 -1
- package/dist/assets/falcon-9-ft-apart.thick.svg +1 -1
- package/dist/assets/falcon-9-ft-second-stage.glb +0 -0
- package/dist/assets/falcon-9-ft-second-stage.painted.svg +1 -1
- package/dist/assets/falcon-9-ft-second-stage.svg +1 -1
- package/dist/assets/falcon-9-ft-second-stage.thick.svg +1 -1
- package/dist/assets/falcon-9-ft.glb +0 -0
- package/dist/assets/falcon-9-v1.1-apart.painted.svg +1 -1
- package/dist/assets/falcon-9-v1.1-apart.svg +1 -1
- package/dist/assets/falcon-9-v1.1-apart.thick.svg +1 -1
- package/dist/assets/falcon-9-v1.1-second-stage.glb +0 -0
- package/dist/assets/falcon-9-v1.1-second-stage.painted.svg +1 -1
- package/dist/assets/falcon-9-v1.1-second-stage.svg +1 -1
- package/dist/assets/falcon-9-v1.1-second-stage.thick.svg +1 -1
- package/dist/assets/falcon-9-v1.1.glb +0 -0
- package/dist/data/falcon.d.ts +3 -1
- package/dist/data/falcon.js +2 -0
- package/dist/data/sources.d.ts +8 -0
- package/dist/data/sources.js +1 -0
- package/dist/element/scene.d.ts +15 -1
- package/dist/element/scene.js +109 -6
- package/dist/element.d.ts +7 -1
- package/dist/element.js +42 -4
- package/dist/framing.d.ts +10 -0
- package/dist/framing.js +4 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/model/falcon.d.ts +1 -0
- package/dist/model/falcon.js +5 -3
- package/dist/model/registry.js +1 -1
- package/dist/parts.d.ts +2 -1
- package/package.json +2 -1
package/dist/element/scene.js
CHANGED
|
@@ -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", () =>
|
|
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
|
|
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.
|
|
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.
|
|
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";
|
package/dist/model/falcon.d.ts
CHANGED
package/dist/model/falcon.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
71
|
+
pieces.push(...merlin([0, 0], 0, exit, nozzle * 1.06));
|
|
70
72
|
return pieces;
|
|
71
73
|
}
|
|
72
74
|
function fairing(d, jointRadius) {
|
package/dist/model/registry.js
CHANGED
|
@@ -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.
|
|
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",
|