@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
package/dist/attrs.d.ts CHANGED
@@ -1,4 +1,25 @@
1
+ /**
2
+ * The attributes of `<uchusen-rocket>` that choose what is drawn, in the order the demo writes them. The `embed.html` query uses the same words.
3
+ *
4
+ * @example
5
+ * ```ts
6
+ * import { OPTION_ATTRIBUTES } from "@johnmorrisdotca/uchusen-rockets";
7
+ *
8
+ * console.log(OPTION_ATTRIBUTES.join(", "));
9
+ * ```
10
+ */
1
11
  export declare const OPTION_ATTRIBUTES: readonly ["rocket", "variant", "show", "look", "view", "build", "mode", "spin", "way", "label", "assets", "engines", "phase"];
12
+ /**
13
+ * Every choice that draws a rocket, as plain text: the same names and values as the element's attributes. Nothing here is trusted. A value outside its list falls back to a default when it is drawn.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import type { RocketOptions } from "@johnmorrisdotca/uchusen-rockets";
18
+ *
19
+ * const options: RocketOptions = { rocket: "falcon-9", variant: "block-5", look: "thick", view: "below" };
20
+ * console.log(options);
21
+ * ```
22
+ */
2
23
  export interface RocketOptions {
3
24
  rocket?: string;
4
25
  variant?: string;
@@ -17,11 +38,56 @@ export interface RocketOptions {
17
38
  /** A phase of the rocket's flight, by its id: the engines that burn there. */
18
39
  phase?: string;
19
40
  }
20
- /** The attributes that draw these options. Empty values are left out. */
41
+ /**
42
+ * The attributes that draw these options. Empty values are left out.
43
+ *
44
+ * @param options The choices to turn into attributes.
45
+ * @returns The attributes by name, as text. A choice left out or empty has no entry.
46
+ * @example
47
+ * ```ts
48
+ * import { attributesFromOptions } from "@johnmorrisdotca/uchusen-rockets";
49
+ *
50
+ * console.log(attributesFromOptions({ rocket: "starship", variant: "v3", look: "painted", show: "" }));
51
+ * ```
52
+ */
21
53
  export declare function attributesFromOptions(options: RocketOptions): Record<string, string>;
22
- /** The options `read` finds. A missing attribute is left out; nothing here is trusted as a real rocket. */
54
+ /**
55
+ * The options `read` finds. A missing attribute is left out; nothing here is trusted as a real rocket.
56
+ *
57
+ * @param read Reads one attribute by name and gives null when it is missing, as `element.getAttribute` does.
58
+ * @returns The choices found, as written. A missing attribute is left out of the result.
59
+ * @example
60
+ * ```ts
61
+ * import { optionsFromAttributes } from "@johnmorrisdotca/uchusen-rockets";
62
+ *
63
+ * const attributes: Record<string, string> = { rocket: "starship", look: "thick" };
64
+ * console.log(optionsFromAttributes((name) => attributes[name] ?? null));
65
+ * ```
66
+ */
23
67
  export declare function optionsFromAttributes(read: (name: string) => string | null): RocketOptions;
24
- /** The options as a query for `embed.html`. */
68
+ /**
69
+ * The options as a query for `embed.html`.
70
+ *
71
+ * @param options The choices to write.
72
+ * @returns A query for `embed.html`, with one entry for each choice made.
73
+ * @example
74
+ * ```ts
75
+ * import { rocketToParams } from "@johnmorrisdotca/uchusen-rockets";
76
+ *
77
+ * console.log(rocketToParams({ rocket: "falcon-9", show: "apart" }).toString());
78
+ * ```
79
+ */
25
80
  export declare function rocketToParams(options: RocketOptions): URLSearchParams;
26
- /** The options a query names. */
81
+ /**
82
+ * The options a query names.
83
+ *
84
+ * @param params The query of an `embed.html` address.
85
+ * @returns The choices the query names.
86
+ * @example
87
+ * ```ts
88
+ * import { rocketFromParams } from "@johnmorrisdotca/uchusen-rockets";
89
+ *
90
+ * console.log(rocketFromParams(new URLSearchParams("rocket=falcon-9&look=thick")));
91
+ * ```
92
+ */
27
93
  export declare function rocketFromParams(params: URLSearchParams): RocketOptions;
package/dist/attrs.js CHANGED
@@ -1,7 +1,28 @@
1
1
  // A rocket's options as text: the attributes of `<uchusen-rocket>` and the query of `embed.html` are the same words, read by one function and written by another.
2
+ /**
3
+ * The attributes of `<uchusen-rocket>` that choose what is drawn, in the order the demo writes them. The `embed.html` query uses the same words.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * import { OPTION_ATTRIBUTES } from "@johnmorrisdotca/uchusen-rockets";
8
+ *
9
+ * console.log(OPTION_ATTRIBUTES.join(", "));
10
+ * ```
11
+ */
2
12
  export const OPTION_ATTRIBUTES = ["rocket", "variant", "show", "look", "view", "build", "mode", "spin", "way", "label", "assets", "engines", "phase"];
3
13
  const text = (value) => (typeof value === "string" && value !== "" ? value : undefined);
4
- /** The attributes that draw these options. Empty values are left out. */
14
+ /**
15
+ * The attributes that draw these options. Empty values are left out.
16
+ *
17
+ * @param options The choices to turn into attributes.
18
+ * @returns The attributes by name, as text. A choice left out or empty has no entry.
19
+ * @example
20
+ * ```ts
21
+ * import { attributesFromOptions } from "@johnmorrisdotca/uchusen-rockets";
22
+ *
23
+ * console.log(attributesFromOptions({ rocket: "starship", variant: "v3", look: "painted", show: "" }));
24
+ * ```
25
+ */
5
26
  export function attributesFromOptions(options) {
6
27
  const out = {};
7
28
  for (const name of OPTION_ATTRIBUTES) {
@@ -11,7 +32,19 @@ export function attributesFromOptions(options) {
11
32
  }
12
33
  return out;
13
34
  }
14
- /** The options `read` finds. A missing attribute is left out; nothing here is trusted as a real rocket. */
35
+ /**
36
+ * The options `read` finds. A missing attribute is left out; nothing here is trusted as a real rocket.
37
+ *
38
+ * @param read Reads one attribute by name and gives null when it is missing, as `element.getAttribute` does.
39
+ * @returns The choices found, as written. A missing attribute is left out of the result.
40
+ * @example
41
+ * ```ts
42
+ * import { optionsFromAttributes } from "@johnmorrisdotca/uchusen-rockets";
43
+ *
44
+ * const attributes: Record<string, string> = { rocket: "starship", look: "thick" };
45
+ * console.log(optionsFromAttributes((name) => attributes[name] ?? null));
46
+ * ```
47
+ */
15
48
  export function optionsFromAttributes(read) {
16
49
  const options = {};
17
50
  for (const name of OPTION_ATTRIBUTES) {
@@ -21,11 +54,33 @@ export function optionsFromAttributes(read) {
21
54
  }
22
55
  return options;
23
56
  }
24
- /** The options as a query for `embed.html`. */
57
+ /**
58
+ * The options as a query for `embed.html`.
59
+ *
60
+ * @param options The choices to write.
61
+ * @returns A query for `embed.html`, with one entry for each choice made.
62
+ * @example
63
+ * ```ts
64
+ * import { rocketToParams } from "@johnmorrisdotca/uchusen-rockets";
65
+ *
66
+ * console.log(rocketToParams({ rocket: "falcon-9", show: "apart" }).toString());
67
+ * ```
68
+ */
25
69
  export function rocketToParams(options) {
26
70
  return new URLSearchParams(attributesFromOptions(options));
27
71
  }
28
- /** The options a query names. */
72
+ /**
73
+ * The options a query names.
74
+ *
75
+ * @param params The query of an `embed.html` address.
76
+ * @returns The choices the query names.
77
+ * @example
78
+ * ```ts
79
+ * import { rocketFromParams } from "@johnmorrisdotca/uchusen-rockets";
80
+ *
81
+ * console.log(rocketFromParams(new URLSearchParams("rocket=falcon-9&look=thick")));
82
+ * ```
83
+ */
29
84
  export function rocketFromParams(params) {
30
85
  return optionsFromAttributes((name) => params.get(name));
31
86
  }
@@ -5,8 +5,33 @@ export interface GlbPart {
5
5
  mesh: PartMesh;
6
6
  at: readonly [number, number, number];
7
7
  }
8
+ /**
9
+ * A binary glTF file: its bytes and how many triangles it holds. Save the bytes as a `.glb` file or hand them to any glTF viewer.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
14
+ *
15
+ * const glb = build({ rocket: "starship", variant: "v3" }).glb();
16
+ * console.log(glb.bytes.length > 1000, glb.triangles > 0);
17
+ * ```
18
+ */
8
19
  export interface Glb {
9
20
  bytes: Uint8Array;
10
21
  triangles: number;
11
22
  }
23
+ /**
24
+ * Writes parts into one small binary glTF file, in Node or a browser, with no dependency: one node for each part and one primitive for each material. The same parts always give the same bytes.
25
+ *
26
+ * @param parts The parts to write: each has a node name, its mesh and where it stands in metres.
27
+ * @returns The file's bytes and its triangle count.
28
+ * @example
29
+ * ```ts
30
+ * import { build, toGlb } from "@johnmorrisdotca/uchusen-rockets/build";
31
+ *
32
+ * const rocket = build({ rocket: "falcon-9", variant: "block-5" });
33
+ * const glb = toGlb(rocket.parts.map((part) => ({ name: part.slot, mesh: part.mesh, at: [0, 0, 0] as const })));
34
+ * console.log(glb.bytes.length > 1000, glb.triangles > 0);
35
+ * ```
36
+ */
12
37
  export declare function toGlb(parts: readonly GlbPart[]): Glb;
package/dist/build/glb.js CHANGED
@@ -10,6 +10,20 @@ const LINES = 1;
10
10
  const round6 = (v) => Math.round(v * 1e6) / 1e6;
11
11
  const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
12
12
  const linearOf = (hex) => [1, 3, 5].map((i) => round6(toLinear(Number.parseInt(hex.slice(i, i + 2), 16) / 255)));
13
+ /**
14
+ * Writes parts into one small binary glTF file, in Node or a browser, with no dependency: one node for each part and one primitive for each material. The same parts always give the same bytes.
15
+ *
16
+ * @param parts The parts to write: each has a node name, its mesh and where it stands in metres.
17
+ * @returns The file's bytes and its triangle count.
18
+ * @example
19
+ * ```ts
20
+ * import { build, toGlb } from "@johnmorrisdotca/uchusen-rockets/build";
21
+ *
22
+ * const rocket = build({ rocket: "falcon-9", variant: "block-5" });
23
+ * const glb = toGlb(rocket.parts.map((part) => ({ name: part.slot, mesh: part.mesh, at: [0, 0, 0] as const })));
24
+ * console.log(glb.bytes.length > 1000, glb.triangles > 0);
25
+ * ```
26
+ */
13
27
  export function toGlb(parts) {
14
28
  const chunks = [];
15
29
  let offset = 0;
@@ -47,8 +61,8 @@ export function toGlb(parts) {
47
61
  const meshes = [];
48
62
  const nodes = [];
49
63
  const roots = [];
50
- for (const part of parts) {
51
- const { mesh } = part;
64
+ /** One mesh of a part or of one of its moving groups: its primitives and the packing's middle and half-size, returned as the nodes that hold it. */
65
+ const packMesh = (mesh, name) => {
52
66
  const faces = [...mesh.faces];
53
67
  // the packing's middle and half-size, over all of the part's points
54
68
  const lo = [Infinity, Infinity, Infinity];
@@ -112,11 +126,19 @@ export function toGlb(parts) {
112
126
  continue;
113
127
  primitives.push({ attributes: { POSITION: packPositions(lines.positions) }, indices: indexAccessor(lines.indices, lines.positions.length / 3), material: useMaterial(name), mode: LINES });
114
128
  }
115
- meshes.push({ name: part.name, primitives });
116
- // a holder that the viewer moves, and the packed mesh under it
117
- nodes.push({ name: `${part.name}-mesh`, mesh: meshes.length - 1, translation: q.map(round6), scale: [round6(half), round6(half), round6(half)] });
118
- const child = nodes.length - 1;
119
- nodes.push({ name: part.name, children: [child], translation: part.at.map(round6) });
129
+ meshes.push({ name, primitives });
130
+ nodes.push({ name: `${name}-mesh`, mesh: meshes.length - 1, translation: q.map(round6), scale: [round6(half), round6(half), round6(half)] });
131
+ return nodes.length - 1;
132
+ };
133
+ for (const part of parts) {
134
+ // a holder that the viewer moves, and the packed mesh under it; each moving group is a node of its own beside it, at its pivot, with the axis it turns about
135
+ const children = [packMesh(part.mesh, part.name)];
136
+ for (const group of part.mesh.moves) {
137
+ const inside = packMesh(group.mesh, `${part.name}~${group.id}`);
138
+ nodes.push({ name: `${part.name}~${group.id}`, children: [inside], translation: group.pivot.map(round6), extras: { axis: group.axis.map(round6), group: group.name } });
139
+ children.push(nodes.length - 1);
140
+ }
141
+ nodes.push({ name: part.name, children, translation: part.at.map(round6) });
120
142
  roots.push(nodes.length - 1);
121
143
  }
122
144
  const json = {
@@ -1,3 +1,4 @@
1
+ import { type FlameKind } from "../data/propellants.ts";
1
2
  import { type Description, type FitReport } from "../fit.ts";
2
3
  import { type Angle, type Vec3, type ViewName } from "../framing.ts";
3
4
  import { type LookName } from "../looks.ts";
@@ -5,25 +6,77 @@ import { type PartMesh, type Bounds, type Piece } from "../model/pieces.ts";
5
6
  import type { PartRecord } from "../parts.ts";
6
7
  import { type LitBell } from "../engines.ts";
7
8
  import { type Glb, toGlb } from "./glb.ts";
8
- /** Raised when a description does not fit: its report says why. */
9
+ /**
10
+ * Raised by `build` when a description does not fit. Its `report` says why, in the same words `check` gives.
11
+ *
12
+ * @param report The report of the fit check that failed.
13
+ * @example
14
+ * ```ts
15
+ * import { build, FitError } from "@johnmorrisdotca/uchusen-rockets/build";
16
+ *
17
+ * try {
18
+ * build({ rocket: "starship", variant: "v3", parts: { booster: "starship-v3-ship" } });
19
+ * } catch (error) {
20
+ * if (error instanceof FitError) console.log(error.report.issues[0]?.message);
21
+ * }
22
+ * ```
23
+ */
9
24
  export declare class FitError extends Error {
10
25
  readonly report: FitReport;
11
26
  constructor(report: FitReport);
12
27
  }
28
+ /**
29
+ * One part of a built rocket: its slot, its record, the pieces it is made of, its mesh and its bounds in metres.
30
+ *
31
+ * @example
32
+ * ```ts
33
+ * import { build, type BuiltPart } from "@johnmorrisdotca/uchusen-rockets/build";
34
+ *
35
+ * const first: BuiltPart | undefined = build({ rocket: "falcon-9", variant: "block-5" }).parts[0];
36
+ * console.log(first?.slot, first?.record.name);
37
+ * ```
38
+ */
13
39
  export interface BuiltPart {
14
40
  slot: string;
15
41
  record: PartRecord;
16
42
  pieces: readonly Piece[];
17
43
  mesh: PartMesh;
18
44
  bounds: Bounds;
45
+ /** The colour of this part's flame, from what a page says its engine burns; `neutral` when none read here says. */
46
+ flame: FlameKind;
19
47
  }
48
+ /**
49
+ * What a drawing is asked for: the look, the view, what to show and whether the engines are lit. Anything left out takes its default.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * import { build, type SvgOptions } from "@johnmorrisdotca/uchusen-rockets/build";
54
+ *
55
+ * const options: SvgOptions = { look: "thick", view: "below", show: "booster" };
56
+ * console.log(build({ rocket: "starship", variant: "v3" }).svg(options).startsWith("<svg"));
57
+ * ```
58
+ */
20
59
  export interface SvgOptions {
21
60
  look?: LookName;
22
61
  view?: ViewName | Angle;
23
62
  show?: string;
24
63
  /** A flame at each engine that is showing, or as many as a count names for each part. The length is a picture, not a measured flame. Left out, the drawing is unchanged. */
25
64
  engines?: boolean | Readonly<Record<string, number>>;
65
+ /** Degrees each moving group of each part is turned from where it is drawn at rest, by slot and then by name: `{ booster: { "grid-fins": 12 } }`. A flight asks for them. */
66
+ moves?: Readonly<Record<string, Readonly<Record<string, number>>>>;
26
67
  }
68
+ /**
69
+ * A rocket built from a description: the fit report, its parts, and the ways to ask for its size, where each part stands, its lit engines, a `.glb` file and a drawing.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * import { build, type Built } from "@johnmorrisdotca/uchusen-rockets/build";
74
+ *
75
+ * const rocket: Built = build({ rocket: "starship", variant: "v3" });
76
+ * console.log(rocket.shows.join(", "));
77
+ * console.log(rocket.boundsOf("together").max);
78
+ * ```
79
+ */
27
80
  export interface Built {
28
81
  report: FitReport;
29
82
  parts: readonly BuiltPart[];
@@ -44,14 +97,51 @@ export interface Built {
44
97
  /** The drawing, in a look, from an angle. */
45
98
  svg(options?: SvgOptions): string;
46
99
  }
47
- /** What a drawing draws of a part: in a stack, what nests inside the part below is out of sight. */
100
+ /**
101
+ * What a drawing draws of a part: in a stack, what nests inside the part below is out of sight.
102
+ *
103
+ * @param part A built part: its record and its pieces.
104
+ * @param together True when the part stands in a stack, false when it is shown alone.
105
+ * @returns The pieces to draw. In a stack, what nests inside the part below is left out.
106
+ * @example
107
+ * ```ts
108
+ * import { build, drawnPieces } from "@johnmorrisdotca/uchusen-rockets/build";
109
+ *
110
+ * const [first] = build({ rocket: "falcon-9", variant: "block-5" }).parts;
111
+ * if (first) console.log(drawnPieces(first, true).length, drawnPieces(first, false).length);
112
+ * ```
113
+ */
48
114
  export declare function drawnPieces(part: {
49
115
  record: PartRecord;
50
116
  pieces: readonly Piece[];
51
117
  }, together: boolean): readonly Piece[];
52
- /** A rocket from a description. Throws FitError when it does not go together. */
118
+ /**
119
+ * A rocket from a description. Throws FitError when it does not go together.
120
+ *
121
+ * @param description The rocket, its variant and any other part in a slot.
122
+ * @returns The built rocket. It throws a `FitError` when the description does not go together.
123
+ * @example
124
+ * ```ts
125
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
126
+ *
127
+ * const rocket = build({ rocket: "falcon-heavy" });
128
+ * console.log(rocket.report.label, rocket.glb().bytes.length > 1000);
129
+ * ```
130
+ */
53
131
  export declare function build(description: Description): Built;
54
- /** One part on its own, by its id. */
132
+ /**
133
+ * One part on its own, by its id.
134
+ *
135
+ * @param id A part's id, as `PART_ENTRIES` lists it.
136
+ * @returns The part's record and bounds, a function for its `.glb` and one for its drawing. It throws for an id that is not a part.
137
+ * @example
138
+ * ```ts
139
+ * import { buildPart } from "@johnmorrisdotca/uchusen-rockets/build";
140
+ *
141
+ * const raptor = buildPart("raptor-vacuum");
142
+ * console.log(raptor.record.name, raptor.bounds.max);
143
+ * ```
144
+ */
55
145
  export declare function buildPart(id: string): {
56
146
  record: PartRecord;
57
147
  bounds: Bounds;
@@ -1,15 +1,32 @@
1
1
  // The builder: one description into one mesh and one drawing. Works in Node and in a browser, with no dependency: the geometry here is this package's own.
2
+ import { propellantOf } from "../data/propellants.js";
2
3
  import { check } from "../fit.js";
3
4
  import { cleanView, DEFAULT_VIEW, PICTURE_H, PICTURE_W, VIEWS } from "../framing.js";
4
5
  import { cleanShow, placesFor, showsOf } from "../layout.js";
5
6
  import { DEFAULT_LOOK } from "../looks.js";
6
7
  import { boundsOf, meshOf } from "../model/pieces.js";
8
+ import { moved } from "../model/moves.js";
7
9
  import { partEntry } from "../model/registry.js";
10
+ import { partIdOf, variantOf } from "../rockets.js";
8
11
  import { plumeMarkup } from "../draw/plume.js";
9
12
  import { defsOf, drawPart, perspective } from "../draw/svg.js";
10
13
  import { bellsOf, litBells, pickBells, plumeLength } from "../engines.js";
11
14
  import { toGlb } from "./glb.js";
12
- /** Raised when a description does not fit: its report says why. */
15
+ /**
16
+ * Raised by `build` when a description does not fit. Its `report` says why, in the same words `check` gives.
17
+ *
18
+ * @param report The report of the fit check that failed.
19
+ * @example
20
+ * ```ts
21
+ * import { build, FitError } from "@johnmorrisdotca/uchusen-rockets/build";
22
+ *
23
+ * try {
24
+ * build({ rocket: "starship", variant: "v3", parts: { booster: "starship-v3-ship" } });
25
+ * } catch (error) {
26
+ * if (error instanceof FitError) console.log(error.report.issues[0]?.message);
27
+ * }
28
+ * ```
29
+ */
13
30
  export class FitError extends Error {
14
31
  report;
15
32
  constructor(report) {
@@ -42,10 +59,20 @@ const widthOf = (id) => {
42
59
  const { min, max } = made(id).bounds;
43
60
  return Math.max(Math.abs(min[0]), Math.abs(max[0]), Math.abs(min[2]), Math.abs(max[2]));
44
61
  };
62
+ /** What colour a part's flame is: the kind its engine's page says it burns, found by the engine's name in the data of a variant the part is true for. */
63
+ function flameKindOf(part) {
64
+ for (const variantId of part.trueTo) {
65
+ const variant = variantOf(part.rocket, variantId);
66
+ for (const [slot, engines] of Object.entries(variant?.facts.engines ?? {}))
67
+ if (partIdOf(part.rocket, variantId, slot) === part.id)
68
+ return propellantOf(part.rocket, engines.value)?.flame ?? "neutral";
69
+ }
70
+ return "neutral";
71
+ }
45
72
  function assembled(report) {
46
73
  const parts = report.parts.map(({ slot, part }) => {
47
74
  const { pieces, mesh, bounds } = made(part.id);
48
- return { slot, record: part, pieces, mesh, bounds };
75
+ return { slot, record: part, pieces, mesh, bounds, flame: flameKindOf(part) };
49
76
  });
50
77
  const slots = parts.map((part) => part.slot);
51
78
  const placesOf = (show) => placesFor(report.parts, cleanShow(show, slots), (part) => widthOf(part.id));
@@ -84,7 +111,7 @@ function assembled(report) {
84
111
  placesOf,
85
112
  flamesOf,
86
113
  glb: () => toGlb(parts.map((part) => ({ name: part.slot, mesh: part.mesh, at: placesOf("together").find((place) => place.slot === part.slot)?.at ?? [0, 0, 0] }))),
87
- svg: ({ look = DEFAULT_LOOK, view = DEFAULT_VIEW, show, engines = false } = {}) => {
114
+ svg: ({ look = DEFAULT_LOOK, view = DEFAULT_VIEW, show, engines = false, moves } = {}) => {
88
115
  const angle = typeof view === "string" ? VIEWS[cleanView(view)] : view;
89
116
  const places = placesOf(show).filter((place) => place.shown);
90
117
  // a flame drawn at full thrust is held in the picture; a count of engines is the timeline's, which has its own room
@@ -103,26 +130,63 @@ function assembled(report) {
103
130
  ? flamesOf(show).filter((bell) => places.some((place) => place.slot === bell.slot))
104
131
  : [];
105
132
  return wrap(ordered
106
- .map(({ place, part }) => drawPart(drawnPieces(part, together), place.at, cam, look) + plumeMarkup(flames.filter((bell) => bell.slot === place.slot), place.at, cam, look, part.slot))
133
+ .map(({ place, part }) => drawPart(moved(drawnPieces(part, together), moves?.[part.slot] ?? {}), place.at, cam, look) + plumeMarkup(flames.filter((bell) => bell.slot === place.slot), place.at, cam, look, part.slot, part.flame))
107
134
  .join(""), look, PICTURE_W, PICTURE_H, report.label);
108
135
  },
109
136
  };
110
137
  }
111
- /** What a drawing draws of a part: in a stack, what nests inside the part below is out of sight. */
138
+ /**
139
+ * What a drawing draws of a part: in a stack, what nests inside the part below is out of sight.
140
+ *
141
+ * @param part A built part: its record and its pieces.
142
+ * @param together True when the part stands in a stack, false when it is shown alone.
143
+ * @returns The pieces to draw. In a stack, what nests inside the part below is left out.
144
+ * @example
145
+ * ```ts
146
+ * import { build, drawnPieces } from "@johnmorrisdotca/uchusen-rockets/build";
147
+ *
148
+ * const [first] = build({ rocket: "falcon-9", variant: "block-5" }).parts;
149
+ * if (first) console.log(drawnPieces(first, true).length, drawnPieces(first, false).length);
150
+ * ```
151
+ */
112
152
  export function drawnPieces(part, together) {
113
153
  const joint = part.record.joints.bottom?.y ?? 0;
114
154
  if (!together || joint <= 0)
115
155
  return part.pieces;
116
156
  return part.pieces.filter((piece) => !(piece.kind === "lathe" && piece.under && Math.max(...piece.profile.map(([, y]) => y)) <= joint + 0.01));
117
157
  }
118
- /** A rocket from a description. Throws FitError when it does not go together. */
158
+ /**
159
+ * A rocket from a description. Throws FitError when it does not go together.
160
+ *
161
+ * @param description The rocket, its variant and any other part in a slot.
162
+ * @returns The built rocket. It throws a `FitError` when the description does not go together.
163
+ * @example
164
+ * ```ts
165
+ * import { build } from "@johnmorrisdotca/uchusen-rockets/build";
166
+ *
167
+ * const rocket = build({ rocket: "falcon-heavy" });
168
+ * console.log(rocket.report.label, rocket.glb().bytes.length > 1000);
169
+ * ```
170
+ */
119
171
  export function build(description) {
120
172
  const report = check(description);
121
173
  if (!report.ok)
122
174
  throw new FitError(report);
123
175
  return assembled(report);
124
176
  }
125
- /** One part on its own, by its id. */
177
+ /**
178
+ * One part on its own, by its id.
179
+ *
180
+ * @param id A part's id, as `PART_ENTRIES` lists it.
181
+ * @returns The part's record and bounds, a function for its `.glb` and one for its drawing. It throws for an id that is not a part.
182
+ * @example
183
+ * ```ts
184
+ * import { buildPart } from "@johnmorrisdotca/uchusen-rockets/build";
185
+ *
186
+ * const raptor = buildPart("raptor-vacuum");
187
+ * console.log(raptor.record.name, raptor.bounds.max);
188
+ * ```
189
+ */
126
190
  export function buildPart(id) {
127
191
  const entry = partEntry(id);
128
192
  if (!entry)
@@ -1,6 +1,15 @@
1
1
  import { type ViewName } from "../framing.ts";
2
2
  import { type LookName } from "../looks.ts";
3
- /** Comparison shapes: plain round sizes, for scale, not facts about anything. */
3
+ /**
4
+ * Comparison shapes: plain round sizes, for scale, not facts about anything.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { COMPARES } from "@johnmorrisdotca/uchusen-rockets/build";
9
+ *
10
+ * console.log(COMPARES.person.height, COMPARES.building.height);
11
+ * ```
12
+ */
4
13
  export declare const COMPARES: {
5
14
  readonly person: {
6
15
  readonly name: "A person, 1.8 m";
@@ -13,13 +22,46 @@ export declare const COMPARES: {
13
22
  readonly width: 18;
14
23
  };
15
24
  };
25
+ /**
26
+ * The name of a comparison shape that can stand in a lineup: `person` or `building`.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * import type { CompareId } from "@johnmorrisdotca/uchusen-rockets/build";
31
+ *
32
+ * const id: CompareId = "person";
33
+ * console.log(id);
34
+ * ```
35
+ */
16
36
  export type CompareId = keyof typeof COMPARES;
37
+ /**
38
+ * One thing standing in a lineup: its key (`starship:v3` or a comparison's name), its label and its height in metres.
39
+ *
40
+ * @example
41
+ * ```ts
42
+ * import { lineupSvg, type LineupItem } from "@johnmorrisdotca/uchusen-rockets/build";
43
+ *
44
+ * const items: LineupItem[] = lineupSvg({ items: ["starship:v3", "person"], unit: 3 }).items;
45
+ * console.log(items.map((item) => item.label));
46
+ * ```
47
+ */
17
48
  export interface LineupItem {
18
49
  /** "starship:v3" or a comparison's name. */
19
50
  key: string;
20
51
  label: string;
21
52
  height: number;
22
53
  }
54
+ /**
55
+ * What a lineup is asked for: the items by key, how many pixels make a metre, and optionally the look, the view, what to show, a ruler and labels.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * import { lineupSvg, type LineupOptions } from "@johnmorrisdotca/uchusen-rockets/build";
60
+ *
61
+ * const options: LineupOptions = { items: ["starship:v2", "starship:v3", "building"], unit: 3, ruler: true, labels: true };
62
+ * console.log(lineupSvg(options).width);
63
+ * ```
64
+ */
23
65
  export interface LineupOptions {
24
66
  /** "starship:v1,starship:v3,person,building". */
25
67
  items: readonly string[];
@@ -31,7 +73,19 @@ export interface LineupOptions {
31
73
  ruler?: boolean;
32
74
  labels?: boolean;
33
75
  }
34
- /** The whole picture as SVG text. Unknown items are left out. */
76
+ /**
77
+ * The whole picture as SVG text. Unknown items are left out.
78
+ *
79
+ * @param options The items and how they are drawn.
80
+ * @returns The picture as SVG text, the items it holds, and its width and height.
81
+ * @example
82
+ * ```ts
83
+ * import { lineupSvg } from "@johnmorrisdotca/uchusen-rockets/build";
84
+ *
85
+ * const picture = lineupSvg({ items: ["falcon-9:block-5", "falcon-heavy:falcon-heavy", "person"], unit: 4, ruler: true });
86
+ * console.log(picture.items.length, picture.svg.startsWith("<svg"));
87
+ * ```
88
+ */
35
89
  export declare function lineupSvg(options: LineupOptions): {
36
90
  svg: string;
37
91
  items: LineupItem[];
@@ -6,7 +6,16 @@ import { DEFAULT_LOOK, LOOKS } from "../looks.js";
6
6
  import { defsOf, drawPart, orthographic } from "../draw/svg.js";
7
7
  import { MATERIALS } from "../model/materials.js";
8
8
  import { variantOf } from "../rockets.js";
9
- /** Comparison shapes: plain round sizes, for scale, not facts about anything. */
9
+ /**
10
+ * Comparison shapes: plain round sizes, for scale, not facts about anything.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { COMPARES } from "@johnmorrisdotca/uchusen-rockets/build";
15
+ *
16
+ * console.log(COMPARES.person.height, COMPARES.building.height);
17
+ * ```
18
+ */
10
19
  export const COMPARES = {
11
20
  person: { name: "A person, 1.8 m", height: 1.8, width: 0.5 },
12
21
  building: { name: "A ten-storey building, 30 m", height: 30, width: 18 },
@@ -33,7 +42,19 @@ function comparison(id, x, base, unit, line, fill) {
33
42
  floors += `M${n(x)} ${n(base - (h * i) / 10)}h${n(w)}`;
34
43
  return `<rect x="${n(x)}" y="${n(base - h)}" width="${n(w)}" height="${n(h)}" fill="${fill}" stroke="${line}" stroke-width=".9"/><path d="${floors}" stroke="${line}" stroke-width=".35" opacity=".6"/>`;
35
44
  }
36
- /** The whole picture as SVG text. Unknown items are left out. */
45
+ /**
46
+ * The whole picture as SVG text. Unknown items are left out.
47
+ *
48
+ * @param options The items and how they are drawn.
49
+ * @returns The picture as SVG text, the items it holds, and its width and height.
50
+ * @example
51
+ * ```ts
52
+ * import { lineupSvg } from "@johnmorrisdotca/uchusen-rockets/build";
53
+ *
54
+ * const picture = lineupSvg({ items: ["falcon-9:block-5", "falcon-heavy:falcon-heavy", "person"], unit: 4, ruler: true });
55
+ * console.log(picture.items.length, picture.svg.startsWith("<svg"));
56
+ * ```
57
+ */
37
58
  export function lineupSvg(options) {
38
59
  const { items, unit, look = DEFAULT_LOOK, view = "front", ruler = true, labels = true } = options;
39
60
  const angle = VIEWS[cleanView(view)];