@johnmorrisdotca/uchusen-rockets 0.3.0 → 0.5.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 (120) hide show
  1. package/CHANGELOG.md +31 -5
  2. package/NOTICE.md +2 -0
  3. package/README.md +316 -4
  4. package/dist/assets/droneship-asog-landed.glb +0 -0
  5. package/dist/assets/droneship-asog.glb +0 -0
  6. package/dist/assets/droneship-jrti-landed.glb +0 -0
  7. package/dist/assets/droneship-jrti.glb +0 -0
  8. package/dist/assets/droneship-ocisly-landed.glb +0 -0
  9. package/dist/assets/droneship-ocisly.glb +0 -0
  10. package/dist/assets/falcon-9-block-5-dragon-2-first-stage.glb +0 -0
  11. package/dist/assets/falcon-9-block-5-dragon-2.glb +0 -0
  12. package/dist/assets/falcon-9-block-5-first-stage.glb +0 -0
  13. package/dist/assets/falcon-9-block-5.glb +0 -0
  14. package/dist/assets/falcon-9-ft-first-stage.glb +0 -0
  15. package/dist/assets/falcon-9-ft.glb +0 -0
  16. package/dist/assets/falcon-9-v1.1-first-stage.glb +0 -0
  17. package/dist/assets/falcon-9-v1.1.glb +0 -0
  18. package/dist/assets/falcon-heavy-falcon-heavy-centre.glb +0 -0
  19. package/dist/assets/falcon-heavy-falcon-heavy-side-left.glb +0 -0
  20. package/dist/assets/falcon-heavy-falcon-heavy-side-right.glb +0 -0
  21. package/dist/assets/falcon-heavy-falcon-heavy.glb +0 -0
  22. package/dist/assets/landing-zone-lz-1-landed.glb +0 -0
  23. package/dist/assets/landing-zone-lz-1.glb +0 -0
  24. package/dist/assets/landing-zone-lz-4-landed.glb +0 -0
  25. package/dist/assets/landing-zone-lz-4.glb +0 -0
  26. package/dist/assets/launch-tower-catch-booster.glb +0 -0
  27. package/dist/assets/launch-tower-catch.glb +0 -0
  28. package/dist/assets/launch-tower-stack-booster.glb +0 -0
  29. package/dist/assets/launch-tower-stack-ship.glb +0 -0
  30. package/dist/assets/launch-tower-stack.glb +0 -0
  31. package/dist/assets/starship-2019.glb +0 -0
  32. package/dist/assets/starship-v1-booster.glb +0 -0
  33. package/dist/assets/starship-v1-flight1-booster.glb +0 -0
  34. package/dist/assets/starship-v1-flight1-ship.glb +0 -0
  35. package/dist/assets/starship-v1-flight1.glb +0 -0
  36. package/dist/assets/starship-v1-ship.glb +0 -0
  37. package/dist/assets/starship-v1.glb +0 -0
  38. package/dist/assets/starship-v2-booster.glb +0 -0
  39. package/dist/assets/starship-v2-ship.glb +0 -0
  40. package/dist/assets/starship-v2.glb +0 -0
  41. package/dist/assets/starship-v3-booster.glb +0 -0
  42. package/dist/assets/starship-v3-ship.glb +0 -0
  43. package/dist/assets/starship-v3.glb +0 -0
  44. package/dist/assets/starship-v4-booster.glb +0 -0
  45. package/dist/assets/starship-v4-ship.glb +0 -0
  46. package/dist/assets/starship-v4.glb +0 -0
  47. package/dist/attrs.d.ts +70 -4
  48. package/dist/attrs.js +59 -4
  49. package/dist/build/glb.d.ts +25 -0
  50. package/dist/build/glb.js +29 -7
  51. package/dist/build/index.d.ts +94 -4
  52. package/dist/build/index.js +71 -7
  53. package/dist/build/lineup.d.ts +56 -2
  54. package/dist/build/lineup.js +23 -2
  55. package/dist/builder.d.ts +47 -1
  56. package/dist/builder.js +14 -1
  57. package/dist/crossfade.d.ts +44 -2
  58. package/dist/crossfade.js +44 -2
  59. package/dist/data/credits.d.ts +44 -1
  60. package/dist/data/credits.js +22 -1
  61. package/dist/data/flight-log.d.ts +33 -1
  62. package/dist/data/flight-log.js +22 -1
  63. package/dist/data/flights.d.ts +61 -2
  64. package/dist/data/flights.js +160 -3
  65. package/dist/data/makers.d.ts +21 -0
  66. package/dist/data/makers.js +10 -0
  67. package/dist/data/propellants.d.ts +59 -0
  68. package/dist/data/propellants.js +43 -0
  69. package/dist/data/sources.d.ts +102 -0
  70. package/dist/data/sources.js +19 -0
  71. package/dist/data/types.d.ts +81 -1
  72. package/dist/data/types.js +26 -1
  73. package/dist/draw/plume.d.ts +10 -1
  74. package/dist/draw/plume.js +15 -5
  75. package/dist/element/plumes.d.ts +7 -8
  76. package/dist/element/plumes.js +36 -21
  77. package/dist/element/scene.js +30 -5
  78. package/dist/element.d.ts +61 -3
  79. package/dist/element.js +53 -5
  80. package/dist/embed.d.ts +110 -5
  81. package/dist/embed.js +88 -5
  82. package/dist/engines.d.ts +86 -4
  83. package/dist/engines.js +75 -4
  84. package/dist/fit.d.ts +68 -0
  85. package/dist/fit.js +13 -0
  86. package/dist/flight.d.ts +136 -9
  87. package/dist/flight.js +130 -10
  88. package/dist/flightbar.d.ts +12 -0
  89. package/dist/flightbar.js +13 -1
  90. package/dist/framing.d.ts +182 -8
  91. package/dist/framing.js +127 -6
  92. package/dist/index.d.ts +3 -0
  93. package/dist/index.js +2 -0
  94. package/dist/layout.d.ts +65 -1
  95. package/dist/layout.js +43 -1
  96. package/dist/looks.d.ts +64 -1
  97. package/dist/looks.js +42 -1
  98. package/dist/model/falcon.js +2 -2
  99. package/dist/model/materials.d.ts +32 -0
  100. package/dist/model/materials.js +10 -0
  101. package/dist/model/moves.d.ts +36 -0
  102. package/dist/model/moves.js +70 -0
  103. package/dist/model/pieces.d.ts +31 -0
  104. package/dist/model/pieces.js +51 -9
  105. package/dist/model/registry.d.ts +22 -0
  106. package/dist/model/registry.js +22 -0
  107. package/dist/model/starship.d.ts +6 -3
  108. package/dist/model/starship.js +12 -7
  109. package/dist/parts.d.ts +33 -1
  110. package/dist/pixels.d.ts +44 -4
  111. package/dist/pixels.js +44 -4
  112. package/dist/react.d.ts +26 -2
  113. package/dist/react.js +13 -0
  114. package/dist/rockets.d.ts +91 -3
  115. package/dist/rockets.js +58 -2
  116. package/dist/spin.d.ts +57 -2
  117. package/dist/spin.js +35 -2
  118. package/dist/tilt.d.ts +48 -4
  119. package/dist/tilt.js +26 -4
  120. package/package.json +27 -7
@@ -1,9 +1,31 @@
1
1
  import type { SourceId } from "./sources.ts";
2
+ /**
3
+ * Another value a source gave for a figure, with the source and the reason it was not the one chosen.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { variantOf, type Disagreement } from "@johnmorrisdotca/uchusen-rockets";
8
+ *
9
+ * const others: readonly Disagreement<number>[] = variantOf("starship", "v3")?.facts.parts.booster?.others ?? [];
10
+ * console.log(others.map((other) => other.value));
11
+ * ```
12
+ */
2
13
  export interface Disagreement<T> {
3
14
  value: T;
4
15
  source: SourceId;
5
16
  note: string;
6
17
  }
18
+ /**
19
+ * A number or a text with where it came from. A figure with no source is blank (`value` is null) and is never estimated. Where sources disagree, the figure keeps the one chosen and `others` holds the rest.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { variantOf, type Figure } from "@johnmorrisdotca/uchusen-rockets";
24
+ *
25
+ * const height: Figure | undefined = variantOf("starship", "v3")?.facts.height;
26
+ * console.log(height?.value, height?.source);
27
+ * ```
28
+ */
7
29
  export interface Figure<T = number> {
8
30
  value: T | null;
9
31
  source: SourceId | null;
@@ -14,10 +36,57 @@ export interface Figure<T = number> {
14
36
  brief?: string;
15
37
  others?: readonly Disagreement<T>[];
16
38
  }
39
+ /**
40
+ * A figure with no value: the shape of "no source gives this".
41
+ *
42
+ * @param note Why there is no value. Optional.
43
+ * @param brief What to say in place of a value wherever it is shown. Optional.
44
+ * @returns A figure whose value and source are null.
45
+ * @example
46
+ * ```ts
47
+ * import { blank } from "@johnmorrisdotca/uchusen-rockets";
48
+ *
49
+ * console.log(blank("No source read gives it.", "not given"));
50
+ * ```
51
+ */
17
52
  export declare const blank: <T = number>(note?: string, brief?: string) => Figure<T>;
18
- /** A figure as words for a table: its value and whatever must stay beside it; a blank says why, or that no source gives it. */
53
+ /**
54
+ * A figure as words for a table: its value and whatever must stay beside it; a blank says why, or that no source gives it.
55
+ *
56
+ * @param figure A figure, or undefined.
57
+ * @param unit Text to put after a number, such as " m". Optional.
58
+ * @returns The figure as words for a table. A blank gives its `brief`, or "No source" when it has none.
59
+ * @example
60
+ * ```ts
61
+ * import { figureWords, variantOf } from "@johnmorrisdotca/uchusen-rockets";
62
+ *
63
+ * console.log(figureWords(variantOf("starship", "v3")?.facts.height, " m"));
64
+ * ```
65
+ */
19
66
  export declare function figureWords(figure: Figure<number | string> | undefined, unit?: string): string;
67
+ /**
68
+ * What kind of thing a variant is: flown, announced (not built), an artist's model, a test vehicle or a support item.
69
+ *
70
+ * @example
71
+ * ```ts
72
+ * import { variantOf, type VariantStatus } from "@johnmorrisdotca/uchusen-rockets";
73
+ *
74
+ * const status: VariantStatus | undefined = variantOf("starship", "v4")?.status;
75
+ * console.log(status);
76
+ * ```
77
+ */
20
78
  export type VariantStatus = "flown" | "announced" | "artist-model" | "test-vehicle" | "support";
79
+ /**
80
+ * The sourced numbers of a variant: its height and diameter, each part's height by slot, its engines, other facts and any sizes that are not a height or a width.
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { variantOf, type VariantFacts } from "@johnmorrisdotca/uchusen-rockets";
85
+ *
86
+ * const facts: VariantFacts | undefined = variantOf("falcon-9", "block-5")?.facts;
87
+ * console.log(facts?.height.value, Object.keys(facts?.parts ?? {}));
88
+ * ```
89
+ */
21
90
  export interface VariantFacts {
22
91
  /** Overall height with every part in place, metres. */
23
92
  height: Figure;
@@ -31,6 +100,17 @@ export interface VariantFacts {
31
100
  /** Sizes that are not a height or a width, shown as they are named here: "Deck length". */
32
101
  sizes?: Readonly<Record<string, Figure>>;
33
102
  }
103
+ /**
104
+ * One version of a rocket as data: its id, name, status, first flight, its parts from the bottom up, and the sourced facts about it.
105
+ *
106
+ * @example
107
+ * ```ts
108
+ * import { variantOf, type Variant } from "@johnmorrisdotca/uchusen-rockets";
109
+ *
110
+ * const variant: Variant | undefined = variantOf("starship", "v3");
111
+ * console.log(variant?.name, variant?.slots);
112
+ * ```
113
+ */
34
114
  export interface Variant {
35
115
  /** "v1", "v2" and so on; a label for what is not a numbered version. */
36
116
  id: string;
@@ -1,5 +1,30 @@
1
+ /**
2
+ * A figure with no value: the shape of "no source gives this".
3
+ *
4
+ * @param note Why there is no value. Optional.
5
+ * @param brief What to say in place of a value wherever it is shown. Optional.
6
+ * @returns A figure whose value and source are null.
7
+ * @example
8
+ * ```ts
9
+ * import { blank } from "@johnmorrisdotca/uchusen-rockets";
10
+ *
11
+ * console.log(blank("No source read gives it.", "not given"));
12
+ * ```
13
+ */
1
14
  export const blank = (note, brief) => ({ value: null, source: null, ...(note ? { note } : {}), ...(brief ? { brief } : {}) });
2
- /** A figure as words for a table: its value and whatever must stay beside it; a blank says why, or that no source gives it. */
15
+ /**
16
+ * A figure as words for a table: its value and whatever must stay beside it; a blank says why, or that no source gives it.
17
+ *
18
+ * @param figure A figure, or undefined.
19
+ * @param unit Text to put after a number, such as " m". Optional.
20
+ * @returns The figure as words for a table. A blank gives its `brief`, or "No source" when it has none.
21
+ * @example
22
+ * ```ts
23
+ * import { figureWords, variantOf } from "@johnmorrisdotca/uchusen-rockets";
24
+ *
25
+ * console.log(figureWords(variantOf("starship", "v3")?.facts.height, " m"));
26
+ * ```
27
+ */
3
28
  export function figureWords(figure, unit = "") {
4
29
  if (!figure || figure.value === null)
5
30
  return figure?.brief ?? "No source";
@@ -2,7 +2,16 @@ import type { Camera } from "./svg.ts";
2
2
  import type { Vec3 } from "../framing.ts";
3
3
  import { type Bell } from "../engines.ts";
4
4
  import type { LookName } from "../looks.ts";
5
+ import type { FlameKind } from "../data/propellants.ts";
6
+ /** A flame's colours in a picture, by the kind its engine's propellant gives: the hot core, the middle and the cool edge, and the line looks' stroke. A picture, not a measurement of a flame. */
7
+ export declare const FLAME_COLOURS: Readonly<Record<FlameKind, {
8
+ core: string;
9
+ mid: string;
10
+ edge: string;
11
+ line: string;
12
+ inner: string;
13
+ }>>;
5
14
  /** The flames for these bells, in picture coordinates. Empty when there are none. */
6
- export declare function plumeMarkup(bells: readonly Bell[], at: Vec3, cam: Camera, look: LookName, prefix: string): string;
15
+ export declare function plumeMarkup(bells: readonly Bell[], at: Vec3, cam: Camera, look: LookName, prefix: string, kind?: FlameKind): string;
7
16
  /** The engines of a part seen from below, each in its own place, the burning ones filled: `engine by engine`. Its words are for a screen reader. */
8
17
  export declare function engineChart(bells: readonly Bell[], burning: readonly Bell[], label: string): string;
@@ -1,4 +1,13 @@
1
1
  import { PLUME_DIAMONDS, plumeLength } from "../engines.js";
2
+ /** A flame's colours in a picture, by the kind its engine's propellant gives: the hot core, the middle and the cool edge, and the line looks' stroke. A picture, not a measurement of a flame. */
3
+ export const FLAME_COLOURS = {
4
+ kerosene: { core: "#fff4d2", mid: "#ff9a3a", edge: "#ff4a12", line: "#e7a24a", inner: "#ffd7a1" },
5
+ methane: { core: "#fff1e6", mid: "#9aa5ff", edge: "#5d6dff", line: "#8f9bf0", inner: "#d9ddff" },
6
+ hydrogen: { core: "#ffffff", mid: "#b8dcff", edge: "#6fa8ff", line: "#9cc7f5", inner: "#e4f1ff" },
7
+ solid: { core: "#fff1c4", mid: "#ffb02e", edge: "#ff3d0a", line: "#f0a030", inner: "#ffe0a0" },
8
+ hypergolic: { core: "#ffffff", mid: "#e6ecff", edge: "#b8c4ee", line: "#c3cbe3", inner: "#f2f5ff" },
9
+ neutral: { core: "#ffffff", mid: "#d5dae3", edge: "#9aa3b2", line: "#b4bcc9", inner: "#e6e9ef" },
10
+ };
2
11
  const n = (v) => (Math.round(v * 100) / 100).toString();
3
12
  function hull(points) {
4
13
  const p = [...points].sort((a, b) => a[0] - b[0] || a[1] - b[1]);
@@ -19,9 +28,10 @@ function hull(points) {
19
28
  }
20
29
  const pathOf = (pts) => `M${pts.map(([x, y]) => `${n(x)} ${n(y)}`).join("L")}Z`;
21
30
  /** The flames for these bells, in picture coordinates. Empty when there are none. */
22
- export function plumeMarkup(bells, at, cam, look, prefix) {
31
+ export function plumeMarkup(bells, at, cam, look, prefix, kind = "neutral") {
23
32
  if (bells.length === 0)
24
33
  return "";
34
+ const colour = FLAME_COLOURS[kind];
25
35
  const painted = look === "painted";
26
36
  const stroke = look === "thick" ? 2.4 : 1.2;
27
37
  const parts = [];
@@ -45,17 +55,17 @@ export function plumeMarkup(bells, at, cam, look, prefix) {
45
55
  const rx = Math.max(1.2, (Math.max(...xs) - Math.min(...xs)) / 2);
46
56
  const ry = Math.max(1.2, (Math.max(...ys) - Math.min(...ys)) / 2);
47
57
  const defs = painted
48
- ? `<defs><linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="${n(mouthPoint[0])}" y1="${n(mouthPoint[1])}" x2="${n(tipPoint[0])}" y2="${n(tipPoint[1])}"><stop offset="0" stop-color="#fff4d2"/><stop offset="0.45" stop-color="#ff9a3a"/><stop offset="1" stop-color="#ff4a12" stop-opacity="0"/></linearGradient><radialGradient id="${glow}" gradientUnits="userSpaceOnUse" cx="${n(cx)}" cy="${n(cy)}" r="${n(Math.max(rx, ry))}"><stop offset="0" stop-color="#fff1c4"/><stop offset="1" stop-color="#ff7a1a" stop-opacity="0"/></radialGradient></defs>`
58
+ ? `<defs><linearGradient id="${id}" gradientUnits="userSpaceOnUse" x1="${n(mouthPoint[0])}" y1="${n(mouthPoint[1])}" x2="${n(tipPoint[0])}" y2="${n(tipPoint[1])}"><stop offset="0" stop-color="${colour.core}"/><stop offset="0.45" stop-color="${colour.mid}"/><stop offset="1" stop-color="${colour.edge}" stop-opacity="0"/></linearGradient><radialGradient id="${glow}" gradientUnits="userSpaceOnUse" cx="${n(cx)}" cy="${n(cy)}" r="${n(Math.max(rx, ry))}"><stop offset="0" stop-color="${colour.core}"/><stop offset="1" stop-color="${colour.mid}" stop-opacity="0"/></radialGradient></defs>`
49
59
  : "";
50
- const flame = shape.length >= 3 ? (painted ? `<path data-plume="1" d="${pathOf(shape)}" fill="url(#${id})" stroke="none"/>` : `<path data-plume="1" d="${pathOf(shape)}" fill="none" stroke="#e7a24a" stroke-width="${stroke}"/><path d="M${n(mouthPoint[0])} ${n(mouthPoint[1])}L${n(tipPoint[0])} ${n(tipPoint[1])}" fill="none" stroke="#ffd7a1" stroke-width="${stroke * 0.6}"/>`) : "";
51
- const ring = painted ? `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.35)}" ry="${n(ry * 1.35)}" fill="url(#${glow})"/>` : `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.2)}" ry="${n(ry * 1.2)}" fill="none" stroke="#e7a24a" stroke-width="${stroke}"/>`;
60
+ const flame = shape.length >= 3 ? (painted ? `<path data-plume="1" d="${pathOf(shape)}" fill="url(#${id})" stroke="none"/>` : `<path data-plume="1" d="${pathOf(shape)}" fill="none" stroke="${colour.line}" stroke-width="${stroke}"/><path d="M${n(mouthPoint[0])} ${n(mouthPoint[1])}L${n(tipPoint[0])} ${n(tipPoint[1])}" fill="none" stroke="${colour.inner}" stroke-width="${stroke * 0.6}"/>`) : "";
61
+ const ring = painted ? `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.35)}" ry="${n(ry * 1.35)}" fill="url(#${glow})"/>` : `<ellipse data-glow="1" cx="${n(cx)}" cy="${n(cy)}" rx="${n(rx * 1.2)}" ry="${n(ry * 1.2)}" fill="none" stroke="${colour.line}" stroke-width="${stroke}"/>`;
52
62
  // the first few shock diamonds, as small rings down the flame: a picture
53
63
  let diamonds = "";
54
64
  for (let k = 1; k <= 3; k++) {
55
65
  const along = k / PLUME_DIAMONDS;
56
66
  const [dx, dy] = cam.project([mouth[0], mouth[1] - plumeLength(bell.exit) * along, mouth[2]]);
57
67
  const size = n(Math.max(0.9, rx * 0.5 * (1 - along)));
58
- diamonds += painted ? `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="#fff4d2" fill-opacity="0.7"/>` : `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="none" stroke="#e7a24a" stroke-width="${stroke * 0.6}"/>`;
68
+ diamonds += painted ? `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="${colour.core}" fill-opacity="0.7"/>` : `<ellipse data-diamond="1" cx="${n(dx)}" cy="${n(dy)}" rx="${size}" ry="${size}" fill="none" stroke="${colour.line}" stroke-width="${stroke * 0.6}"/>`;
59
69
  }
60
70
  if (flame !== "")
61
71
  parts.push(`<g>${defs}${ring}${flame}${diamonds}</g>`);
@@ -1,7 +1,6 @@
1
+ import type { FlameKind } from "../data/propellants.ts";
1
2
  import { BufferGeometry, Group, PointLight, ShaderMaterial } from "three";
2
3
  import { type Bell } from "../engines.ts";
3
- /** What burns in each engine's flame, as a picture: a fuel's flame is drawn in its own colours. */
4
- export type Fuel = "methane" | "kerosene";
5
4
  /** One part's flames: at most as many as it has bells. Its buffers are made once and written when the engines that burn change. */
6
5
  export declare class PartPlumes {
7
6
  readonly group: Group<import("three").Object3DEventMap>;
@@ -9,23 +8,23 @@ export declare class PartPlumes {
9
8
  private flames;
10
9
  private glows;
11
10
  private key;
12
- constructor(kit: PlumeKit, bells: readonly Bell[]);
11
+ constructor(kit: PlumeKit, bells: readonly Bell[], kind: FlameKind);
13
12
  /** The bells that burn now. Nothing is written when they are the ones that burned last. */
14
13
  light(burning: readonly Bell[]): void;
15
14
  dispose(): void;
16
15
  }
17
- /** One set of everything a flame needs, for one viewer. */
16
+ /** One set of everything a flame needs, for one viewer. The materials of each kind of flame share one program and one set of clocks; only the two colours differ. */
18
17
  export declare class PlumeKit {
19
18
  readonly shape: BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
20
19
  readonly quad: BufferGeometry<import("three").NormalBufferAttributes, import("three").BufferGeometryEventMap>;
21
- readonly flame: ShaderMaterial;
22
- readonly glow: ShaderMaterial;
23
20
  /** The hull's own glow in the painted look. Always in the scene, at no strength when no engine burns, so lighting engines never makes the materials build again. */
24
21
  readonly light: PointLight;
25
22
  private uniforms;
26
- constructor();
23
+ private kinds;
24
+ private kind;
25
+ flameFor(kind: FlameKind): ShaderMaterial;
26
+ glowFor(kind: FlameKind): ShaderMaterial;
27
27
  setLook(painted: boolean): void;
28
- setFuel(fuel: Fuel): void;
29
28
  /** 0 near the ground, 1 high up where a flame fans out. */
30
29
  setAir(air: number): void;
31
30
  /** Where the ground stands, for a flame to stop at. */
@@ -1,4 +1,3 @@
1
- // Flames for the 3D view, made here and loaded only with it. One shape, one material and one instanced draw for every engine of a part, however many burn, so lighting and dousing engines changes numbers, never what the graphics card holds. The look of a flame (its shock diamonds, its length, its glow) is a picture, not a measurement.
2
1
  import { AdditiveBlending, BufferGeometry, Color, DoubleSide, DynamicDrawUsage, Float32BufferAttribute, Group, InstancedMesh, Matrix4, PointLight, Quaternion, ShaderMaterial, Vector3 } from "three";
3
2
  import { PLUME_DIAMONDS, plumeLength } from "../engines.js";
4
3
  const STEPS = 28;
@@ -100,9 +99,14 @@ function glowShape() {
100
99
  geometry.setIndex([0, 1, 2, 0, 2, 3]);
101
100
  return geometry;
102
101
  }
103
- const FUEL_COLOURS = {
104
- methane: { hot: "#fff1e6", cool: "#6f7dff" },
102
+ /** What a flame is drawn in: the kind of flame its engine's propellant gives (`src/data/propellants.ts`), and a neutral grey-white where no page read says. A picture, not a measurement. */
103
+ const FLAME_TONES = {
105
104
  kerosene: { hot: "#fff4d2", cool: "#ff7a1a" },
105
+ methane: { hot: "#fff1e6", cool: "#6f7dff" },
106
+ hydrogen: { hot: "#ffffff", cool: "#6fa8ff" },
107
+ solid: { hot: "#fff1c4", cool: "#ff4a12" },
108
+ hypergolic: { hot: "#ffffff", cool: "#b8c4ee" },
109
+ neutral: { hot: "#ffffff", cool: "#9aa3b2" },
106
110
  };
107
111
  const place = new Matrix4();
108
112
  const still = new Quaternion();
@@ -115,11 +119,11 @@ export class PartPlumes {
115
119
  flames;
116
120
  glows;
117
121
  key = "";
118
- constructor(kit, bells) {
122
+ constructor(kit, bells, kind) {
119
123
  this.bells = bells;
120
124
  this.group.name = "plumes";
121
- this.flames = new InstancedMesh(kit.shape, kit.flame, bells.length);
122
- this.glows = new InstancedMesh(kit.quad, kit.glow, bells.length);
125
+ this.flames = new InstancedMesh(kit.shape, kit.flameFor(kind), bells.length);
126
+ this.glows = new InstancedMesh(kit.quad, kit.glowFor(kind), bells.length);
123
127
  for (const mesh of [this.flames, this.glows]) {
124
128
  mesh.instanceMatrix.setUsage(DynamicDrawUsage);
125
129
  mesh.frustumCulled = false;
@@ -149,12 +153,10 @@ export class PartPlumes {
149
153
  this.glows.dispose();
150
154
  }
151
155
  }
152
- /** One set of everything a flame needs, for one viewer. */
156
+ /** One set of everything a flame needs, for one viewer. The materials of each kind of flame share one program and one set of clocks; only the two colours differ. */
153
157
  export class PlumeKit {
154
158
  shape = flameShape();
155
159
  quad = glowShape();
156
- flame;
157
- glow;
158
160
  /** The hull's own glow in the painted look. Always in the scene, at no strength when no engine burns, so lighting engines never makes the materials build again. */
159
161
  light = new PointLight("#ffb56b", 0, 0, 1.6);
160
162
  uniforms = {
@@ -164,21 +166,31 @@ export class PlumeKit {
164
166
  uLine: { value: 1 },
165
167
  uFloor: { value: -1e6 },
166
168
  uGlow: { value: 0.75 },
167
- uHot: { value: new Color(FUEL_COLOURS.kerosene.hot) },
168
- uCool: { value: new Color(FUEL_COLOURS.kerosene.cool) },
169
169
  };
170
- constructor() {
171
- const common = { transparent: true, depthWrite: false, blending: AdditiveBlending, toneMapped: false };
172
- this.flame = new ShaderMaterial({ ...common, vertexShader: FLAME_VERTEX, fragmentShader: FLAME_FRAGMENT, uniforms: this.uniforms, side: DoubleSide });
173
- this.glow = new ShaderMaterial({ ...common, vertexShader: GLOW_VERTEX, fragmentShader: GLOW_FRAGMENT, uniforms: this.uniforms });
170
+ kinds = new Map();
171
+ kind(kind) {
172
+ const known = this.kinds.get(kind);
173
+ if (known)
174
+ return known;
175
+ const tone = FLAME_TONES[kind];
176
+ const uniforms = { ...this.uniforms, uHot: { value: new Color(tone.hot) }, uCool: { value: new Color(tone.cool) } };
177
+ const common = { transparent: true, depthWrite: false, blending: AdditiveBlending, toneMapped: false, uniforms };
178
+ const made = {
179
+ flame: new ShaderMaterial({ ...common, vertexShader: FLAME_VERTEX, fragmentShader: FLAME_FRAGMENT, side: DoubleSide }),
180
+ glow: new ShaderMaterial({ ...common, vertexShader: GLOW_VERTEX, fragmentShader: GLOW_FRAGMENT }),
181
+ };
182
+ this.kinds.set(kind, made);
183
+ return made;
184
+ }
185
+ flameFor(kind) {
186
+ return this.kind(kind).flame;
187
+ }
188
+ glowFor(kind) {
189
+ return this.kind(kind).glow;
174
190
  }
175
191
  setLook(painted) {
176
192
  this.uniforms.uLine.value = painted ? 0 : 1;
177
193
  }
178
- setFuel(fuel) {
179
- this.uniforms.uHot.value.set(FUEL_COLOURS[fuel].hot);
180
- this.uniforms.uCool.value.set(FUEL_COLOURS[fuel].cool);
181
- }
182
194
  /** 0 near the ground, 1 high up where a flame fans out. */
183
195
  setAir(air) {
184
196
  this.uniforms.uAir.value = air;
@@ -200,8 +212,11 @@ export class PlumeKit {
200
212
  dispose() {
201
213
  this.shape.dispose();
202
214
  this.quad.dispose();
203
- this.flame.dispose();
204
- this.glow.dispose();
215
+ for (const { flame, glow } of this.kinds.values()) {
216
+ flame.dispose();
217
+ glow.dispose();
218
+ }
219
+ this.kinds.clear();
205
220
  this.light.dispose();
206
221
  this.light.removeFromParent();
207
222
  }
@@ -77,8 +77,6 @@ void main() {
77
77
  /** The materials of an engine's bell, in this package's own meshes. */
78
78
  const NOZZLE_MATERIALS = new Set(["bell", "bell-inside", "engine-metal"]);
79
79
  const asVec = (v) => [v.x, v.y, v.z];
80
- /** A flame's colours, a picture: Raptor burns methane, Merlin kerosene. */
81
- const fuelOf = (built) => (built.report.parts.some(({ part }) => part.rocket === "starship") ? "methane" : "kerosene");
82
80
  /** The pictures a licensed model's material may hold (the shared sky it reflects is not one of them). */
83
81
  const MODEL_PICTURES = ["map", "normalMap", "roughnessMap", "metalnessMap", "aoMap", "emissiveMap"];
84
82
  export class RocketViewer {
@@ -242,8 +240,20 @@ export class RocketViewer {
242
240
  to: [0, 0, 0],
243
241
  hideAtEnd: false,
244
242
  plumes: null,
243
+ moves: [],
245
244
  };
246
245
  this.takeMeshes(source, code, view);
246
+ for (const node of source.children.slice(1)) {
247
+ const axis = node.userData.axis;
248
+ if (!axis)
249
+ continue;
250
+ const group = new Group();
251
+ group.name = node.name;
252
+ group.position.copy(node.position);
253
+ code.add(group);
254
+ this.takeMeshes(node, group, view);
255
+ view.moves.push({ name: String(node.userData.group), group, axis: new Vector3(axis[0], axis[1], axis[2]).normalize(), angle: 0 });
256
+ }
247
257
  const place = places.find((p) => p.slot === part.slot);
248
258
  holder.position.set(...(place?.at ?? [0, 0, 0]));
249
259
  holder.visible = place?.shown ?? false;
@@ -252,7 +262,6 @@ export class RocketViewer {
252
262
  }
253
263
  this.addGlow();
254
264
  this.setLook(look);
255
- this.plumes.setFuel(fuelOf(built));
256
265
  this.frameTo(angle ?? this.currentAngle(), false);
257
266
  if (this.clockNow) {
258
267
  this.flightKey = "";
@@ -455,10 +464,11 @@ export class RocketViewer {
455
464
  for (const view of this.parts) {
456
465
  const count = counts.get(view.slot) ?? 0;
457
466
  if (count > 0 && !view.plumes) {
458
- const bells = bellsOf(this.built.parts.find((part) => part.slot === view.slot)?.pieces ?? []);
467
+ const built = this.built.parts.find((part) => part.slot === view.slot);
468
+ const bells = bellsOf(built?.pieces ?? []);
459
469
  if (bells.length === 0)
460
470
  continue;
461
- view.plumes = new PartPlumes(this.plumes, bells);
471
+ view.plumes = new PartPlumes(this.plumes, bells, built?.flame ?? "neutral");
462
472
  view.holder.add(view.plumes.group);
463
473
  }
464
474
  if (!view.plumes)
@@ -507,7 +517,12 @@ export class RocketViewer {
507
517
  part.holder.position.set(...(place?.at ?? [0, 0, 0]));
508
518
  part.holder.rotation.set(0, 0, 0);
509
519
  part.holder.visible = place?.shown ?? false;
520
+ for (const move of part.moves) {
521
+ move.angle = 0;
522
+ move.group.quaternion.identity();
523
+ }
510
524
  }
525
+ this.canvas.dataset.turned = "";
511
526
  this.plumes.setAir(0);
512
527
  this.plumes.setFloor(-1e6);
513
528
  this.reframe();
@@ -583,10 +598,20 @@ export class RocketViewer {
583
598
  continue;
584
599
  const turn = MathUtils.degToRad(pose.pitch);
585
600
  const middle = (parts.find((one) => one.slot === part.slot)?.height ?? 0) / 2;
601
+ for (const move of part.moves) {
602
+ const angle = pose.moves[move.name] ?? 0;
603
+ if (angle === move.angle)
604
+ continue;
605
+ move.angle = angle;
606
+ move.group.quaternion.setFromAxisAngle(move.axis, MathUtils.degToRad(angle));
607
+ }
586
608
  part.holder.visible = !pose.gone;
587
609
  part.holder.rotation.set(0, 0, turn);
588
610
  part.holder.position.set(place.at[0] + pose.dx + Math.sin(turn) * middle, place.at[1] + pose.dy + middle - Math.cos(turn) * middle, place.at[2]);
589
611
  }
612
+ const turned = this.parts.flatMap((part) => part.moves.filter((move) => move.angle !== 0).map((move) => `${part.slot}:${move.group.name.split("~").slice(1).join("~")}=${Math.round(move.angle)}`)).join(" ");
613
+ if (turned !== this.canvas.dataset.turned)
614
+ this.canvas.dataset.turned = turned;
590
615
  this.plumes.setAir(frame.air);
591
616
  this.plumes.setFloor(frame.groundY);
592
617
  this.ground?.set(frame.groundY, this.options.reducedMotion ? 0 : frame.smoke, performance.now() / 1000);
package/dist/element.d.ts CHANGED
@@ -3,10 +3,45 @@ import { UchusenFlight } from "./flightbar.ts";
3
3
  import type { Description, FitReport } from "./fit.ts";
4
4
  import { type Angle, type ViewChoice } from "./framing.ts";
5
5
  import { ROCKETS } from "./rockets.ts";
6
+ /**
7
+ * What a `<uchusen-rocket>` is doing, as its `data-state` attribute says: `picture` (the drawing), `loading` (the 3D view is on its way), `3d`, `fallback` (no WebGL, so the drawing stays) or `error`.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import type { RocketState } from "@johnmorrisdotca/uchusen-rockets/element";
12
+ *
13
+ * const states: RocketState[] = ["picture", "loading", "3d", "fallback", "error"];
14
+ * console.log(states.length);
15
+ * ```
16
+ */
6
17
  export type RocketState = "picture" | "loading" | "3d" | "fallback" | "error";
7
18
  declare const HTMLBase: typeof HTMLElement;
8
- /** Where the licensed models are, for every element with no `assets` attribute of its own. Left unset: beside this module, in ./assets/. */
19
+ /**
20
+ * Where the licensed models are, for every element with no `assets` attribute of its own. Left unset: beside this module, in ./assets/.
21
+ *
22
+ * @param url The folder the licensed models are in. A trailing slash is added if it has none.
23
+ * @example
24
+ * ```ts no-run
25
+ * import { setAssetBase } from "@johnmorrisdotca/uchusen-rockets/element";
26
+ *
27
+ * setAssetBase("/static/uchusen-assets/");
28
+ * ```
29
+ */
9
30
  export declare function setAssetBase(url: string): void;
31
+ /**
32
+ * The `<uchusen-rocket>` element: a rocket as a drawing that a 3D view takes over from when the browser can draw WebGL. Its attributes are the words of `RocketOptions`. It exposes its state as `data-state` and loads three.js only for a 3D view.
33
+ *
34
+ * @example
35
+ * ```ts no-run
36
+ * import { UchusenRocket } from "@johnmorrisdotca/uchusen-rockets/element";
37
+ *
38
+ * const rocket: UchusenRocket = document.createElement("uchusen-rocket") as UchusenRocket;
39
+ * rocket.setAttribute("rocket", "starship");
40
+ * rocket.setAttribute("variant", "v3");
41
+ * rocket.setAttribute("look", "painted");
42
+ * document.body.append(rocket);
43
+ * ```
44
+ */
10
45
  export declare class UchusenRocket extends HTMLBase {
11
46
  static readonly observedAttributes: string[];
12
47
  private descriptionValue;
@@ -117,14 +152,37 @@ export declare class UchusenRocket extends HTMLBase {
117
152
  private start;
118
153
  private startTilt;
119
154
  }
120
- /** Rockets side by side on one baseline at one scale: `<uchusen-lineup items="starship:v1,starship:v3,person,building" unit="4" ruler labels>`. */
155
+ /**
156
+ * Rockets side by side on one baseline at one scale: `<uchusen-lineup items="starship:v1,starship:v3,person,building" unit="4" ruler labels>`.
157
+ *
158
+ * @example
159
+ * ```ts no-run
160
+ * import { UchusenLineup } from "@johnmorrisdotca/uchusen-rockets/element";
161
+ *
162
+ * const lineup: UchusenLineup = document.createElement("uchusen-lineup") as UchusenLineup;
163
+ * lineup.setAttribute("items", "starship:v2,starship:v3,person");
164
+ * lineup.setAttribute("unit", "3");
165
+ * lineup.toggleAttribute("ruler", true);
166
+ * document.body.append(lineup);
167
+ * ```
168
+ */
121
169
  export declare class UchusenLineup extends HTMLBase {
122
170
  static readonly observedAttributes: string[];
123
171
  connectedCallback(): void;
124
172
  attributeChangedCallback(name: string, before: string | null, after: string | null): void;
125
173
  private render;
126
174
  }
127
- /** Defines both elements, once. Importing this module does it. */
175
+ /**
176
+ * Defines both elements, once. Importing this module does it.
177
+ *
178
+ * @example
179
+ * ```ts no-run
180
+ * import { defineUchusenRocket } from "@johnmorrisdotca/uchusen-rockets/element";
181
+ *
182
+ * defineUchusenRocket();
183
+ * console.log(customElements.get("uchusen-rocket") !== undefined);
184
+ * ```
185
+ */
128
186
  export declare function defineUchusenRocket(): void;
129
187
  export { ROCKETS, UchusenFlight };
130
188
  declare global {
package/dist/element.js CHANGED
@@ -21,7 +21,17 @@ const RELEASE_MS = 30_000;
21
21
  /** The drawing, followed along a turn, is made again at most this often. */
22
22
  const PLACE_MS = 50;
23
23
  let assetBase = null;
24
- /** Where the licensed models are, for every element with no `assets` attribute of its own. Left unset: beside this module, in ./assets/. */
24
+ /**
25
+ * Where the licensed models are, for every element with no `assets` attribute of its own. Left unset: beside this module, in ./assets/.
26
+ *
27
+ * @param url The folder the licensed models are in. A trailing slash is added if it has none.
28
+ * @example
29
+ * ```ts no-run
30
+ * import { setAssetBase } from "@johnmorrisdotca/uchusen-rockets/element";
31
+ *
32
+ * setAssetBase("/static/uchusen-assets/");
33
+ * ```
34
+ */
25
35
  export function setAssetBase(url) {
26
36
  assetBase = url.endsWith("/") ? url : `${url}/`;
27
37
  }
@@ -86,6 +96,20 @@ function whyNot(mode) {
86
96
  return "no-webgl";
87
97
  return "";
88
98
  }
99
+ /**
100
+ * The `<uchusen-rocket>` element: a rocket as a drawing that a 3D view takes over from when the browser can draw WebGL. Its attributes are the words of `RocketOptions`. It exposes its state as `data-state` and loads three.js only for a 3D view.
101
+ *
102
+ * @example
103
+ * ```ts no-run
104
+ * import { UchusenRocket } from "@johnmorrisdotca/uchusen-rockets/element";
105
+ *
106
+ * const rocket: UchusenRocket = document.createElement("uchusen-rocket") as UchusenRocket;
107
+ * rocket.setAttribute("rocket", "starship");
108
+ * rocket.setAttribute("variant", "v3");
109
+ * rocket.setAttribute("look", "painted");
110
+ * document.body.append(rocket);
111
+ * ```
112
+ */
89
113
  export class UchusenRocket extends HTMLBase {
90
114
  static observedAttributes = ["rocket", "variant", "show", "look", "view", "build", "mode", "assets", "spin", "way", "label", "engines", "phase"];
91
115
  descriptionValue = null;
@@ -479,11 +503,12 @@ export class UchusenRocket extends HTMLBase {
479
503
  const from = this.driven ? turnedBy(angle, this.driven.yaw, this.driven.tilt) : (this.viewer?.currentAngle() ?? angle);
480
504
  const label = this.getAttribute("label") ?? this.builtNow.report.label;
481
505
  const lit = flying ? (media("(prefers-reduced-motion: reduce)") ? false : flying.burns) : this.engineLights();
482
- const key = `${this.key}|${look}|${show}|${from.az.toFixed(2)}|${from.el.toFixed(2)}|${label}|${flying ? flying.index : ""}|${JSON.stringify(lit)}`;
506
+ const moves = flying ? Object.fromEntries(Object.entries(flying.phase.pose).flatMap(([slot, pose]) => (pose.moves ? [[slot, pose.moves]] : []))) : undefined;
507
+ const key = `${this.key}|${look}|${show}|${from.az.toFixed(2)}|${from.el.toFixed(2)}|${label}|${flying ? flying.index : ""}|${JSON.stringify(lit)}|${JSON.stringify(moves ?? {})}`;
483
508
  let node = this.drawing.firstElementChild;
484
509
  if (key !== this.drawn.key || !node) {
485
510
  const template = document.createElement("template");
486
- template.innerHTML = this.builtNow.svg({ look, show, view: from, engines: lit });
511
+ template.innerHTML = this.builtNow.svg({ look, show, view: from, engines: lit, ...(moves ? { moves } : {}) });
487
512
  node = template.content.firstElementChild;
488
513
  if (!node)
489
514
  return;
@@ -715,7 +740,20 @@ const LINEUP_CSS = `
715
740
  :host { display: block; overflow-x: auto; color: inherit; }
716
741
  svg { display: block; }
717
742
  `;
718
- /** Rockets side by side on one baseline at one scale: `<uchusen-lineup items="starship:v1,starship:v3,person,building" unit="4" ruler labels>`. */
743
+ /**
744
+ * Rockets side by side on one baseline at one scale: `<uchusen-lineup items="starship:v1,starship:v3,person,building" unit="4" ruler labels>`.
745
+ *
746
+ * @example
747
+ * ```ts no-run
748
+ * import { UchusenLineup } from "@johnmorrisdotca/uchusen-rockets/element";
749
+ *
750
+ * const lineup: UchusenLineup = document.createElement("uchusen-lineup") as UchusenLineup;
751
+ * lineup.setAttribute("items", "starship:v2,starship:v3,person");
752
+ * lineup.setAttribute("unit", "3");
753
+ * lineup.toggleAttribute("ruler", true);
754
+ * document.body.append(lineup);
755
+ * ```
756
+ */
719
757
  export class UchusenLineup extends HTMLBase {
720
758
  static observedAttributes = ["items", "unit", "look", "view", "show", "ruler", "labels"];
721
759
  connectedCallback() {
@@ -754,7 +792,17 @@ export class UchusenLineup extends HTMLBase {
754
792
  this.dataset.count = String(shown.length);
755
793
  }
756
794
  }
757
- /** Defines both elements, once. Importing this module does it. */
795
+ /**
796
+ * Defines both elements, once. Importing this module does it.
797
+ *
798
+ * @example
799
+ * ```ts no-run
800
+ * import { defineUchusenRocket } from "@johnmorrisdotca/uchusen-rockets/element";
801
+ *
802
+ * defineUchusenRocket();
803
+ * console.log(customElements.get("uchusen-rocket") !== undefined);
804
+ * ```
805
+ */
758
806
  export function defineUchusenRocket() {
759
807
  if (typeof customElements === "undefined")
760
808
  return;