@johnmorrisdotca/uchusen-rockets 0.19.0 → 0.20.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 (130) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/LICENSES/Orbiter-MIT.txt +21 -0
  3. package/NOTICE.md +37 -1
  4. package/README.md +14 -7
  5. package/dist/assets/falcon-heavy-test-flight-apart.painted.svg +1 -0
  6. package/dist/assets/falcon-heavy-test-flight-apart.svg +1 -0
  7. package/dist/assets/falcon-heavy-test-flight-apart.thick.svg +1 -0
  8. package/dist/assets/falcon-heavy-test-flight-car.glb +0 -0
  9. package/dist/assets/falcon-heavy-test-flight-car.painted.svg +1 -0
  10. package/dist/assets/falcon-heavy-test-flight-car.svg +1 -0
  11. package/dist/assets/falcon-heavy-test-flight-car.thick.svg +1 -0
  12. package/dist/assets/falcon-heavy-test-flight-centre.glb +0 -0
  13. package/dist/assets/falcon-heavy-test-flight-centre.painted.svg +1 -0
  14. package/dist/assets/falcon-heavy-test-flight-centre.svg +1 -0
  15. package/dist/assets/falcon-heavy-test-flight-centre.thick.svg +1 -0
  16. package/dist/assets/falcon-heavy-test-flight-side-left.glb +0 -0
  17. package/dist/assets/falcon-heavy-test-flight-side-left.painted.svg +1 -0
  18. package/dist/assets/falcon-heavy-test-flight-side-left.svg +1 -0
  19. package/dist/assets/falcon-heavy-test-flight-side-left.thick.svg +1 -0
  20. package/dist/assets/falcon-heavy-test-flight-side-right.glb +0 -0
  21. package/dist/assets/falcon-heavy-test-flight-side-right.painted.svg +1 -0
  22. package/dist/assets/falcon-heavy-test-flight-side-right.svg +1 -0
  23. package/dist/assets/falcon-heavy-test-flight-side-right.thick.svg +1 -0
  24. package/dist/assets/falcon-heavy-test-flight-starman.glb +0 -0
  25. package/dist/assets/falcon-heavy-test-flight-starman.painted.svg +1 -0
  26. package/dist/assets/falcon-heavy-test-flight-starman.svg +1 -0
  27. package/dist/assets/falcon-heavy-test-flight-starman.thick.svg +1 -0
  28. package/dist/assets/falcon-heavy-test-flight.glb +0 -0
  29. package/dist/assets/falcon-heavy-test-flight.painted.svg +1 -0
  30. package/dist/assets/falcon-heavy-test-flight.svg +1 -0
  31. package/dist/assets/falcon-heavy-test-flight.thick.svg +1 -0
  32. package/dist/assets/models/roadster-first-generation-car.glb +0 -0
  33. package/dist/assets/models/roadster-second-generation-2-car.glb +0 -0
  34. package/dist/assets/models/roadster-second-generation-car.glb +0 -0
  35. package/dist/assets/roadster-first-generation-apart.painted.svg +1 -0
  36. package/dist/assets/roadster-first-generation-apart.svg +1 -0
  37. package/dist/assets/roadster-first-generation-apart.thick.svg +1 -0
  38. package/dist/assets/roadster-first-generation-car.glb +0 -0
  39. package/dist/assets/roadster-first-generation-car.painted.svg +1 -0
  40. package/dist/assets/roadster-first-generation-car.svg +1 -0
  41. package/dist/assets/roadster-first-generation-car.thick.svg +1 -0
  42. package/dist/assets/roadster-first-generation-starman.glb +0 -0
  43. package/dist/assets/roadster-first-generation-starman.painted.svg +1 -0
  44. package/dist/assets/roadster-first-generation-starman.svg +1 -0
  45. package/dist/assets/roadster-first-generation-starman.thick.svg +1 -0
  46. package/dist/assets/roadster-first-generation.glb +0 -0
  47. package/dist/assets/roadster-first-generation.painted.svg +1 -0
  48. package/dist/assets/roadster-first-generation.svg +1 -0
  49. package/dist/assets/roadster-first-generation.thick.svg +1 -0
  50. package/dist/assets/roadster-second-generation-2-apart.painted.svg +1 -0
  51. package/dist/assets/roadster-second-generation-2-apart.svg +1 -0
  52. package/dist/assets/roadster-second-generation-2-apart.thick.svg +1 -0
  53. package/dist/assets/roadster-second-generation-2-car.glb +0 -0
  54. package/dist/assets/roadster-second-generation-2-car.painted.svg +1 -0
  55. package/dist/assets/roadster-second-generation-2-car.svg +1 -0
  56. package/dist/assets/roadster-second-generation-2-car.thick.svg +1 -0
  57. package/dist/assets/roadster-second-generation-2-starman.glb +0 -0
  58. package/dist/assets/roadster-second-generation-2-starman.painted.svg +1 -0
  59. package/dist/assets/roadster-second-generation-2-starman.svg +1 -0
  60. package/dist/assets/roadster-second-generation-2-starman.thick.svg +1 -0
  61. package/dist/assets/roadster-second-generation-2.glb +0 -0
  62. package/dist/assets/roadster-second-generation-2.painted.svg +1 -0
  63. package/dist/assets/roadster-second-generation-2.svg +1 -0
  64. package/dist/assets/roadster-second-generation-2.thick.svg +1 -0
  65. package/dist/assets/roadster-second-generation-apart.painted.svg +1 -0
  66. package/dist/assets/roadster-second-generation-apart.svg +1 -0
  67. package/dist/assets/roadster-second-generation-apart.thick.svg +1 -0
  68. package/dist/assets/roadster-second-generation-car.glb +0 -0
  69. package/dist/assets/roadster-second-generation-car.painted.svg +1 -0
  70. package/dist/assets/roadster-second-generation-car.svg +1 -0
  71. package/dist/assets/roadster-second-generation-car.thick.svg +1 -0
  72. package/dist/assets/roadster-second-generation-starman.glb +0 -0
  73. package/dist/assets/roadster-second-generation-starman.painted.svg +1 -0
  74. package/dist/assets/roadster-second-generation-starman.svg +1 -0
  75. package/dist/assets/roadster-second-generation-starman.thick.svg +1 -0
  76. package/dist/assets/roadster-second-generation.glb +0 -0
  77. package/dist/assets/roadster-second-generation.painted.svg +1 -0
  78. package/dist/assets/roadster-second-generation.svg +1 -0
  79. package/dist/assets/roadster-second-generation.thick.svg +1 -0
  80. package/dist/data/countries.d.ts +51 -0
  81. package/dist/data/countries.js +91 -0
  82. package/dist/data/credits.d.ts +36 -0
  83. package/dist/data/credits.js +36 -0
  84. package/dist/data/falcon.js +26 -14
  85. package/dist/data/makers.js +1 -0
  86. package/dist/data/roadster.d.ts +13 -0
  87. package/dist/data/roadster.js +73 -0
  88. package/dist/data/sources.d.ts +56 -0
  89. package/dist/data/sources.js +7 -0
  90. package/dist/element/flag.d.ts +30 -0
  91. package/dist/element/flag.js +23 -0
  92. package/dist/element/probe.d.ts +15 -0
  93. package/dist/element/probe.js +46 -0
  94. package/dist/element/scene.d.ts +7 -0
  95. package/dist/element/scene.js +38 -10
  96. package/dist/element.d.ts +7 -0
  97. package/dist/element.js +74 -20
  98. package/dist/index.d.ts +3 -0
  99. package/dist/index.js +2 -0
  100. package/dist/inspace.js +1 -0
  101. package/dist/model/registry.js +6 -4
  102. package/dist/model/roadster.d.ts +31 -0
  103. package/dist/model/roadster.js +112 -0
  104. package/dist/parts.d.ts +1 -1
  105. package/dist/physics/atmosphere.d.ts +182 -0
  106. package/dist/physics/atmosphere.js +246 -0
  107. package/dist/physics/attitude.d.ts +342 -0
  108. package/dist/physics/attitude.js +351 -0
  109. package/dist/physics/burns.d.ts +194 -0
  110. package/dist/physics/burns.js +157 -0
  111. package/dist/physics/elements.d.ts +133 -0
  112. package/dist/physics/elements.js +239 -0
  113. package/dist/physics/gravity.d.ts +53 -0
  114. package/dist/physics/gravity.js +78 -0
  115. package/dist/physics/index.d.ts +17 -0
  116. package/dist/physics/index.js +8 -0
  117. package/dist/physics/propagate.d.ts +110 -0
  118. package/dist/physics/propagate.js +122 -0
  119. package/dist/physics/rocket.d.ts +105 -0
  120. package/dist/physics/rocket.js +102 -0
  121. package/dist/physics/transfers.d.ts +128 -0
  122. package/dist/physics/transfers.js +155 -0
  123. package/dist/physics/vector.d.ts +20 -0
  124. package/dist/physics/vector.js +9 -0
  125. package/dist/react.d.ts +1 -1
  126. package/dist/rockets.d.ts +5 -0
  127. package/dist/rockets.js +9 -4
  128. package/dist/sun.d.ts +13 -0
  129. package/dist/sun.js +25 -0
  130. package/package.json +11 -1
@@ -31,6 +31,7 @@ import { placeNameLabels } from "./namePlace.js";
31
31
  import { plinthGroup } from "./plinth.js";
32
32
  import { PartPlumes, PlumeKit } from "./plumes.js";
33
33
  import { ThrusterLayer } from "./thrusterLayer.js";
34
+ import { glProbe } from "./probe.js";
34
35
  import { craftJets, endQuads } from "../thrusters.js";
35
36
  /** The ships whose lamps turn the sun down in the Lit look. Kit modules stay in daylight. */
36
37
  const NIGHT_SHIPS = new Set(["fleet-ship-01", "fleet-kyotori", "fleet-drone", "fleet-ship-03", "fleet-ship-ariake", "fleet-02", "fleet-02-original", "fleet-kaminari", "fleet-emiake", "fleet-romaryu"]);
@@ -55,15 +56,8 @@ const COMPILE_MS = 4000;
55
56
  /** A change of craft, section or Fit eases the camera for this long, even in scale. */
56
57
  const SIZE_EASE_MS = 800;
57
58
  const autoRotateOf = (spin) => (spin.seconds <= 0 ? 0 : (60 / spin.seconds) * (spin.way === "cw" ? -1 : 1));
58
- /** A real graphics card keeps smooth edges. Software drawing spends the frame on them, and a heavy ship then misses a frame of the wait. */
59
- const smoothEdges = () => {
60
- const canvas = document.createElement("canvas");
61
- const gl = canvas.getContext("webgl2");
62
- const info = gl?.getExtension("WEBGL_debug_renderer_info");
63
- const name = info && gl ? String(gl.getParameter(info.UNMASKED_RENDERER_WEBGL)) : "";
64
- gl?.getExtension("WEBGL_lose_context")?.loseContext();
65
- return !/swiftshader|llvmpipe|softpipe/i.test(name);
66
- };
59
+ /** A real graphics card keeps smooth edges. Software drawing spends the frame on them, and a heavy ship then misses a frame of the wait. The page's one WebGL probe says which this is. */
60
+ const smoothEdges = () => !glProbe().software;
67
61
  const LINE_VERTEX = /* glsl */ `
68
62
  varying vec3 vNormal;
69
63
  varying vec2 vUv;
@@ -427,6 +421,31 @@ export class RocketViewer {
427
421
  this.offScreen = next;
428
422
  this.wake();
429
423
  }
424
+ /** The direction towards the Sun, in the craft's own frame, when a page gives one; null keeps the showroom light. */
425
+ sunFrom = null;
426
+ /** One light from the Sun's real direction, as in space: the far side dark, with at most a faint fill. Null puts the showroom light back. */
427
+ setSun(direction) {
428
+ const before = this.sunFrom;
429
+ this.sunFrom = direction ? new Vector3(...direction) : null;
430
+ this.canvas.dataset.sun = this.sunFrom ? "on" : "off";
431
+ if (before === this.sunFrom || (before && this.sunFrom && before.equals(this.sunFrom)))
432
+ return;
433
+ this.nightSide();
434
+ this.aimKey();
435
+ this.wake();
436
+ }
437
+ /** The key light comes from the Sun when there is one, else from the showroom's upper front. */
438
+ keyDirection() {
439
+ return (this.sunFrom ?? new Vector3(3, 5, 4)).clone().normalize();
440
+ }
441
+ aimKey() {
442
+ if (this.shadows && this.built) {
443
+ this.fitShadow();
444
+ return;
445
+ }
446
+ this.sun.position.copy(this.sun.target.position).addScaledVector(this.keyDirection(), 7);
447
+ this.sun.target.updateMatrixWorld();
448
+ }
430
449
  /** Open space lets a craft leave the frame. Wrap, the older float, brings it back on the other side. */
431
450
  edgesOpen = false;
432
451
  setEdges(open) {
@@ -1081,6 +1100,15 @@ export class RocketViewer {
1081
1100
  /** The Lit look of a named SPX ship is the night side. Daylight stays the default, so lamps glow in the sun, and a kit module stays in daylight. */
1082
1101
  nightSide() {
1083
1102
  const night = this.shipNight && this.lightsMode !== "off" && this.glowOn;
1103
+ if (this.sunFrom && !night) {
1104
+ // in space there is one light: the Sun, a little stronger than the showroom's key, and next to nothing from anywhere else
1105
+ this.sun.intensity = 2.4;
1106
+ this.rim.intensity = 0;
1107
+ this.fill.intensity = 0;
1108
+ this.sky.intensity = 0.03;
1109
+ this.scene.environmentIntensity = 0.06;
1110
+ return;
1111
+ }
1084
1112
  this.sun.intensity = night ? 0.16 : 1.6;
1085
1113
  this.rim.intensity = night ? 0.04 : 1.1;
1086
1114
  this.fill.intensity = night ? 0.03 : 0.35;
@@ -1607,7 +1635,7 @@ export class RocketViewer {
1607
1635
  const { min, max } = this.built.boundsOf(this.show);
1608
1636
  const centre = new Vector3((min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2);
1609
1637
  const radius = Math.max(1, Math.hypot(max[0] - min[0], max[1] - min[1], max[2] - min[2]) / 2);
1610
- const direction = new Vector3(3, 5, 4).normalize();
1638
+ const direction = this.keyDirection();
1611
1639
  this.sun.position.copy(centre).addScaledVector(direction, radius * 2);
1612
1640
  this.sun.target.position.copy(centre);
1613
1641
  const shadow = this.sun.shadow;
package/dist/element.d.ts CHANGED
@@ -63,8 +63,12 @@ export declare class UchusenRocket extends HTMLBase {
63
63
  private builtNow;
64
64
  private queued;
65
65
  private stateNow;
66
+ /** What the flag in the corner is for: a country's code, "" for none, "off", or "-" before the first look. */
67
+ private flagKey;
66
68
  private shownAt;
67
69
  private starting;
70
+ /** Whether this element has asked the page's WebGL answer since it started watching. */
71
+ private glAsked;
68
72
  private key;
69
73
  private driven;
70
74
  private placeFrame;
@@ -199,6 +203,8 @@ export declare class UchusenRocket extends HTMLBase {
199
203
  /** The drawing for what the attributes say now. It is made from code, so it needs no file. A zoom or a move only changes which part of it the box shows (its viewBox), so the drawing is as sharp as the box and never grows past it. */
200
204
  private redraw;
201
205
  private mark;
206
+ /** `flags`: the rocket's country's flag in the picture's corner, from hata (an optional peer) when the page has it. `data-flag` is the code shown, or empty once it is settled that none is; it is absent while one is loading or the attribute is off. */
207
+ private syncFlag;
202
208
  private applyRocket;
203
209
  /** A fleet ship is built coarse first (the first picture and the far view) and fine when the camera comes near, an inset opens or one part is shown alone: once per visit, never on a turn or a look change. */
204
210
  private fine;
@@ -255,6 +261,7 @@ export declare class UchusenLineup extends HTMLBase {
255
261
  */
256
262
  export declare function defineUchusenRocket(): void;
257
263
  export { ROCKETS, UchusenFlight };
264
+ export { setFlagLoader, type FlagSource } from "./element/flag.ts";
258
265
  export type { FlightWords } from "./flightbar.ts";
259
266
  declare global {
260
267
  interface HTMLElementTagNameMap {
package/dist/element.js CHANGED
@@ -6,6 +6,7 @@ import { buildOf } from "./buildPlay.js";
6
6
  import { bellsOf, enginesWanted, pickBells } from "./engines.js";
7
7
  import { lightMode } from "./model/fleet/lights.js";
8
8
  import { thrusterMode } from "./thrusters.js";
9
+ import { parseSun } from "./sun.js";
9
10
  import { motionsOf, motionSpeed, motionWanted } from "./model/fleet/motion.js";
10
11
  import { engineChart } from "./draw/plume.js";
11
12
  import { burnsOf, FlightClock, flightOf, frameAt, phaseAt, phasePosition } from "./flight.js";
@@ -21,7 +22,9 @@ import { ROCKETS, SLOT_NAMES } from "./rockets.js";
21
22
  import { tiltMode, tiltTurn } from "./tilt.js";
22
23
  import { skyQuery } from "./element/solarGroups.js";
23
24
  import { Panel } from "./element/panel.js";
25
+ import { glProbe } from "./element/probe.js";
24
26
  import { spinOf } from "./spin.js";
27
+ import { flagSvg } from "./element/flag.js";
25
28
  const HTMLBase = typeof HTMLElement === "undefined" ? class {
26
29
  } : HTMLElement;
27
30
  /** A view out of sight keeps its WebGL canvas this long before letting it go (a browser allows only a few at a time). */
@@ -46,6 +49,15 @@ export function setAssetBase(url) {
46
49
  assetBase = url.endsWith("/") ? url : `${url}/`;
47
50
  }
48
51
  const defaultBase = () => assetBase ?? new URL("./assets/", import.meta.url).href;
52
+ /** A country's name in the page's language, or its code where the browser has no name for it. */
53
+ function regionName(code, lang) {
54
+ try {
55
+ return new Intl.DisplayNames([lang || "en"], { type: "region" }).of(code) ?? code;
56
+ }
57
+ catch {
58
+ return code;
59
+ }
60
+ }
49
61
  const CSS = `
50
62
  :host { display: block; position: relative; color: inherit; }
51
63
  :host([hidden]) { display: none; }
@@ -72,6 +84,8 @@ const CSS = `
72
84
  .panel table { border-collapse: collapse; width: 100%; }
73
85
  .panel th, .panel td { text-align: left; padding: 0.15rem 0.4rem; border-bottom: 1px solid color-mix(in srgb, currentColor 25%, transparent); vertical-align: top; }
74
86
  .panel p { margin: 0 0 0.5rem; }
87
+ .flag { position: absolute; top: 0.5rem; left: 0.5rem; display: block; width: 1.5rem; aspect-ratio: 4 / 3; line-height: 0; pointer-events: none; box-shadow: 0 0 0 1px color-mix(in srgb, currentColor 35%, transparent); }
88
+ .flag svg { display: block; width: 100%; height: 100%; }
75
89
  .tilt { position: absolute; right: 0.5rem; bottom: 0.5rem; min-height: 2rem; padding: 0.25rem 0.75rem; border: 1px solid currentColor; border-radius: 1rem; background: transparent; color: inherit; font: inherit; font-size: 0.8rem; cursor: pointer; }
76
90
  @media (prefers-reduced-motion: reduce) {
77
91
  .drawing, .canvas { filter: none !important; transition: opacity var(--uchusen-fade, 400ms) ease; }
@@ -94,29 +108,13 @@ function adopt(root, css, holder) {
94
108
  const rocketSheet = { sheet: null };
95
109
  const lineupSheet = { sheet: null };
96
110
  const media = (query) => typeof matchMedia === "function" && matchMedia(query).matches;
97
- let webgl = null;
98
- function webglWorks() {
99
- if (webgl !== null)
100
- return webgl;
101
- try {
102
- const probe = document.createElement("canvas");
103
- const gl = probe.getContext("webgl2") ?? probe.getContext("webgl");
104
- webgl = Boolean(gl);
105
- gl?.getExtension("WEBGL_lose_context")?.loseContext();
106
- }
107
- catch {
108
- webgl = false;
109
- }
110
- return webgl;
111
- }
111
+ /** Why the 3D view is not wanted, from what is known without making a WebGL context. Whether WebGL works is asked once, when the first element comes on screen. */
112
112
  function whyNot(mode) {
113
113
  if (mode === "drawing")
114
114
  return "off";
115
115
  const connection = navigator.connection;
116
116
  if (connection?.saveData || media("(prefers-reduced-data: reduce)"))
117
117
  return "reduced-data";
118
- if (!webglWorks())
119
- return "no-webgl";
120
118
  return "";
121
119
  }
122
120
  /**
@@ -134,7 +132,7 @@ function whyNot(mode) {
134
132
  * ```
135
133
  */
136
134
  export class UchusenRocket extends HTMLBase {
137
- static observedAttributes = ["rocket", "variant", "show", "look", "view", "build", "mode", "assets", "spin", "way", "label", "engines", "lights", "thrusters", "phase", "motion", "labels", "inspector", "beside", "captions", "scale-note", "off-screen", "scale", "progress", "float", "names", "leave", "docked", "edges", "world", "world-turn", "form"];
135
+ static observedAttributes = ["rocket", "variant", "show", "look", "view", "build", "mode", "assets", "spin", "way", "label", "engines", "lights", "thrusters", "phase", "motion", "labels", "inspector", "beside", "captions", "scale-note", "off-screen", "scale", "progress", "float", "names", "leave", "docked", "edges", "world", "world-turn", "form", "sun", "flags"];
138
136
  descriptionValue = null;
139
137
  picture = null;
140
138
  holder = null;
@@ -152,8 +150,12 @@ export class UchusenRocket extends HTMLBase {
152
150
  builtNow = null;
153
151
  queued = false;
154
152
  stateNow = "picture";
153
+ /** What the flag in the corner is for: a country's code, "" for none, "off", or "-" before the first look. */
154
+ flagKey = "-";
155
155
  shownAt = 0;
156
156
  starting = false;
157
+ /** Whether this element has asked the page's WebGL answer since it started watching. */
158
+ glAsked = false;
157
159
  key = "";
158
160
  driven = null;
159
161
  placeFrame = 0;
@@ -334,7 +336,9 @@ export class UchusenRocket extends HTMLBase {
334
336
  attributeChangedCallback(name, before, after) {
335
337
  if (before === after)
336
338
  return;
337
- if (name === "scale") {
339
+ if (name === "flags")
340
+ this.syncFlag();
341
+ else if (name === "scale") {
338
342
  this.viewer?.setScaleMode(after === "true");
339
343
  // `scale="true"` is the true-scale view. A number is a Kaminari size, which rebuilds the craft.
340
344
  if (after !== "true" && after !== "false")
@@ -359,6 +363,8 @@ export class UchusenRocket extends HTMLBase {
359
363
  this.viewer?.setFloating(after !== null);
360
364
  else if (name === "edges")
361
365
  this.viewer?.setEdges(after === "open");
366
+ else if (name === "sun")
367
+ this.viewer?.setSun(parseSun(after));
362
368
  else if (name === "world")
363
369
  this.viewer?.setOrbit(after === "earth");
364
370
  else if (name === "world-turn")
@@ -677,6 +683,7 @@ export class UchusenRocket extends HTMLBase {
677
683
  holder.className = "stage";
678
684
  picture.append(drawing, holder);
679
685
  root.appendChild(picture);
686
+ this.flagKey = "-";
680
687
  this.picture = picture;
681
688
  this.drawing = drawing;
682
689
  this.holder = holder;
@@ -747,6 +754,40 @@ export class UchusenRocket extends HTMLBase {
747
754
  else
748
755
  delete this.dataset.docked;
749
756
  this.dataset.custom = String(this.builtNow.report.custom);
757
+ this.syncFlag();
758
+ }
759
+ /** `flags`: the rocket's country's flag in the picture's corner, from hata (an optional peer) when the page has it. `data-flag` is the code shown, or empty once it is settled that none is; it is absent while one is loading or the attribute is off. */
760
+ syncFlag() {
761
+ const picture = this.picture;
762
+ if (!picture)
763
+ return;
764
+ const on = this.hasAttribute("flags") && this.getAttribute("flags") !== "off";
765
+ const code = on && this.builtNow ? (ROCKETS[this.descriptionNow().rocket]?.country.value ?? "") : "";
766
+ const key = on ? code : "off";
767
+ if (key === this.flagKey)
768
+ return;
769
+ this.flagKey = key;
770
+ picture.querySelector(".flag")?.remove();
771
+ if (!on)
772
+ return void delete this.dataset.flag;
773
+ if (!code)
774
+ return void (this.dataset.flag = "");
775
+ delete this.dataset.flag;
776
+ void flagSvg(code).then((svg) => {
777
+ if (this.flagKey !== key || this.picture !== picture)
778
+ return;
779
+ if (svg) {
780
+ const badge = document.createElement("span");
781
+ badge.className = "flag";
782
+ badge.setAttribute("part", "flag");
783
+ badge.setAttribute("role", "img");
784
+ badge.setAttribute("aria-label", regionName(code, this.closest("[lang]")?.getAttribute("lang")));
785
+ badge.innerHTML = svg;
786
+ badge.firstElementChild?.setAttribute("aria-hidden", "true");
787
+ picture.append(badge);
788
+ }
789
+ this.dataset.flag = svg ? code : "";
790
+ });
750
791
  }
751
792
  async applyRocket() {
752
793
  this.mark();
@@ -900,7 +941,8 @@ export class UchusenRocket extends HTMLBase {
900
941
  }
901
942
  startWatching() {
902
943
  this.watch?.disconnect();
903
- const reason = whyNot(this.getAttribute("mode") ?? "auto");
944
+ let reason = whyNot(this.getAttribute("mode") ?? "auto");
945
+ this.glAsked = false;
904
946
  if (reason) {
905
947
  this.setState(reason === "off" ? "picture" : "fallback", reason);
906
948
  if (reason !== "off")
@@ -916,6 +958,16 @@ export class UchusenRocket extends HTMLBase {
916
958
  this.kickFlight();
917
959
  if (reason)
918
960
  return;
961
+ if (seen.isIntersecting && !this.glAsked) {
962
+ // The probe makes a context, which software drawing takes seconds over: it waits until a view is about to draw, and is made once for the whole page.
963
+ this.glAsked = true;
964
+ if (!glProbe().works) {
965
+ reason = "no-webgl";
966
+ this.setState("fallback", reason);
967
+ this.emit("uchusen-fallback", { reason });
968
+ return;
969
+ }
970
+ }
919
971
  if (seen.isIntersecting) {
920
972
  this.shownAt ||= performance.now();
921
973
  window.clearTimeout(this.releaseTimer);
@@ -1003,6 +1055,7 @@ export class UchusenRocket extends HTMLBase {
1003
1055
  made.drive(this.driven);
1004
1056
  made.setFloating(this.hasAttribute("float"));
1005
1057
  made.setEdges(this.getAttribute("edges") === "open");
1058
+ made.setSun(parseSun(this.getAttribute("sun")));
1006
1059
  made.setNameLabels(this.getAttribute("names") !== "off");
1007
1060
  made.setVisible(true);
1008
1061
  const { look, show, viewName, angle } = this.settings(this.builtNow ?? built);
@@ -1195,3 +1248,4 @@ export function defineUchusenRocket() {
1195
1248
  }
1196
1249
  defineUchusenRocket();
1197
1250
  export { ROCKETS, UchusenFlight };
1251
+ export { setFlagLoader } from "./element/flag.js";
package/dist/index.d.ts CHANGED
@@ -4,6 +4,8 @@ export { blank, figureWords } from "./data/types.ts";
4
4
  export type { Disagreement, Figure, Variant, VariantFacts, VariantStatus } from "./data/types.ts";
5
5
  export { MAKERS } from "./data/makers.ts";
6
6
  export type { MakerId } from "./data/makers.ts";
7
+ export { COUNTRY_CODES, countryOf, MAKER_COUNTRIES } from "./data/countries.ts";
8
+ export type { CountryCode } from "./data/countries.ts";
7
9
  export { FLIGHT_LOG, flightLogOf } from "./data/flight-log.ts";
8
10
  export type { LoggedFlight } from "./data/flight-log.ts";
9
11
  export { moved, movingGroupsOf } from "./model/moves.ts";
@@ -57,6 +59,7 @@ export { beaconLamp, dutyOf, lampFactor, lightMode, lightPool, sternLamp, workLa
57
59
  export type { Motion, MotionKind, MotionSpeed } from "./model/fleet/motion.ts";
58
60
  export { bellsOf, enginesWanted, FLAME_SETTLE_MS, pickBells, plumeLength, ringsOf } from "./engines.ts";
59
61
  export type { Bell } from "./engines.ts";
62
+ export { parseSun } from "./sun.ts";
60
63
  export { OPTION_ATTRIBUTES, attributesFromOptions, optionsFromAttributes, rocketFromParams, rocketToParams } from "./attrs.ts";
61
64
  export type { RocketOptions } from "./attrs.ts";
62
65
  export { assetPathOf, CDN, EMBED_KINDS, SITE, THREE_VERSION, embedCode, embedNote, paintedCredit } from "./embed.ts";
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@
2
2
  export { PLANNED_ROCKETS, ROCKETS, partIdOf, SLOT_NAMES, variantOf } from "./rockets.js";
3
3
  export { blank, figureWords } from "./data/types.js";
4
4
  export { MAKERS } from "./data/makers.js";
5
+ export { COUNTRY_CODES, countryOf, MAKER_COUNTRIES } from "./data/countries.js";
5
6
  export { FLIGHT_LOG, flightLogOf } from "./data/flight-log.js";
6
7
  export { moved, movingGroupsOf } from "./model/moves.js";
7
8
  export { PROPELLANTS, propellantOf } from "./data/propellants.js";
@@ -34,6 +35,7 @@ export { beamLight } from "./draw/beamLight.js";
34
35
  export { angleAt, MOTIONS, motionNote, motionSpeed, motionsOf, motionWanted, slideAt } from "./model/fleet/motion.js";
35
36
  export { beaconLamp, dutyOf, lampFactor, lightMode, lightPool, sternLamp, workLamp } from "./model/fleet/lights.js";
36
37
  export { bellsOf, enginesWanted, FLAME_SETTLE_MS, pickBells, plumeLength, ringsOf } from "./engines.js";
38
+ export { parseSun } from "./sun.js";
37
39
  export { OPTION_ATTRIBUTES, attributesFromOptions, optionsFromAttributes, rocketFromParams, rocketToParams } from "./attrs.js";
38
40
  export { assetPathOf, CDN, EMBED_KINDS, SITE, THREE_VERSION, embedCode, embedNote, paintedCredit } from "./embed.js";
39
41
  export { COMPANIES, COMPANY_KINDS, companyOf } from "./model/fleet/company.js";
package/dist/inspace.js CHANGED
@@ -7,6 +7,7 @@ const SPACE = {
7
7
  sls: ["upper-stage", "service-module", "capsule"],
8
8
  "falcon-9": ["second-stage", "fairing", "trunk", "capsule"],
9
9
  dragon: ["trunk", "capsule"],
10
+ "falcon-heavy": ["car", "starman"],
10
11
  };
11
12
  /**
12
13
  * The slots to draw when the craft is in space, or null when the picture stays the whole variant.
@@ -1,6 +1,6 @@
1
1
  // Every part there is, with its record and how to make its pieces. A new rocket adds its rows here and nothing else changes.
2
2
  import { DRAGON_VARIANTS } from "../data/dragon.js";
3
- import { FALCON_9_VARIANTS } from "../data/falcon.js";
3
+ import { FALCON_9_VARIANTS, FALCON_HEAVY_VARIANTS } from "../data/falcon.js";
4
4
  import { STARSHIP_VARIANTS } from "../data/starship.js";
5
5
  import { capsule, centreCore, DRAGON_DRAWS, falconFairing, falconFirstStage, FALCON_DRAWS, falconSecondStage, grasshopper, merlin, sideBooster, trunk } from "./falcon.js";
6
6
  import { FALCON_1_VARIANTS } from "../data/falcon1.js";
@@ -37,6 +37,8 @@ import { HUBBLE_VARIANTS } from "../data/hubble.js";
37
37
  import { HUBBLE_BUILDS } from "./hubble.js";
38
38
  import { TIANGONG_VARIANTS } from "../data/tiangong.js";
39
39
  import { TIANGONG_BUILDS } from "./tiangong.js";
40
+ import { ROADSTER_VARIANTS } from "../data/roadster.js";
41
+ import { ROADSTER_BUILDS } from "./roadster.js";
40
42
  import { DRONE_VARIANTS } from "../data/drone.js";
41
43
  import { DRONE_BUILDS } from "./drone.js";
42
44
  import { DRAGON_INSIDE_BUILDS } from "./dragoninside.js";
@@ -145,11 +147,11 @@ const FH_FROM = "This package's code for the two line looks, proportions taken f
145
147
  const FH_SIDE_HEIGHT = 45.6;
146
148
  const FH_OFFSET = 4.25;
147
149
  const figure = (value, source) => ({ value, source });
148
- entries.push({ record: { id: "falcon-heavy-falcon-heavy-centre", kind: "first-stage", name: "Falcon Heavy centre core, with its upper stage and fairing", rocket: "falcon-heavy", trueTo: ["falcon-heavy"], height: 70, diameter: 5.2, sourced: { height: figure(70, "wikipedia-falcon-heavy"), diameter: { ...blankFigure, note: "3.7 m the core; 5.2 m the fairing." } }, joints: {}, painted: PAINTED["falcon-heavy-falcon-heavy-centre"], drawnFrom: FH_FROM }, pieces: () => centreCore(70, 1.85) }, { record: { id: "falcon-heavy-falcon-heavy-side-left", kind: "first-stage", name: "Falcon Heavy side booster, left", rocket: "falcon-heavy", trueTo: ["falcon-heavy"], height: FH_SIDE_HEIGHT, diameter: 3.7, strapOn: { x: -FH_OFFSET, z: 0 }, sourced: { height: blankFigure, diameter: figure(3.7, "wikipedia-falcon-heavy") }, joints: {}, painted: PAINTED["falcon-heavy-falcon-heavy-side-left"], drawnFrom: FH_FROM }, pieces: () => sideBooster(FH_SIDE_HEIGHT, 1.85, -1) }, { record: { id: "falcon-heavy-falcon-heavy-side-right", kind: "first-stage", name: "Falcon Heavy side booster, right", rocket: "falcon-heavy", trueTo: ["falcon-heavy"], height: FH_SIDE_HEIGHT, diameter: 3.7, strapOn: { x: FH_OFFSET, z: 0 }, sourced: { height: blankFigure, diameter: figure(3.7, "wikipedia-falcon-heavy") }, joints: {}, painted: PAINTED["falcon-heavy-falcon-heavy-side-right"], drawnFrom: FH_FROM }, pieces: () => sideBooster(FH_SIDE_HEIGHT, 1.85, 1) }, { record: { id: "grasshopper-grasshopper-vehicle", kind: "test-vehicle", name: "Grasshopper, test vehicle", rocket: "grasshopper", trueTo: ["grasshopper"], height: 32, diameter: 3.2, sourced: { height: figure(32, "wikipedia-grasshopper"), diameter: blankFigure }, joints: {}, painted: PAINTED["grasshopper-grasshopper-vehicle"], drawnFrom: "This package's code for the two line looks, proportions taken from Echo_3's low-poly model at the sourced height; that model for the painted look." }, pieces: () => grasshopper(32) });
150
+ entries.push({ record: { id: "falcon-heavy-falcon-heavy-centre", kind: "first-stage", name: "Falcon Heavy centre core, with its upper stage and fairing", rocket: "falcon-heavy", trueTo: ["falcon-heavy"], height: 70, diameter: 5.2, sourced: { height: figure(70, "wikipedia-falcon-heavy"), diameter: { ...blankFigure, note: "3.7 m the core; 5.2 m the fairing." } }, joints: {}, painted: PAINTED["falcon-heavy-falcon-heavy-centre"], drawnFrom: FH_FROM }, pieces: () => centreCore(70, 1.85) }, { record: { id: "falcon-heavy-falcon-heavy-side-left", kind: "first-stage", name: "Falcon Heavy side booster, left", rocket: "falcon-heavy", trueTo: ["falcon-heavy", "test-flight"], height: FH_SIDE_HEIGHT, diameter: 3.7, strapOn: { x: -FH_OFFSET, z: 0 }, sourced: { height: blankFigure, diameter: figure(3.7, "wikipedia-falcon-heavy") }, joints: {}, painted: PAINTED["falcon-heavy-falcon-heavy-side-left"], drawnFrom: FH_FROM }, pieces: () => sideBooster(FH_SIDE_HEIGHT, 1.85, -1) }, { record: { id: "falcon-heavy-falcon-heavy-side-right", kind: "first-stage", name: "Falcon Heavy side booster, right", rocket: "falcon-heavy", trueTo: ["falcon-heavy", "test-flight"], height: FH_SIDE_HEIGHT, diameter: 3.7, strapOn: { x: FH_OFFSET, z: 0 }, sourced: { height: blankFigure, diameter: figure(3.7, "wikipedia-falcon-heavy") }, joints: {}, painted: PAINTED["falcon-heavy-falcon-heavy-side-right"], drawnFrom: FH_FROM }, pieces: () => sideBooster(FH_SIDE_HEIGHT, 1.85, 1) }, { record: { id: "grasshopper-grasshopper-vehicle", kind: "test-vehicle", name: "Grasshopper, test vehicle", rocket: "grasshopper", trueTo: ["grasshopper"], height: 32, diameter: 3.2, sourced: { height: figure(32, "wikipedia-grasshopper"), diameter: blankFigure }, joints: {}, painted: PAINTED["grasshopper-grasshopper-vehicle"], drawnFrom: "This package's code for the two line looks, proportions taken from Echo_3's low-poly model at the sourced height; that model for the painted look." }, pieces: () => grasshopper(32) });
149
151
  // ---------------------------------------------------------------------------------------------------------------------------------
150
152
  // Rockets drawn wholly by this package's code, from their tables
151
- const stackFacts = [...DRONE_VARIANTS, ...HUBBLE_VARIANTS, ...ISS_VARIANTS, ...TIANGONG_VARIANTS, ...JUPITER_C_VARIANTS, ...MERCURY_VARIANTS, ...GEMINI_VARIANTS, ...APOLLO_SOYUZ_VARIANTS, ...LUNAR_MODULE_VARIANTS, ...STARBASE_VARIANTS, ...DRAGON_VARIANTS, ...TOWER_VARIANTS, ...DRONESHIP_VARIANTS, ...LANDING_ZONE_VARIANTS, ...SOYUZ_VARIANTS, ...NEW_GLENN_VARIANTS, ...ELECTRON_VARIANTS, ...SLS_VARIANTS, ...FALCON_1_VARIANTS, ...SATURN_V_VARIANTS, ...SHUTTLE_VARIANTS, ...SHIP_02_VARIANTS];
152
- const stacks = [...DRONE_BUILDS, ...HUBBLE_BUILDS, ...ISS_BUILDS, ...TIANGONG_BUILDS, ...JUPITER_C_BUILDS, ...MERCURY_BUILDS, ...GEMINI_BUILDS, ...APOLLO_SOYUZ_BUILDS, ...LUNAR_MODULE_BUILDS, ...STARBASE_BUILDS, ...DRAGON_INSIDE_BUILDS, ...TOWER_BUILDS, ...DRONESHIP_BUILDS, ...LANDING_ZONE_BUILDS, ...SOYUZ_BUILDS, ...NEW_GLENN_BUILDS, ...ELECTRON_BUILDS, ...SLS_BUILDS, ...FALCON_1_BUILDS, ...SATURN_V_BUILDS, ...SHUTTLE_BUILDS, ...SHIP_02_BUILDS];
153
+ const stackFacts = [...ROADSTER_VARIANTS, ...FALCON_HEAVY_VARIANTS, ...DRONE_VARIANTS, ...HUBBLE_VARIANTS, ...ISS_VARIANTS, ...TIANGONG_VARIANTS, ...JUPITER_C_VARIANTS, ...MERCURY_VARIANTS, ...GEMINI_VARIANTS, ...APOLLO_SOYUZ_VARIANTS, ...LUNAR_MODULE_VARIANTS, ...STARBASE_VARIANTS, ...DRAGON_VARIANTS, ...TOWER_VARIANTS, ...DRONESHIP_VARIANTS, ...LANDING_ZONE_VARIANTS, ...SOYUZ_VARIANTS, ...NEW_GLENN_VARIANTS, ...ELECTRON_VARIANTS, ...SLS_VARIANTS, ...FALCON_1_VARIANTS, ...SATURN_V_VARIANTS, ...SHUTTLE_VARIANTS, ...SHIP_02_VARIANTS];
154
+ const stacks = [...ROADSTER_BUILDS, ...DRONE_BUILDS, ...HUBBLE_BUILDS, ...ISS_BUILDS, ...TIANGONG_BUILDS, ...JUPITER_C_BUILDS, ...MERCURY_BUILDS, ...GEMINI_BUILDS, ...APOLLO_SOYUZ_BUILDS, ...LUNAR_MODULE_BUILDS, ...STARBASE_BUILDS, ...DRAGON_INSIDE_BUILDS, ...TOWER_BUILDS, ...DRONESHIP_BUILDS, ...LANDING_ZONE_BUILDS, ...SOYUZ_BUILDS, ...NEW_GLENN_BUILDS, ...ELECTRON_BUILDS, ...SLS_BUILDS, ...FALCON_1_BUILDS, ...SATURN_V_BUILDS, ...SHUTTLE_BUILDS, ...SHIP_02_BUILDS];
153
155
  for (const build of stacks)
154
156
  for (const part of build.parts) {
155
157
  const { height, diameter } = facts(stackFacts, build.variant, part.slot);
@@ -0,0 +1,31 @@
1
+ import type { Piece, Vec3 } from "./pieces.ts";
2
+ import type { StackBuild } from "./stacks.ts";
3
+ /** One car as measured from its model, in metres: nose towards +Z, on the ground at y = 0. */
4
+ export interface CarDraw {
5
+ /** Nose to tail, and across. */
6
+ length: number;
7
+ width: number;
8
+ /** The top of the windscreen. */
9
+ height: number;
10
+ /** The wheels: their middles along the car, their radius and how far each stands from the middle. */
11
+ front: number;
12
+ rear: number;
13
+ wheel: number;
14
+ track: number;
15
+ /** The top of the doors. */
16
+ belt: number;
17
+ /** Where the windscreen meets the body, along the car. */
18
+ screen: number;
19
+ /** The driver's hip, in the left seat: across, up and along. */
20
+ seat: Vec3;
21
+ /** The middle of the steering wheel. */
22
+ wheelAt: Vec3;
23
+ }
24
+ export declare const ROADSTER_DRAWS: Readonly<Record<string, CarDraw>>;
25
+ /** The car: its body seen from the side and pressed to its width, its cockpit open, the windscreen and the four wheels. */
26
+ export declare function carPieces(d: CarDraw): Piece[];
27
+ /** Starman in the driver's seat: legs forward to the pedals, his back against the seat, his right hand on the wheel, his left arm along the top of the door. */
28
+ export declare function starmanPieces(d: CarDraw): Piece[];
29
+ /** Where the payload stands in Falcon Heavy: 0.8 m above where its fairing begins (the fairing's sourced 13.1 m under the 70 m top), clear of the fairing's walls. The 0.8 m is a placing, not a figure. */
30
+ export declare const FALCON_HEAVY_PAYLOAD_Y: number;
31
+ export declare const ROADSTER_BUILDS: readonly StackBuild[];
@@ -0,0 +1,112 @@
1
+ // The Roadster in plain shapes for the line looks, and Starman in every look. The car's length, wheelbase, wheels and seat are measured from each licensed model at its sourced scale; the painted look is the
2
+ // model. Starman is this package's own: a seated figure in a white suit with dark trim, a white helmet with a dark visor, his right hand on the wheel and his left arm on the door. He shares the car's origin
3
+ // (the middle of the car on the ground), so he sits in its seat in any build that places the two together.
4
+ import { ROADSTER_FLIGHT } from "../data/roadster.js";
5
+ import { blank } from "../data/types.js";
6
+ import { centreCore } from "./falcon.js";
7
+ import { strut } from "./pieces.js";
8
+ export const ROADSTER_DRAWS = {
9
+ "first-generation": { length: 3.946, width: 1.85, height: 1.03, front: 1.114, rear: -1.258, wheel: 0.3, track: 1.46, belt: 0.8, screen: 0.95, seat: [0.29, 0.34, -0.22], wheelAt: [0.29, 0.62, 0.37] },
10
+ "second-generation": { length: 4.47, width: 1.83, height: 1.14, front: 1.417, rear: -1.408, wheel: 0.31, track: 1.52, belt: 0.84, screen: 0.75, seat: [0.32, 0.22, -0.18], wheelAt: [0.32, 0.58, 0.3] },
11
+ "second-generation-2": { length: 4.28, width: 1.86, height: 1.07, front: 1.398, rear: -1.429, wheel: 0.31, track: 1.58, belt: 0.8, screen: 0.7, seat: [0.32, 0.17, -0.2], wheelAt: [0.32, 0.55, 0.28] },
12
+ };
13
+ const side = (d) => {
14
+ const nose = d.length / 2;
15
+ return [
16
+ [-nose, 0.22],
17
+ [-d.length * 0.16, 0.14],
18
+ [nose * 0.8, 0.14],
19
+ [nose, 0.32],
20
+ [nose * 0.92, 0.6],
21
+ [d.screen, d.belt - 0.02],
22
+ [-nose * 0.9, d.belt + 0.04],
23
+ [-nose, 0.62],
24
+ ];
25
+ };
26
+ const octagon = (r) => Array.from({ length: 12 }, (_, i) => [Math.cos((i * Math.PI) / 6) * r, Math.sin((i * Math.PI) / 6) * r]);
27
+ /** The car: its body seen from the side and pressed to its width, its cockpit open, the windscreen and the four wheels. */
28
+ export function carPieces(d) {
29
+ const half = d.width / 2;
30
+ const pieces = [{ kind: "plate", material: "red", frame: { o: [0, 0, 0], u: [0, 0, 1], v: [0, 1, 0], w: [1, 0, 0] }, outline: side(d), thickness: d.width - 0.06 }];
31
+ // the open cockpit, dark, sunk into the top of the body
32
+ pieces.push({ kind: "plate", material: "dark", frame: { o: [0, d.belt + 0.005, 0], u: [1, 0, 0], v: [0, 0, 1], w: [0, 1, 0] }, outline: [[-half + 0.2, -0.75], [half - 0.2, -0.75], [half - 0.2, d.screen - 0.05], [-half + 0.2, d.screen - 0.05]], thickness: 0.01, plain: true });
33
+ // the windscreen, leaning back from the scuttle
34
+ const lean = 0.55;
35
+ const rise = d.height - d.belt;
36
+ const up = [0, Math.cos(lean), -Math.sin(lean)];
37
+ pieces.push({ kind: "plate", material: "glass", frame: { o: [0, d.belt, d.screen], u: [1, 0, 0], v: up, w: [0, Math.sin(lean), Math.cos(lean)] }, outline: [[-half + 0.18, 0], [half - 0.18, 0], [half - 0.26, rise / Math.cos(lean)], [-half + 0.26, rise / Math.cos(lean)]], thickness: 0.012 });
38
+ for (const z of [d.front, d.rear])
39
+ for (const s of [1, -1]) {
40
+ const x = (s * d.track) / 2;
41
+ pieces.push({ kind: "plate", material: "black", frame: { o: [x, d.wheel, z], u: [0, 0, 1], v: [0, 1, 0], w: [1, 0, 0] }, outline: octagon(d.wheel), thickness: 0.24 });
42
+ pieces.push({ kind: "plate", material: "fin-dark", frame: { o: [x + s * 0.121, d.wheel, z], u: [0, 0, 1], v: [0, 1, 0], w: [1, 0, 0] }, outline: octagon(d.wheel * 0.62), thickness: 0.01, plain: true });
43
+ }
44
+ return pieces;
45
+ }
46
+ const at = (p, dx, dy, dz) => [p[0] + dx, p[1] + dy, p[2] + dz];
47
+ /** Starman in the driver's seat: legs forward to the pedals, his back against the seat, his right hand on the wheel, his left arm along the top of the door. */
48
+ export function starmanPieces(d) {
49
+ const hip = d.seat;
50
+ const shoulder = at(hip, 0, 0.52, -0.1);
51
+ const knee = at(hip, 0, 0.12, 0.44);
52
+ // the feet on the floor, which is never under the ground
53
+ const foot = [hip[0], Math.max(0.12, hip[1] - 0.18), hip[2] + 0.86];
54
+ const right = at(shoulder, -0.19, 0, 0);
55
+ const left = at(shoulder, 0.19, 0, 0);
56
+ const door = [Math.min(d.width / 2 - 0.08, hip[0] + 0.42), d.belt + 0.05, hip[2] + 0.12];
57
+ const pieces = [
58
+ // the torso, leaning back with the seat
59
+ { kind: "lathe", material: "white", segs: 16, at: [hip[0], hip[2]], lean: [0, -0.19], profile: [[0.14, hip[1]], [0.18, hip[1] + 0.22], [0.19, shoulder[1] - 0.06], [0.1, shoulder[1] + 0.04]] },
60
+ // the helmet, white, round, with its dark visor facing the road
61
+ { kind: "lathe", material: "white", segs: 20, at: [shoulder[0], shoulder[2] - 0.02], profile: [[0.07, shoulder[1] + 0.04], [0.15, shoulder[1] + 0.13], [0.155, shoulder[1] + 0.22], [0.12, shoulder[1] + 0.31], [0.001, shoulder[1] + 0.35]] },
62
+ { kind: "plate", material: "glass", frame: { o: [shoulder[0], shoulder[1] + 0.2, shoulder[2] + 0.12], u: [1, 0, 0], v: [0, 1, 0], w: [0, 0, 1] }, outline: [[-0.1, -0.06], [0.1, -0.06], [0.11, 0.05], [-0.11, 0.05]], thickness: 0.04, plain: true },
63
+ // a dark ring where the helmet meets the suit
64
+ { kind: "lathe", material: "black", segs: 16, at: [shoulder[0], shoulder[2] - 0.02], profile: [[0.12, shoulder[1] + 0.01], [0.12, shoulder[1] + 0.06]], only: "mesh" },
65
+ ];
66
+ for (const s of [-1, 1]) {
67
+ const h = at(hip, s * 0.1, 0, 0);
68
+ const k = at(knee, s * 0.1, 0, 0);
69
+ const f = at(foot, s * 0.1, 0, 0);
70
+ pieces.push(strut("white", h, k, 0.15, 0.15), strut("white", k, f, 0.13, 0.13), strut("black", f, at(f, 0, 0.02, 0.1), 0.12, 0.14));
71
+ }
72
+ // his right hand on the wheel's rim, his left arm on the door top
73
+ const hand = at(d.wheelAt, -0.12, 0.04, -0.03);
74
+ const elbowR = at(right, -0.02, -0.22, 0.2);
75
+ const elbowL = at(left, 0.12, -0.2, 0.05);
76
+ pieces.push(strut("white", right, elbowR, 0.11, 0.11), strut("white", elbowR, hand, 0.1, 0.1), strut("black", hand, at(hand, 0, 0, 0.06), 0.09, 0.09));
77
+ pieces.push(strut("white", left, elbowL, 0.11, 0.11), strut("white", elbowL, door, 0.1, 0.1), strut("black", door, at(door, 0, 0, 0.07), 0.09, 0.09));
78
+ return pieces;
79
+ }
80
+ const FILES = {
81
+ "first-generation": { file: "roadster-first-generation-car.glb", credit: "the86guy-roadster" },
82
+ "second-generation": { file: "roadster-second-generation-car.glb", credit: "metarex4d-roadster" },
83
+ "second-generation-2": { file: "roadster-second-generation-2-car.glb", credit: "gmmichaelonearth-roadster", also: ["minchou19-wheels"] },
84
+ };
85
+ const STARMAN_NAME = "Starman, a mannequin in a pressure suit";
86
+ const carPart = (variant, place) => {
87
+ const d = ROADSTER_DRAWS[variant];
88
+ return { slot: "car", kind: "payload", name: variant === "first-generation" ? "The Roadster, first generation" : "The Roadster, second generation", height: d.height, diameter: d.width, sourcedDiameter: blank(`Its width as drawn: the model at its sourced scale is ${d.width} m across.`), painted: FILES[variant], ...(place ? { place } : {}), pieces: () => carPieces(d) };
89
+ };
90
+ const starmanPart = (variant, place) => {
91
+ const d = ROADSTER_DRAWS[variant];
92
+ return { slot: "starman", kind: "payload", name: STARMAN_NAME, height: d.seat[1] + 0.87, diameter: 0.6, sourcedDiameter: blank("No source read here gives Starman's size."), place, pieces: () => starmanPieces(d) };
93
+ };
94
+ const FROM = (variant) => variant === "first-generation"
95
+ ? "The 86 guy's model for the painted look, with its badges and roof left off and its bonnet made bare carbon; this package's plain shapes for the line looks, measured from it at the sourced length. Starman is this package's own in every look."
96
+ : "Its author's model for the painted look, with its badges left off, painted the first generation's red; this package's plain shapes for the line looks, measured from it at the sourced wheelbase. Starman is this package's own in every look.";
97
+ /** Where the payload stands in Falcon Heavy: 0.8 m above where its fairing begins (the fairing's sourced 13.1 m under the 70 m top), clear of the fairing's walls. The 0.8 m is a placing, not a figure. */
98
+ export const FALCON_HEAVY_PAYLOAD_Y = 70 - 13.1 + 0.8;
99
+ export const ROADSTER_BUILDS = [
100
+ ...Object.keys(ROADSTER_DRAWS).map((variant) => ({ rocket: "roadster", variant, drawnFrom: FROM(variant), parts: [carPart(variant), starmanPart(variant, { x: 0, z: 0 })] })),
101
+ {
102
+ rocket: "falcon-heavy",
103
+ variant: ROADSTER_FLIGHT.id,
104
+ drawnFrom: "Antonio_Adams's low-poly Falcon Heavy and the 86 guy's Roadster for the painted look, this package's plain shapes for the line looks. Starman is this package's own in every look.",
105
+ parts: [
106
+ // the same vehicle as Falcon Heavy's own, on its first flight: its centre core is its own part here, so the test flight is reached from its own foot
107
+ { slot: "centre", kind: "first-stage", name: "Falcon Heavy centre core, with its upper stage and fairing", height: 70, diameter: 5.2, sourcedDiameter: blank("3.7 m the core; 5.2 m the fairing."), painted: { file: "falcon-heavy-falcon-heavy-centre.glb", credit: "antonio-falcon-heavy" }, pieces: () => centreCore(70, 1.85) },
108
+ carPart("first-generation", { x: 0, z: 0, y: FALCON_HEAVY_PAYLOAD_Y }),
109
+ starmanPart("first-generation", { x: 0, z: 0, y: FALCON_HEAVY_PAYLOAD_Y }),
110
+ ],
111
+ },
112
+ ];
package/dist/parts.d.ts CHANGED
@@ -11,7 +11,7 @@ import type { Figure } from "./data/types.ts";
11
11
  * console.log(kind);
12
12
  * ```
13
13
  */
14
- export type PartKind = "booster" | "first-stage" | "ship" | "second-stage" | "fairing" | "capsule" | "trunk" | "engine" | "fin" | "flap" | "ring" | "test-vehicle" | "third-stage" | "fourth-stage" | "tail" | "adapter" | "service-module" | "tower" | "tank" | "solid-booster" | "liquid-booster" | "orbiter" | "deck" | "mount" | "arms" | "site" | "spacecraft" | "module" | "array" | "antenna";
14
+ export type PartKind = "booster" | "first-stage" | "ship" | "second-stage" | "fairing" | "capsule" | "trunk" | "engine" | "fin" | "flap" | "ring" | "test-vehicle" | "third-stage" | "fourth-stage" | "tail" | "adapter" | "service-module" | "tower" | "tank" | "solid-booster" | "liquid-booster" | "orbiter" | "deck" | "mount" | "arms" | "site" | "spacecraft" | "module" | "array" | "antenna" | "payload";
15
15
  /**
16
16
  * Where a part mates. Every part's origin is the middle of its base on its axis, at its lowest point; Y is up and the windward side faces +Z, in metres.
17
17
  *