@johnmorrisdotca/uchusen-rockets 0.4.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 (75) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/NOTICE.md +2 -0
  3. package/README.md +78 -3
  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/build/glb.js +15 -7
  48. package/dist/build/index.d.ts +5 -0
  49. package/dist/build/index.js +16 -3
  50. package/dist/data/flights.d.ts +7 -0
  51. package/dist/data/flights.js +160 -13
  52. package/dist/data/propellants.d.ts +59 -0
  53. package/dist/data/propellants.js +43 -0
  54. package/dist/data/sources.d.ts +70 -0
  55. package/dist/data/sources.js +9 -0
  56. package/dist/draw/plume.d.ts +10 -1
  57. package/dist/draw/plume.js +15 -5
  58. package/dist/element/plumes.d.ts +7 -8
  59. package/dist/element/plumes.js +36 -21
  60. package/dist/element/scene.js +30 -5
  61. package/dist/element.js +3 -2
  62. package/dist/flight.d.ts +2 -0
  63. package/dist/flight.js +8 -1
  64. package/dist/flightbar.js +1 -1
  65. package/dist/index.d.ts +3 -0
  66. package/dist/index.js +2 -0
  67. package/dist/model/falcon.js +2 -2
  68. package/dist/model/moves.d.ts +36 -0
  69. package/dist/model/moves.js +70 -0
  70. package/dist/model/pieces.d.ts +31 -0
  71. package/dist/model/pieces.js +51 -9
  72. package/dist/model/starship.d.ts +6 -3
  73. package/dist/model/starship.js +12 -7
  74. package/dist/react.d.ts +2 -2
  75. package/package.json +18 -3
@@ -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.js CHANGED
@@ -503,11 +503,12 @@ export class UchusenRocket extends HTMLBase {
503
503
  const from = this.driven ? turnedBy(angle, this.driven.yaw, this.driven.tilt) : (this.viewer?.currentAngle() ?? angle);
504
504
  const label = this.getAttribute("label") ?? this.builtNow.report.label;
505
505
  const lit = flying ? (media("(prefers-reduced-motion: reduce)") ? false : flying.burns) : this.engineLights();
506
- const 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 ?? {})}`;
507
508
  let node = this.drawing.firstElementChild;
508
509
  if (key !== this.drawn.key || !node) {
509
510
  const template = document.createElement("template");
510
- 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 } : {}) });
511
512
  node = template.content.firstElementChild;
512
513
  if (!node)
513
514
  return;
package/dist/flight.d.ts CHANGED
@@ -93,6 +93,8 @@ export interface PoseNow {
93
93
  dy: number;
94
94
  pitch: number;
95
95
  gone: boolean;
96
+ /** Degrees each moving group is turned, by name. */
97
+ moves: Readonly<Record<string, number>>;
96
98
  }
97
99
  /**
98
100
  * Everything the picture needs at one place on the timeline: the phase, how each part stands, which engines burn, how far the ground has fallen, and how much smoke stands on the pad.
package/dist/flight.js CHANGED
@@ -103,7 +103,14 @@ const poseIn = (pose, part, height) => ({
103
103
  dy: pose?.rest ? -part.baseY : (pose?.dy ?? 0) * height,
104
104
  pitch: pose?.pitch ?? 0,
105
105
  gone: pose?.gone ?? false,
106
+ moves: pose?.moves ?? {},
106
107
  });
108
+ const mixMoves = (from, to, t) => {
109
+ const out = {};
110
+ for (const name of new Set([...Object.keys(from), ...Object.keys(to)]))
111
+ out[name] = mix(from[name] ?? 0, to[name] ?? 0, t);
112
+ return out;
113
+ };
107
114
  /**
108
115
  * Everything the picture needs at a place on the timeline, for a vehicle `height` metres tall.
109
116
  *
@@ -129,7 +136,7 @@ export function frameAt(flight, position, parts, height) {
129
136
  for (const part of parts) {
130
137
  const from = poseIn(phase.pose[part.slot], part, height);
131
138
  const to = poseIn(next.pose[part.slot], part, height);
132
- const now = { dx: mix(from.dx, to.dx, t), dy: mix(from.dy, to.dy, t), pitch: mix(from.pitch, to.pitch, t), gone: from.gone };
139
+ const now = { dx: mix(from.dx, to.dx, t), dy: mix(from.dy, to.dy, t), pitch: mix(from.pitch, to.pitch, t), gone: from.gone, moves: mixMoves(from.moves, to.moves, t) };
133
140
  poses[part.slot] = now;
134
141
  if (now.dx !== 0 || now.dy !== 0)
135
142
  separated = true;
package/dist/flightbar.js CHANGED
@@ -167,7 +167,7 @@ export class UchusenFlight extends HTMLBase {
167
167
  parts.leave.disabled = !state.active;
168
168
  parts.range.value = String(state.position);
169
169
  parts.range.setAttribute("aria-valuetext", `${clockWords(phase.at)}, ${phase.name}`);
170
- parts.now.textContent = `${clockWords(phase.at)}, ${phase.name}${phase.altitude?.value ? `, ${phase.altitude.value}` : ""}`;
170
+ parts.now.textContent = `${clockWords(phase.at)}, ${phase.name}${phase.altitude?.value ? `, ${phase.altitude.value}` : ""}${phase.speed?.value ? `, ${phase.speed.value}` : ""}`;
171
171
  parts.what.textContent = phase.what;
172
172
  const chartKey = `${flight.id}/${index}/${state.active}`;
173
173
  if (chartKey !== this.chartKey) {
package/dist/index.d.ts CHANGED
@@ -6,6 +6,9 @@ export { MAKERS } from "./data/makers.ts";
6
6
  export type { MakerId } from "./data/makers.ts";
7
7
  export { FLIGHT_LOG, flightLogOf } from "./data/flight-log.ts";
8
8
  export type { LoggedFlight } from "./data/flight-log.ts";
9
+ export { moved, movingGroupsOf } from "./model/moves.ts";
10
+ export { PROPELLANTS, propellantOf } from "./data/propellants.ts";
11
+ export type { EnginePropellant, FlameKind } from "./data/propellants.ts";
9
12
  export { SOURCES } from "./data/sources.ts";
10
13
  export type { Source, SourceId } from "./data/sources.ts";
11
14
  export { creditLine, MODEL_CREDITS } from "./data/credits.ts";
package/dist/index.js CHANGED
@@ -3,6 +3,8 @@ export { PLANNED_ROCKETS, ROCKETS, partIdOf, SLOT_NAMES, variantOf } from "./roc
3
3
  export { blank, figureWords } from "./data/types.js";
4
4
  export { MAKERS } from "./data/makers.js";
5
5
  export { FLIGHT_LOG, flightLogOf } from "./data/flight-log.js";
6
+ export { moved, movingGroupsOf } from "./model/moves.js";
7
+ export { PROPELLANTS, propellantOf } from "./data/propellants.js";
6
8
  export { SOURCES } from "./data/sources.js";
7
9
  export { creditLine, MODEL_CREDITS } from "./data/credits.js";
8
10
  export { PART_ENTRIES, partEntry } from "./model/registry.js";
@@ -56,11 +56,11 @@ function firstStage(d, withTop) {
56
56
  if (d.legs)
57
57
  for (const az of [45, 135, 225, 315]) {
58
58
  const frame = radial(deg(az));
59
- pieces.push({ kind: "plate", material: "black", frame, outline: [[r - 0.05, 0.8], [r + 0.5, 0.8], [r + 0.55, 3], [r + 0.15, 10.5], [r - 0.05, 11]], thickness: 1.2 });
59
+ pieces.push({ kind: "plate", material: "black", frame, outline: [[r - 0.05, 0.8], [r + 0.5, 0.8], [r + 0.55, 3], [r + 0.15, 10.5], [r - 0.05, 11]], thickness: 1.2, move: { name: "legs", pivot: pointOf(frame, r + 0.2, 0.8), axis: frame.w } });
60
60
  }
61
61
  if (d.fins)
62
62
  for (const az of [45, 135, 225, 315])
63
- pieces.push(...gridFin(az, tank + 0.1, r, r + 1.4, 1.5, 0.3, false));
63
+ pieces.push(...gridFin(az, tank + 0.1, r, r + 1.4, 1.5, 0.3, false, { name: "grid-fins", about: "tangent" }));
64
64
  return pieces;
65
65
  }
66
66
  function secondStage(d) {
@@ -0,0 +1,36 @@
1
+ import type { Piece, Vec3 } from "./pieces.ts";
2
+ /** A direction turned by `degrees` right-handed about `axis` (Rodrigues' rotation). */
3
+ export declare function turnedVector(v: Vec3, axis: Vec3, degrees: number): Vec3;
4
+ /** A point turned about a pivot. */
5
+ export declare function turnedPoint(p: Vec3, pivot: Vec3, axis: Vec3, degrees: number): Vec3;
6
+ /**
7
+ * The pieces with each moving group turned by the angle its name has in `angles` (degrees, 0 where it has none). A turned piece no longer carries its `move`: it is drawn where it stands. A body of revolution cannot turn, and is left as it is.
8
+ *
9
+ * @param pieces The pieces of a part.
10
+ * @param angles Degrees by the name of a moving group, such as `grid-fins`.
11
+ * @returns The pieces, with the moving groups turned.
12
+ * @example
13
+ * ```ts
14
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
15
+ * import { movingGroupsOf } from "@johnmorrisdotca/uchusen-rockets";
16
+ *
17
+ * const booster = build({ rocket: "starship", variant: "v3" }).parts[0];
18
+ * console.log(booster ? movingGroupsOf(booster.pieces) : []);
19
+ * ```
20
+ */
21
+ export declare function moved(pieces: readonly Piece[], angles: Readonly<Record<string, number>>): readonly Piece[];
22
+ /**
23
+ * The names of the groups a part has that a flight can turn.
24
+ *
25
+ * @param pieces The pieces of a part.
26
+ * @returns The distinct names, in the order they come.
27
+ * @example
28
+ * ```ts
29
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
30
+ * import { movingGroupsOf } from "@johnmorrisdotca/uchusen-rockets";
31
+ *
32
+ * const stage = build({ rocket: "falcon-9", variant: "block-5" }).parts[0];
33
+ * console.log(stage ? movingGroupsOf(stage.pieces) : []);
34
+ * ```
35
+ */
36
+ export declare function movingGroupsOf(pieces: readonly Piece[]): string[];
@@ -0,0 +1,70 @@
1
+ const unit = (v) => {
2
+ const length = Math.hypot(...v) || 1;
3
+ return [v[0] / length, v[1] / length, v[2] / length];
4
+ };
5
+ /** A direction turned by `degrees` right-handed about `axis` (Rodrigues' rotation). */
6
+ export function turnedVector(v, axis, degrees) {
7
+ const k = unit(axis);
8
+ const a = (degrees * Math.PI) / 180;
9
+ const cos = Math.cos(a);
10
+ const sin = Math.sin(a);
11
+ const dot = k[0] * v[0] + k[1] * v[1] + k[2] * v[2];
12
+ const cross = [k[1] * v[2] - k[2] * v[1], k[2] * v[0] - k[0] * v[2], k[0] * v[1] - k[1] * v[0]];
13
+ return [v[0] * cos + cross[0] * sin + k[0] * dot * (1 - cos), v[1] * cos + cross[1] * sin + k[1] * dot * (1 - cos), v[2] * cos + cross[2] * sin + k[2] * dot * (1 - cos)];
14
+ }
15
+ /** A point turned about a pivot. */
16
+ export function turnedPoint(p, pivot, axis, degrees) {
17
+ const d = turnedVector([p[0] - pivot[0], p[1] - pivot[1], p[2] - pivot[2]], axis, degrees);
18
+ return [pivot[0] + d[0], pivot[1] + d[1], pivot[2] + d[2]];
19
+ }
20
+ const turnedFrame = (f, move, degrees) => ({
21
+ o: turnedPoint(f.o, move.pivot, move.axis, degrees),
22
+ u: turnedVector(f.u, move.axis, degrees),
23
+ v: turnedVector(f.v, move.axis, degrees),
24
+ w: turnedVector(f.w, move.axis, degrees),
25
+ });
26
+ /**
27
+ * The pieces with each moving group turned by the angle its name has in `angles` (degrees, 0 where it has none). A turned piece no longer carries its `move`: it is drawn where it stands. A body of revolution cannot turn, and is left as it is.
28
+ *
29
+ * @param pieces The pieces of a part.
30
+ * @param angles Degrees by the name of a moving group, such as `grid-fins`.
31
+ * @returns The pieces, with the moving groups turned.
32
+ * @example
33
+ * ```ts
34
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
35
+ * import { movingGroupsOf } from "@johnmorrisdotca/uchusen-rockets";
36
+ *
37
+ * const booster = build({ rocket: "starship", variant: "v3" }).parts[0];
38
+ * console.log(booster ? movingGroupsOf(booster.pieces) : []);
39
+ * ```
40
+ */
41
+ export function moved(pieces, angles) {
42
+ if (!pieces.some((piece) => piece.move && angles[piece.move.name]))
43
+ return pieces;
44
+ return pieces.map((piece) => {
45
+ const move = piece.move;
46
+ const degrees = move ? (angles[move.name] ?? 0) : 0;
47
+ if (!move || degrees === 0 || piece.kind === "lathe")
48
+ return piece;
49
+ if (piece.kind === "plate")
50
+ return { ...piece, move: undefined, frame: turnedFrame(piece.frame, move, degrees) };
51
+ return { ...piece, move: undefined, paths: piece.paths.map((path) => path.map((p) => turnedPoint(p, move.pivot, move.axis, degrees))) };
52
+ });
53
+ }
54
+ /**
55
+ * The names of the groups a part has that a flight can turn.
56
+ *
57
+ * @param pieces The pieces of a part.
58
+ * @returns The distinct names, in the order they come.
59
+ * @example
60
+ * ```ts
61
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
62
+ * import { movingGroupsOf } from "@johnmorrisdotca/uchusen-rockets";
63
+ *
64
+ * const stage = build({ rocket: "falcon-9", variant: "block-5" }).parts[0];
65
+ * console.log(stage ? movingGroupsOf(stage.pieces) : []);
66
+ * ```
67
+ */
68
+ export function movingGroupsOf(pieces) {
69
+ return [...new Set(pieces.flatMap((piece) => (piece.move ? [piece.move.name] : [])))];
70
+ }
@@ -7,6 +7,15 @@ export interface Frame {
7
7
  v: Vec3;
8
8
  w: Vec3;
9
9
  }
10
+ /**
11
+ * A piece that a flight can turn: the name of the group it belongs to (every piece with the same name and pivot turns together), the point it turns about and the axis it turns round, in the part's own metres.
12
+ * A positive angle turns it right-handed about the axis. The piece is drawn where its group is at rest, which is the angle 0.
13
+ */
14
+ export interface Move {
15
+ name: string;
16
+ pivot: Vec3;
17
+ axis: Vec3;
18
+ }
10
19
  /** A body of revolution: `profile` is [radius, y] points going up the outside (going the other way faces inward). */
11
20
  export interface Lathe {
12
21
  kind: "lathe";
@@ -32,6 +41,7 @@ export interface Lathe {
32
41
  body?: boolean;
33
42
  /** Its outline is drawn in a drawing. */
34
43
  outline?: boolean;
44
+ move?: Move;
35
45
  }
36
46
  /** A flat outline with a thickness, in a frame: `outline` is [u, v] points, going round; it stands from -thickness/2 to +thickness/2 along w. Convex, or it is drawn as its hull. */
37
47
  export interface Plate {
@@ -47,6 +57,7 @@ export interface Plate {
47
57
  only?: "mesh" | "drawing";
48
58
  /** No feature lines of its own. */
49
59
  plain?: boolean;
60
+ move?: Move;
50
61
  }
51
62
  /** Lines only, in the part's own space. */
52
63
  export interface Lines {
@@ -54,6 +65,7 @@ export interface Lines {
54
65
  weight: "feature" | "weld";
55
66
  paths: readonly (readonly Vec3[])[];
56
67
  only?: "mesh" | "drawing";
68
+ move?: Move;
57
69
  }
58
70
  export type Piece = Lathe | Plate | Lines;
59
71
  /** A frame standing on the axis: u points outwards at `az` radians, v is up, w is along the circle. */
@@ -76,15 +88,34 @@ export interface LineBuffers {
76
88
  positions: number[];
77
89
  indices: number[];
78
90
  }
91
+ /** The mesh of one moving group, its points relative to its pivot, so the group turns about its own origin. */
92
+ export interface MoveMesh {
93
+ /** The group's name, and a node name of its own: `grid-fins~0`, `grid-fins~1`. */
94
+ name: string;
95
+ id: string;
96
+ pivot: Vec3;
97
+ axis: Vec3;
98
+ mesh: PartMesh;
99
+ }
79
100
  export interface PartMesh {
80
101
  faces: Map<MaterialName, Buffers>;
81
102
  feature: LineBuffers;
82
103
  weld: LineBuffers;
104
+ /** What a flight can turn, apart from the rest. Empty for a part with no moving group. */
105
+ moves: MoveMesh[];
83
106
  }
84
107
  /** A ring just proud of the surface at a height, so it is not lost in it. */
85
108
  export declare function ringPath(piece: Lathe, y: number, lift?: number): Vec3[];
86
109
  /** The triangles and lines of a part. */
87
110
  export declare function meshOf(pieces: readonly Piece[]): PartMesh;
111
+ /** The groups a flight can turn: the pieces with a `move`, by name and pivot, in the order they come, each with a node name of its own. */
112
+ export declare function moveGroups(pieces: readonly Piece[]): {
113
+ name: string;
114
+ id: string;
115
+ pivot: Vec3;
116
+ axis: Vec3;
117
+ pieces: Piece[];
118
+ }[];
88
119
  export interface Bounds {
89
120
  min: Vec3;
90
121
  max: Vec3;
@@ -178,7 +178,43 @@ function plateFaces(mesh, piece) {
178
178
  }
179
179
  /** The triangles and lines of a part. */
180
180
  export function meshOf(pieces) {
181
- const mesh = { faces: new Map(), feature: { positions: [], indices: [] }, weld: { positions: [], indices: [] } };
181
+ const mesh = meshOfFixed(pieces.filter((piece) => !piece.move));
182
+ for (const group of moveGroups(pieces)) {
183
+ const part = meshOfFixed(group.pieces);
184
+ const shift = (positions) => {
185
+ for (let i = 0; i < positions.length; i += 3)
186
+ for (let c = 0; c < 3; c++)
187
+ positions[i + c] -= group.pivot[c];
188
+ };
189
+ for (const buffers of part.faces.values())
190
+ shift(buffers.positions);
191
+ shift(part.feature.positions);
192
+ shift(part.weld.positions);
193
+ mesh.moves.push({ name: group.name, id: group.id, pivot: group.pivot, axis: group.axis, mesh: part });
194
+ }
195
+ return mesh;
196
+ }
197
+ /** The groups a flight can turn: the pieces with a `move`, by name and pivot, in the order they come, each with a node name of its own. */
198
+ export function moveGroups(pieces) {
199
+ const groups = [];
200
+ const counts = new Map();
201
+ for (const piece of pieces) {
202
+ if (!piece.move)
203
+ continue;
204
+ const key = `${piece.move.name}@${piece.move.pivot.join(",")}`;
205
+ let group = groups.find((one) => one.key === key);
206
+ if (!group) {
207
+ const n = counts.get(piece.move.name) ?? 0;
208
+ counts.set(piece.move.name, n + 1);
209
+ group = { name: piece.move.name, id: `${piece.move.name}~${n}`, pivot: piece.move.pivot, axis: piece.move.axis, pieces: [], key };
210
+ groups.push(group);
211
+ }
212
+ group.pieces.push(piece);
213
+ }
214
+ return groups.map((group) => ({ name: group.name, id: group.id, pivot: group.pivot, axis: group.axis, pieces: group.pieces }));
215
+ }
216
+ function meshOfFixed(pieces) {
217
+ const mesh = { faces: new Map(), feature: { positions: [], indices: [] }, weld: { positions: [], indices: [] }, moves: [] };
182
218
  for (const piece of pieces) {
183
219
  if (piece.only === "drawing")
184
220
  continue;
@@ -211,13 +247,19 @@ export function meshOf(pieces) {
211
247
  export function boundsOf(mesh) {
212
248
  const min = [Infinity, Infinity, Infinity];
213
249
  const max = [-Infinity, -Infinity, -Infinity];
214
- for (const b of mesh.faces.values())
215
- for (let i = 0; i < b.positions.length; i += 3)
216
- for (let c = 0; c < 3; c++) {
217
- if (b.positions[i + c] < min[c])
218
- min[c] = b.positions[i + c];
219
- if (b.positions[i + c] > max[c])
220
- max[c] = b.positions[i + c];
221
- }
250
+ const sweep = (from, offset) => {
251
+ for (const b of from.faces.values())
252
+ for (let i = 0; i < b.positions.length; i += 3)
253
+ for (let c = 0; c < 3; c++) {
254
+ const value = b.positions[i + c] + offset[c];
255
+ if (value < min[c])
256
+ min[c] = value;
257
+ if (value > max[c])
258
+ max[c] = value;
259
+ }
260
+ };
261
+ sweep(mesh, [0, 0, 0]);
262
+ for (const group of mesh.moves)
263
+ sweep(group.mesh, group.pivot);
222
264
  return { min, max };
223
265
  }
@@ -57,10 +57,13 @@ export interface StarshipDraw {
57
57
  export declare const STARSHIP_DRAWS: Readonly<Record<string, StarshipDraw>>;
58
58
  /** One Raptor from its exit up: the bell, its dark inside and the machinery over it. Its origin is the middle of its exit. */
59
59
  export declare function raptor(at: readonly [number, number], y: number, type: "sea" | "vacuum"): Piece[];
60
- /** A grid fin held out flat: a frame and a lattice, u outwards and v along the circle. */
61
- export declare function gridFin(azDeg: number, y: number, rootRadius: number, reach: number, width: number, thick: number, octagon: boolean): Piece[];
60
+ /** A grid fin held out flat: a frame and a lattice, u outwards and v along the circle. With `turn`, every piece of it is one moving group, hinged at its root: round the fin's own axis out from the hull (`radial`, how it steers) or across it (`tangent`, how it folds in). */
61
+ export declare function gridFin(azDeg: number, y: number, rootRadius: number, reach: number, width: number, thick: number, octagon: boolean, turn?: {
62
+ name: string;
63
+ about: "radial" | "tangent";
64
+ }): Piece[];
62
65
  /** A flap's plate, tiled on its windward face. `outline` is [radius, height]. */
63
- export declare function flap(draw: FlapDraw, side: 1 | -1): Plate;
66
+ export declare function flap(draw: FlapDraw, side: 1 | -1, name?: string): Plate;
64
67
  /** Super Heavy: its base on the axis at the bells' exit. */
65
68
  export declare function boosterPieces(d: StarshipDraw): Piece[];
66
69
  /** The ship: its base on the axis at the bottom of its skirt. */
@@ -230,8 +230,8 @@ function clip(a, b, outline) {
230
230
  [a[0] + d[0] * t1, a[1] + d[1] * t1],
231
231
  ];
232
232
  }
233
- /** A grid fin held out flat: a frame and a lattice, u outwards and v along the circle. */
234
- export function gridFin(azDeg, y, rootRadius, reach, width, thick, octagon) {
233
+ /** A grid fin held out flat: a frame and a lattice, u outwards and v along the circle. With `turn`, every piece of it is one moving group, hinged at its root: round the fin's own axis out from the hull (`radial`, how it steers) or across it (`tangent`, how it folds in). */
234
+ export function gridFin(azDeg, y, rootRadius, reach, width, thick, octagon, turn) {
235
235
  const frame = flat(deg(azDeg), y);
236
236
  const r0 = rootRadius + 0.35;
237
237
  const hw = width / 2;
@@ -291,10 +291,14 @@ export function gridFin(azDeg, y, rootRadius, reach, width, thick, octagon) {
291
291
  });
292
292
  // where it meets the hull
293
293
  pieces.push(strut("fin", pointOf(frame, rootRadius - 0.1, 0, 0), pointOf(frame, r0 + 0.05, 0, 0), 1, 0.7, { only: "mesh" }));
294
+ if (turn) {
295
+ const move = { name: turn.name, pivot: pointOf(frame, r0, 0, 0), axis: turn.about === "radial" ? frame.u : frame.v };
296
+ return pieces.map((piece) => ({ ...piece, move }));
297
+ }
294
298
  return pieces;
295
299
  }
296
300
  /** A flap's plate, tiled on its windward face. `outline` is [radius, height]. */
297
- export function flap(draw, side) {
301
+ export function flap(draw, side, name) {
298
302
  const az = deg(draw.az) * side;
299
303
  const frame = radial(az);
300
304
  // w must look windward, so the +w face takes the tiles
@@ -308,7 +312,8 @@ export function flap(draw, side) {
308
312
  if (seam)
309
313
  ribs.push(seam);
310
314
  }
311
- return { kind: "plate", material: "steel", top: "tile-plain", frame: windward, outline: draw.outline, thickness: draw.thickness, ribs };
315
+ const hinge = name ? { name, pivot: pointOf(frame, Math.min(...reach), 0, 0), axis: [0, side, 0] } : undefined;
316
+ return { kind: "plate", material: "steel", top: "tile-plain", frame: windward, outline: draw.outline, thickness: draw.thickness, ribs, ...(hinge ? { move: hinge } : {}) };
312
317
  }
313
318
  /** A flap's root fairing above it. */
314
319
  function fairing(azDeg, y0, y1, surface, proud) {
@@ -448,7 +453,7 @@ export function boosterPieces(d) {
448
453
  // the band of vents round the common dome
449
454
  pieces.push({ kind: "lathe", material: "engine-pale", segs: 64, only: "mesh", profile: [[r + 0.015, topOfHull * 0.62 - 0.12], [r + 0.015, topOfHull * 0.62 + 0.12]] });
450
455
  for (const az of d.fins.az)
451
- pieces.push(...gridFin(az, d.fins.y, r, d.fins.reach, d.fins.width, d.fins.thick, d.fins.octagon));
456
+ pieces.push(...gridFin(az, d.fins.y, r, d.fins.reach, d.fins.width, d.fins.thick, d.fins.octagon, { name: "grid-fins", about: "radial" }));
452
457
  if (d.ring.height > 0)
453
458
  pieces.push(...hotStage(d));
454
459
  else
@@ -475,8 +480,8 @@ export function shipPieces(d) {
475
480
  pieces.push(...engineRings(d.shipEngines, 0.2));
476
481
  const surface = (y) => radiusAt(profile, y);
477
482
  for (const side of [1, -1]) {
478
- pieces.push(flap(d.aft, side));
479
- pieces.push(flap(d.forward, side));
483
+ pieces.push(flap(d.aft, side, "aft-flaps"));
484
+ pieces.push(flap(d.forward, side, "forward-flaps"));
480
485
  const aftTop = Math.max(...d.aft.outline.map(([, y]) => y));
481
486
  pieces.push(fairing(d.aft.az * side, aftTop - 1, aftTop + 2.8, surface, 0.8));
482
487
  const fwdBottom = Math.min(...d.forward.outline.map(([, y]) => y));
package/dist/react.d.ts CHANGED
@@ -64,14 +64,14 @@ export declare function UchusenRocket({ build, ...rest }: UchusenRocketProps): i
64
64
  /** A phase of the rocket's flight, by its id. */
65
65
  phase?: string;
66
66
  ref?: Ref<HTMLElement>;
67
- part?: string | undefined | undefined;
68
67
  id?: string | undefined | undefined;
68
+ about?: string | undefined | undefined;
69
+ part?: string | undefined | undefined;
69
70
  slot?: string | undefined | undefined;
70
71
  children?: import("react").ReactNode;
71
72
  title?: string | undefined | undefined;
72
73
  "aria-label"?: string | undefined | undefined;
73
74
  "aria-live"?: "off" | "assertive" | "polite" | undefined | undefined;
74
- about?: string | undefined | undefined;
75
75
  "aria-pressed"?: boolean | "false" | "mixed" | "true" | undefined | undefined;
76
76
  "aria-valuetext"?: string | undefined | undefined;
77
77
  color?: string | undefined | undefined;