axstack 0.36.1 → 0.37.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/README.md +3 -2
  2. package/bin/axstack.js +4 -18
  3. package/docs/getting-started.md +6 -6
  4. package/docs/installation.md +20 -41
  5. package/docs/workflows.md +12 -9
  6. package/package.json +1 -1
  7. package/profiles/presets/claude-only.json +3 -3
  8. package/profiles/presets/codex-only.json +3 -3
  9. package/profiles/presets/mixed.json +5 -4
  10. package/skills/axstack/references/role-roster.md +5 -3
  11. package/skills/axstack/references/ui-verification.md +6 -10
  12. package/skills/axstack-anatomy/ANATOMY.md +172 -0
  13. package/skills/axstack-anatomy/CHANGELOG.md +70 -0
  14. package/skills/axstack-anatomy/LICENSE +21 -0
  15. package/skills/axstack-anatomy/PROVENANCE.md +21 -0
  16. package/skills/axstack-anatomy/README.md +316 -0
  17. package/skills/axstack-anatomy/SKILL.md +74 -0
  18. package/skills/axstack-anatomy/SOURCE-PROOF.json +107 -0
  19. package/skills/axstack-anatomy/examples/arcade-cabinet/arcade-cabinet.css +44 -0
  20. package/skills/axstack-anatomy/examples/arcade-cabinet/build.mjs +714 -0
  21. package/skills/axstack-anatomy/examples/arcade-cabinet/live.js +597 -0
  22. package/skills/axstack-anatomy/examples/arcade-cabinet/parts.mjs +89 -0
  23. package/skills/axstack-anatomy/examples/arcade-cabinet/screen.mjs +114 -0
  24. package/skills/axstack-anatomy/examples/desk-computer/build.mjs +435 -0
  25. package/skills/axstack-anatomy/examples/dial-indicator/build.mjs +275 -0
  26. package/skills/axstack-anatomy/examples/raptor-engine/build.mjs +938 -0
  27. package/skills/axstack-anatomy/examples/raptor-engine/live.js +907 -0
  28. package/skills/axstack-anatomy/examples/raptor-engine/raptor.css +359 -0
  29. package/skills/axstack-anatomy/examples/ripple-tank/build.mjs +558 -0
  30. package/skills/axstack-anatomy/examples/ripple-tank/live.js +579 -0
  31. package/skills/axstack-anatomy/examples/ripple-tank/model.mjs +131 -0
  32. package/skills/axstack-anatomy/examples/ripple-tank/ripple.css +103 -0
  33. package/skills/axstack-anatomy/examples/spot-plate/art.tsx +110 -0
  34. package/skills/axstack-anatomy/examples/spot-plate/drops.ts +541 -0
  35. package/skills/axstack-anatomy/examples/spot-plate/fan.ts +84 -0
  36. package/skills/axstack-anatomy/examples/spot-plate/figure.tsx +25 -0
  37. package/skills/axstack-anatomy/examples/spot-plate/geometry.ts +280 -0
  38. package/skills/axstack-anatomy/examples/spot-plate/live.tsx +339 -0
  39. package/skills/axstack-anatomy/examples/spot-plate/rig.ts +230 -0
  40. package/skills/axstack-anatomy/examples/spot-plate/spot-plate.css +92 -0
  41. package/skills/axstack-anatomy/examples/test-rig/art.tsx +196 -0
  42. package/skills/axstack-anatomy/examples/test-rig/figure.tsx +23 -0
  43. package/skills/axstack-anatomy/examples/test-rig/geometry.ts +305 -0
  44. package/skills/axstack-anatomy/examples/test-rig/live.tsx +511 -0
  45. package/skills/axstack-anatomy/examples/test-rig/pose.ts +107 -0
  46. package/skills/axstack-anatomy/examples/test-rig/rig.ts +170 -0
  47. package/skills/axstack-anatomy/examples/test-rig/stretch.css +80 -0
  48. package/skills/axstack-anatomy/examples/test-rig/view.ts +99 -0
  49. package/skills/axstack-anatomy/examples/turning-dial/build.mjs +479 -0
  50. package/skills/axstack-anatomy/examples/turning-dial/dial.css +10 -0
  51. package/skills/axstack-anatomy/examples/turning-dial/live.js +342 -0
  52. package/skills/axstack-anatomy/examples/turning-dial/stress.mjs +269 -0
  53. package/skills/axstack-anatomy/kit/audit.mjs +1913 -0
  54. package/skills/axstack-anatomy/kit/canvas-painter.mjs +722 -0
  55. package/skills/axstack-anatomy/kit/gl-shared.mjs +184 -0
  56. package/skills/axstack-anatomy/kit/gl.mjs +184 -0
  57. package/skills/axstack-anatomy/kit/iso-kit.mjs +674 -0
  58. package/skills/axstack-anatomy/kit/iso-kit.ts +842 -0
  59. package/skills/axstack-anatomy/kit/iso.css +106 -0
  60. package/skills/axstack-anatomy/kit/lathe.mjs +1184 -0
  61. package/skills/axstack-anatomy/kit/react/draw.tsx +205 -0
  62. package/skills/axstack-anatomy/kit/react/live.tsx +140 -0
  63. package/skills/axstack-anatomy/kit/tube.mjs +306 -0
  64. package/skills/axstack-anatomy/kit/turn-audit.mjs +1096 -0
  65. package/skills/axstack-anatomy/kit/turn-build.mjs +1355 -0
  66. package/skills/axstack-anatomy/kit/turn-fixed.mjs +3800 -0
  67. package/skills/axstack-anatomy/kit/turn.mjs +3610 -0
  68. package/skills/axstack-anatomy/references/3d.md +352 -0
  69. package/skills/axstack-anatomy/references/craft.md +325 -0
  70. package/skills/axstack-anatomy/references/kit.md +419 -0
  71. package/skills/axstack-anatomy/references/motion.md +165 -0
  72. package/skills/axstack-anatomy/references/react.md +42 -0
  73. package/skills/axstack-anatomy/references/verify.md +351 -0
  74. package/skills/axstack-anatomy/references/walkthrough.md +75 -0
  75. package/skills/axstack-anatomy/references/webgl.md +473 -0
  76. package/skills/axstack-anatomy/scripts/audit-affine-bounds.test.mjs +54 -0
  77. package/skills/axstack-anatomy/scripts/audit-cache.test.mjs +37 -0
  78. package/skills/axstack-anatomy/scripts/audit-clearance-envelope.test.mjs +39 -0
  79. package/skills/axstack-anatomy/scripts/audit-empty-clipping.test.mjs +38 -0
  80. package/skills/axstack-anatomy/scripts/audit-order-convergence.test.mjs +55 -0
  81. package/skills/axstack-anatomy/scripts/audit-path-origin.test.mjs +26 -0
  82. package/skills/axstack-anatomy/scripts/audit-profile.test.mjs +41 -0
  83. package/skills/axstack-anatomy/scripts/audit-projection.test.mjs +36 -0
  84. package/skills/axstack-anatomy/scripts/audit-slide-clearance.test.mjs +58 -0
  85. package/skills/axstack-anatomy/scripts/audit-svg-attributes.test.mjs +46 -0
  86. package/skills/axstack-anatomy/scripts/audit-taper.test.mjs +29 -0
  87. package/skills/axstack-anatomy/scripts/capture-selector.test.mjs +131 -0
  88. package/skills/axstack-anatomy/scripts/capture.mjs +228 -0
  89. package/skills/axstack-anatomy/scripts/capture.test.mjs +42 -0
  90. package/skills/axstack-anatomy/scripts/chrome-port.mjs +36 -0
  91. package/skills/axstack-anatomy/scripts/chrome-startup.test.mjs +38 -0
  92. package/skills/axstack-anatomy/scripts/drive-actions.test.mjs +46 -0
  93. package/skills/axstack-anatomy/scripts/drive.mjs +302 -0
  94. package/skills/axstack-anatomy/scripts/drive.test.mjs +72 -0
  95. package/skills/axstack-anatomy/scripts/enter-key.test.mjs +38 -0
  96. package/skills/axstack-anatomy/scripts/fidelity-cut.test.mjs +54 -0
  97. package/skills/axstack-anatomy/scripts/fidelity-tube-coverage.test.mjs +40 -0
  98. package/skills/axstack-anatomy/scripts/inline-kit.mjs +26 -0
  99. package/skills/axstack-anatomy/scripts/line-ends.mjs +306 -0
  100. package/skills/axstack-anatomy/scripts/lines-arcs.test.mjs +50 -0
  101. package/skills/axstack-anatomy/scripts/lines-bezier.test.mjs +28 -0
  102. package/skills/axstack-anatomy/scripts/lines-round.test.mjs +56 -0
  103. package/skills/axstack-anatomy/scripts/lines-rounded-rect.test.mjs +32 -0
  104. package/skills/axstack-anatomy/scripts/lines.mjs +31 -0
  105. package/skills/axstack-anatomy/scripts/lines.test.mjs +38 -0
  106. package/skills/axstack-anatomy/scripts/navigation.test.mjs +55 -0
  107. package/skills/axstack-anatomy/scripts/orbit-sampling.test.mjs +50 -0
  108. package/skills/axstack-anatomy/scripts/react-dots-opacity.test.mjs +51 -0
  109. package/skills/axstack-anatomy/scripts/react-readout-first-write.test.mjs +37 -0
  110. package/skills/axstack-anatomy/scripts/react-solid-seamless.test.mjs +47 -0
  111. package/skills/axstack-anatomy/scripts/react-visibility-target.test.mjs +38 -0
  112. package/skills/axstack-anatomy/scripts/turn-bench.mjs +74 -0
  113. package/skills/axstack-anatomy/scripts/turn-check.mjs +641 -0
  114. package/skills/axstack-anatomy/scripts/turn-fidelity.mjs +413 -0
  115. package/skills/axstack-anatomy/scripts/turn-group-reference.test.mjs +40 -0
  116. package/skills/axstack-anatomy/scripts/turn-split-owner.test.mjs +62 -0
  117. package/skills/axstack-anatomy/theme.js +56 -0
  118. package/skills/axstack-explain/SKILL.md +28 -15
  119. package/skills/axstack-explain/references/delivery.md +22 -0
  120. package/skills/{axstack-diagram → axstack-explain}/references/fidelity.md +1 -7
  121. package/skills/axstack-explain/references/inline-pages.md +8 -8
  122. package/skills/axstack-explain/references/visual-qa.md +3 -5
  123. package/skills/axstack-explain/references/whiteboard-defense.md +59 -0
  124. package/src/archify-legacy.js +70 -0
  125. package/src/capabilities.js +1 -29
  126. package/src/installer.js +11 -31
  127. package/skills/axstack-diagram/SKILL.md +0 -28
  128. package/skills/axstack-diagram/references/archify.md +0 -76
  129. package/src/archify-pin.js +0 -5
  130. package/src/archify.js +0 -151
@@ -0,0 +1,938 @@
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import * as k from "../../kit/iso-kit.mjs";
5
+ import * as G from "../../kit/lathe.mjs";
6
+ import { tubePieces, tubeSvg, crossingsOf } from "../../kit/tube.mjs";
7
+ import * as A from "../../kit/audit.mjs";
8
+ import { glCamera } from "../../kit/gl.mjs";
9
+ import { glScript } from "../../scripts/inline-kit.mjs";
10
+
11
+ const HERE = dirname(fileURLToPath(import.meta.url));
12
+ const THEME = process.argv.includes("--light") ? "light" : "dark";
13
+ const DEG = Math.PI / 180;
14
+
15
+ const W = 932;
16
+ const H = 900;
17
+ const AZIMUTH = 38;
18
+ const ELEVATION = 27;
19
+ const Z_EXIT = 172;
20
+ const TOP = 310;
21
+ const BEAM_Z = Z_EXIT + TOP + 23;
22
+ const BEAM_H = 28;
23
+ const DECK = { x: -150, y: -150, w: 300, d: 300, r: 10 };
24
+ const DECK_H = 15;
25
+ const FOOT_H = 8;
26
+ const COLUMN = 24;
27
+ const COLUMN_AT = [
28
+ [-124, 124],
29
+ [124, -124],
30
+ ];
31
+ const CONE_TIP = 32;
32
+ const CHAMBER_BAR = 300;
33
+ const SEA_LEVEL_TF = 230;
34
+ const EXIT_M = 1.3;
35
+ const BACK_TF = (101325 * Math.PI * (EXIT_M / 2) ** 2) / 9806.65;
36
+
37
+ const P = k.fitProjection(
38
+ [
39
+ ...k.boxCorners(DECK, -DECK_H - FOOT_H, 0),
40
+ [COLUMN_AT[0][0] - COLUMN, COLUMN_AT[0][1] + COLUMN, BEAM_Z + BEAM_H],
41
+ [COLUMN_AT[1][0] + COLUMN, COLUMN_AT[1][1] - COLUMN, BEAM_Z + BEAM_H],
42
+ [-190 / Math.SQRT2, 190 / Math.SQRT2, BEAM_Z + BEAM_H],
43
+ [190 / Math.SQRT2, -190 / Math.SQRT2, BEAM_Z + BEAM_H],
44
+ ],
45
+ W,
46
+ H,
47
+ { pad: 26, azimuth: AZIMUTH, elevation: ELEVATION },
48
+ );
49
+ const V = G.viewOf(P);
50
+ const KS = G.scaleOf(P);
51
+ const at = (point) => k.iso(point, P);
52
+ const S = (paths, style) => k.solidSvg(paths, style);
53
+ const Ln = (d, style) => k.lineSvg(d, style);
54
+ const Dots = (points, style) => k.dotsSvg(points, style);
55
+ const FADE = [0.04, 0.42];
56
+ const Faded = (list, style) => k.fadedSvg(list, style);
57
+
58
+ const polar = (deg, r, s, centre = [0, 0]) => [centre[0] + r * Math.cos(deg * DEG), centre[1] + r * Math.sin(deg * DEG), Z_EXIT + s];
59
+ const dir = (deg) => [Math.cos(deg * DEG), Math.sin(deg * DEG), 0];
60
+ const ENGINE = G.frameOf([0, 0, Z_EXIT], [0, 0, 1], [1, 0, 0], [0, 1, 0]);
61
+ const FTP_DEG = -20;
62
+ const FTP_AT = [72 * Math.cos(FTP_DEG * DEG), 72 * Math.sin(FTP_DEG * DEG)];
63
+ const FTP = G.frameOf([FTP_AT[0], FTP_AT[1], Z_EXIT], [0, 0, 1], [1, 0, 0], [0, 1, 0]);
64
+ const ftp = (deg, r, s) => G.pointOf(FTP, s, r, deg * DEG);
65
+ const DECKF = G.frameOf([0, 0, 0], [0, 0, 1], [1, 0, 0], [0, 1, 0]);
66
+ const out = (deg, r, z) => [r * Math.cos(deg * DEG), r * Math.sin(deg * DEG), z];
67
+ const LUGS = [
68
+ [135, 185],
69
+ [-45, -45],
70
+ ];
71
+
72
+ const PIECES = {
73
+ gimbal: { name: "Gimbal and thrust puck", say: "gimbal · carries 230 tf into the stand", move: [0, 0, 0] },
74
+ otp: { name: "Oxygen turbopump", say: "oxygen turbopump · spun by oxygen-rich gas", move: [0, 0, -26] },
75
+ orpb: { name: "Oxygen-rich preburner", say: "oxygen-rich preburner · all the oxygen", move: [0, 0, -52] },
76
+ lox: { name: "Oxygen duct and main valve", say: "oxygen duct · pump to preburner", move: out(166, 52, -12) },
77
+ chamber: { name: "Injector and main chamber", say: "main chamber · 300 bar · no igniter", move: [0, 0, -80] },
78
+ nozzle: { name: "Nozzle", say: "nozzle · 1.3 m exit · methane cooled", move: [0, 0, -120] },
79
+ ftp: { name: "Methane turbopump and fuel-rich preburner", say: "methane turbopump · spun by fuel-rich gas", move: out(FTP_DEG, 70, -46) },
80
+ hotgas: { name: "Hot-gas manifold", say: "hot-gas manifold · fuel-rich gas to the injector", move: out(FTP_DEG, 36, -62) },
81
+ downcomer: { name: "Downcomer and main fuel valve", say: "downcomer · methane to the cooling jacket", move: out(40, 66, -84) },
82
+ plumbing: { name: "Plumbing", say: "plumbing", move: [0, 0, 0] },
83
+ controller: { name: "Engine controller and harness", say: "engine controller", move: out(70, 70, -28) },
84
+ stand: { name: "Stand", say: "", move: [0, 0, 0] },
85
+ };
86
+
87
+ const STAGGER = ["nozzle", "downcomer", "controller", "plumbing", "lox", "ftp", "hotgas", "chamber", "orpb", "otp", "gimbal", "stand"];
88
+ const SPRING = { stagger: 0.035, stiffness: 64, damping: 14.2, substeps: 4 };
89
+ const R = A.recorder(P, { order: STAGGER, spring: SPRING });
90
+ const { items, nameFor } = R;
91
+ const record = R.solid;
92
+ const rideOf = (piece, move) => (move ? Object.keys(PIECES).find((id) => PIECES[id].move === move) ?? piece : piece);
93
+ function put(piece, ref, svg, { bias = 0, move, ride, shapes = [], name, route, chunk } = {}) {
94
+ R.put({ piece, ride: ride ?? rideOf(piece, move), at: ref, bias, svg, move: move ?? PIECES[piece].move, shapes, name: name ?? nameFor(`${piece}.item`), route, chunk });
95
+ }
96
+ function tubeItems(piece, route, r, { name, tone = "hi", bias = 0, move, maxLength, spacing, closed = false, owner, touch, bundle, limp = false, breaks = [], gaps = [], caps = true, ends = [true, true], rims, free = [false, false] } = {}) {
97
+ const id = nameFor(name ?? `${piece}.tube`);
98
+ R.route(id, route, r, { owner: owner ?? id, touch, bundle, limp, gaps, closed, ends, rims, free, piece, move: move ?? PIECES[piece].move, ride: rideOf(piece, move) });
99
+ tubePieces(route, r, P, { maxLength, spacing, closed, breaks, gaps, caps }).forEach((part, index) =>
100
+ put(piece, part.mid, tubeSvg(part, { tone }), { bias, move, name: `${id}:${index}`, route: id, chunk: index, shapes: [A.tube(part.points, part.radii)] }),
101
+ );
102
+ return id;
103
+ }
104
+
105
+ const ex = (F, s) => [F.o[0], F.o[1], F.o[2] + s];
106
+
107
+ function turned(piece, F, profile, style = {}, { extra = "", inner = "", bias = 0, move, rims, bevel = 0, steps, name, owner, smooth = profile.some((point) => point[2]) } = {}) {
108
+ const paths = G.solidOf(profile, F, P, { bevel, smooth: smooth === true ? { rows: Math.max(...profile.map(([, r]) => r)) < 20 ? 3 : 5 } : smooth, ...(rims ? { rims } : {}), ...(steps ? { steps } : {}) });
109
+ const mid = (profile[0][0] + profile[profile.length - 1][0]) / 2;
110
+ const id = nameFor(name ?? `${piece}.turned${Math.round(profile[0][0])}`);
111
+ const shape = record(A.solid(F, profile, { name: id, owner, curve: smooth && !smooth.slope ? profile : undefined }));
112
+ put(piece, G.add3(F.o, G.mul3(F.a, mid)), S(paths, { tone: "hi", ...style, inner: inner ? Ln(inner, { tone: "faint" }) : "" }) + extra, { bias, move, name: id, shapes: [shape] });
113
+ return shape;
114
+ }
115
+
116
+ function band(piece, F, s0, s1, r, style = {}, options = {}) {
117
+ return turned(piece, F, [
118
+ [s0, r],
119
+ [s1, r],
120
+ ], style, options);
121
+ }
122
+
123
+ function flangeAt(piece, centre, axis, r, thick, { bolts = 8, tone = "hi", move, bias = 0, boltR, name, owner } = {}) {
124
+ const F = G.frameAlong(centre, axis);
125
+ const id = nameFor(name ?? `${piece}.flange`);
126
+ const paths = G.disc(-thick / 2, thick / 2, r, F, P);
127
+ const faces = [];
128
+ const towards = G.dot3(F.a, V) > 0 ? thick / 2 : -thick / 2;
129
+ if (Math.abs(G.dot3(F.a, V)) > 0.12 && bolts) faces.push(Dots(G.dotsOf(F, towards, boltR ?? r * 0.74, bolts, P, { all: true }), { size: 0.42, tone: "mid" }));
130
+ put(piece, centre, S(paths, { tone }) + faces.join(""), { bias, move, name: id, shapes: [record(A.disc(F, -thick / 2, thick / 2, r, { name: id, owner }))] });
131
+ }
132
+
133
+ function pipe(piece, control, r, { bend = 2.6, tone = "hi", flanges = [true, true], flangeR = 1.55, coupling = false, bands = [], bellows = null, valve = null, move, bias = 0, maxLength = 26, spacing, name, touch, breaks = [], gaps: extra = [], free } = {}) {
134
+ const flangeRs = [flangeR].flat().length > 1 ? [flangeR].flat() : [flangeR, flangeR].flat();
135
+ if (coupling) flanges = [flanges[0], false];
136
+ const line = G.fillet(control, Math.max(r * bend, 3), r > 3 ? 24 : 12, { P, tube: r });
137
+ const total = G.pathLength(line);
138
+ const collars = [];
139
+ [[0, -1], [total, 1]].forEach(([along, sign], index) => {
140
+ if (!flanges[index]) return;
141
+ const thick = Math.max(0.9, r * 0.55);
142
+ const tangent = G.tangentAlong(line, along);
143
+ collars.push({ kind: "flange", centre: G.add3(G.pointAlong(line, along), G.mul3(tangent, (-sign * thick) / 2)), tangent, radius: r * flangeRs[index], thick, gap: sign < 0 ? [0, thick] : [total - thick, total] });
144
+ });
145
+ if (coupling) {
146
+ const thick = Math.max(1.6, r * 1.4);
147
+ collars.push({ kind: "coupling", centre: G.pointAlong(line, total - thick / 2), tangent: G.tangentAlong(line, total), radius: r * 1.5, thick, gap: [total - thick, total] });
148
+ }
149
+ const ring = (along, radius, thick, kind) => collars.push({ kind, centre: G.pointAlong(line, along), tangent: G.tangentAlong(line, along), radius, thick, gap: [along - thick / 2, along + thick / 2] });
150
+ for (const share of bands) ring(total * share, r * 1.18, Math.max(0.8, r * 0.7), "band");
151
+ if (bellows) {
152
+ const [from, to, count] = bellows;
153
+ for (let index = 0; index < count; index++) ring(total * (from + ((to - from) * index) / (count - 1)), r * 1.26, Math.max(0.6, r * 0.3), "bellows");
154
+ }
155
+ const gaps = [...collars.map(({ gap }) => gap), ...extra];
156
+ if (valve) {
157
+ const along = valve[0] > 1 ? valve[0] : total * valve[0];
158
+ gaps.push([along - valve[1] * 2.4, along + valve[1] * 2.4]);
159
+ }
160
+ const owner = tubeItems(piece, line, r, { name: name ?? `${piece}.pipe`, tone, bias, move, maxLength, spacing: spacing ?? Math.max(0.8, Math.min(2.4, r * 0.18)), touch, gaps, breaks, free, rims: flanges.map((flanged, index) => (flanged ? r * flangeRs[index] : index && coupling ? r * 1.5 : r)) });
161
+ for (const { kind, centre, tangent, radius, thick } of collars)
162
+ flangeAt(piece, centre, tangent, radius, thick, { bolts: kind === "flange" && r > 3 ? 8 : 0, tone: kind === "flange" || kind === "coupling" ? tone : "mid", move, bias, name: `${owner}.${kind}`, owner });
163
+ line.owner = owner;
164
+ return line;
165
+ }
166
+
167
+ function boxAt(piece, plan, z, h, style = {}, { extra = "", bias = 0, move, bevel = 0.5, steps = 3, name, owner } = {}) {
168
+ const centre = [plan.x + plan.w / 2, plan.y + plan.d / 2, z + h / 2];
169
+ const id = nameFor(name ?? `${piece}.box`);
170
+ put(piece, centre, S(k.slabOf(plan, z, h, P, steps, bevel), { tone: "hi", ...style }) + extra, { bias, move, name: id, shapes: [record(A.box([plan.x, plan.y, z], [plan.x + plan.w, plan.y + plan.d, z + h], { name: id, owner }))] });
171
+ }
172
+
173
+ function valveOn(piece, line, share, r, { move, tone = "hi", name, side = [0, 0, 1] } = {}) {
174
+ const total = G.pathLength(line);
175
+ const along = share > 1 ? share : total * share;
176
+ const centre = G.pointAlong(line, along);
177
+ const tangent = G.tangentAlong(line, along);
178
+ const F = G.frameAlong(centre, tangent);
179
+ const body = r * 1.85;
180
+ const id = nameFor(name ?? `${line.owner}.valve`);
181
+ const owner = line.owner;
182
+ const crown = body * 1.08;
183
+ const bulge = (x) => r * 1.45 + (crown - r * 1.45) * Math.sqrt(Math.max(0, 1 - (x / (r * 1.6)) ** 2));
184
+ const bulgeSlope = (x) => {
185
+ if (Math.abs(x) >= r * 1.6 - 1e-9) return 0;
186
+ const t = Math.max(-0.995, Math.min(0.995, x / (r * 1.6)));
187
+ return (-(crown - r * 1.45) * t) / (r * 1.6 * Math.sqrt(1 - t * t));
188
+ };
189
+ const meridian = [
190
+ [-r * 2.4, 0],
191
+ [-r * 2.4, r * 1.45],
192
+ ...Array.from({ length: 17 }, (_, index) => {
193
+ const x = r * 1.6 * Math.sin(((index - 8) / 8) * (Math.PI / 2));
194
+ return [x, bulge(x), index && index < 16 ? 1 : 0];
195
+ }),
196
+ [r * 2.4, r * 1.45],
197
+ [r * 2.4, 0],
198
+ ];
199
+ put(
200
+ piece,
201
+ centre,
202
+ S(G.lathe(meridian, F, P, { steps: 36, smooth: { slope: bulgeSlope, rows: 3 } }), { tone }) + Dots([...G.dotsOf(F, -r * 2.4, r * 1.2, 8, P, { all: true }), ...G.dotsOf(F, r * 2.4, r * 1.2, 8, P, { all: true })].filter((_, index) => index % 2 === 0), { size: 0.4, tone: "mid" }),
203
+ { move, name: id, shapes: [record(A.body(F, meridian, { name: id, owner }))] },
204
+ );
205
+ const up = G.unit3(side);
206
+ const NF = G.frameAlong(centre, up);
207
+ const s0 = body * 1.08 - 0.25;
208
+ const s1 = body * 1.08 + body * 0.45;
209
+ put(piece, G.add3(centre, G.mul3(up, (s0 + s1) / 2)), S(G.disc(s0, s1, r * 0.5, NF, P), { tone }), { move, bias: 0.2, name: `${id}.neck`, shapes: [record(A.disc(NF, s0, s1, r * 0.5, { name: `${id}.neck`, owner }))] });
210
+ if (up[2] > 0.99) {
211
+ const top = G.add3(centre, [0, 0, s1]);
212
+ const plan = { x: top[0] - r * 1.5, y: top[1] - r * 1.5, w: r * 3, d: r * 3, r: 0.8 };
213
+ const lid = top[2] + r * 2.2;
214
+ boxAt(piece, plan, top[2], r * 2.2, { tone }, {
215
+ move,
216
+ bias: 0.3,
217
+ name: `${id}.actuator`,
218
+ owner,
219
+ extra: Ln(k.planOutline(k.insetPlan(plan, 0.8), lid, P, 2), { tone: "faint" }) + Dots([at([plan.x + 1.2, plan.y + 1.2, lid]), at([plan.x + plan.w - 1.2, plan.y + plan.d - 1.2, lid])], { size: 0.4 }),
220
+ });
221
+ const port = [top[0], top[1], lid];
222
+ return { port, approach: G.add3(port, [0, 0, 5]), lid };
223
+ }
224
+ const can = [[s1, 0], [s1, r * 1.5], [s1 + r * 2.2, r * 1.5], [s1 + r * 2.2, 0]];
225
+ const end = s1 + r * 2.2;
226
+ put(piece, G.add3(centre, G.mul3(up, (s1 + end) / 2)), S(G.lathe(can, NF, P, { steps: 32, bevel: 0.6 }), { tone }) + Ln(G.circleOf(NF, end, r * 1.1, P, 24), { tone: "faint" }) + Dots(G.dotsOf(NF, end, r * 1.25, 4, P, { all: true, phase: Math.PI / 4 }), { size: 0.4 }), { move, bias: 0.3, name: `${id}.actuator`, shapes: [record(A.body(NF, can, { name: `${id}.actuator`, owner }))] });
227
+ const port = G.add3(centre, G.mul3(up, end));
228
+ return { port, approach: G.add3(port, G.mul3(up, 5)) };
229
+ }
230
+
231
+ function ballAt(piece, centre, r, { move, tone = "mid", bias = 0, name, owner } = {}) {
232
+ const F = G.frameAlong(centre, [0, 0, 1]);
233
+ const id = nameFor(name ?? `${piece}.ball`);
234
+ put(piece, centre, S(G.sphereOf(centre, r, P, { steps: G.stepsFor(r, 32) }), { tone }), { move, bias, name: id, shapes: [record(A.ball(centre, r, { name: id, owner }))] });
235
+ }
236
+
237
+ function elbow(piece, face, normal, faceR, toward, ball, rim, { name, owner } = {}) {
238
+ const centre = G.add3(face, G.mul3(normal, Math.sqrt(ball ** 2 - faceR ** 2)));
239
+ ballAt(piece, centre, ball, { tone: "hi", name, owner });
240
+ const axis = G.unit3(G.sub3(toward, centre));
241
+ return G.add3(centre, G.mul3(axis, Math.sqrt(ball ** 2 - rim ** 2)));
242
+ }
243
+
244
+ const sideOn = (d) => G.unit3(G.cross3(G.unit3(G.sub3(V, G.mul3(d, G.dot3(V, d)))), d));
245
+
246
+ function boss(piece, centre, outward, r, length, { move, tone = "hi", bias = 0.1, name, host, ring = true, owner } = {}) {
247
+ if (host) return padOn(piece, host, centre, outward, r, length, { move, tone, bias, name: name ?? `${piece}.boss`, ring, owner });
248
+ const F = G.frameAlong(centre, outward);
249
+ const id = nameFor(name ?? `${piece}.boss`);
250
+ put(piece, G.add3(centre, G.mul3(F.a, length / 2)), S(G.disc(0, length, r, F, P), { tone }) + (ring && G.dot3(F.a, V) > 0.15 ? Ln(G.circleOf(F, length, r * 0.55, P, 16), { tone: "faint" }) : ""), { bias, move, name: id, shapes: [record(A.disc(F, 0, length, r, { name: id, owner }))] });
251
+ return G.add3(centre, G.mul3(F.a, length));
252
+ }
253
+
254
+ function padOn(piece, host, point, outward, a, h, { move, name, tone = "hi", bias = 0.1, owner, ring = false } = {}) {
255
+ const hosts = [host].flat();
256
+ const inside = (p) => hosts.some((shape) => A.sdf(shape, p) < 0);
257
+ const axis = G.unit3(outward);
258
+ const F = G.frameAlong(point, axis);
259
+ const start = (angle) => {
260
+ let lo = -2.5 * a;
261
+ let hi = h;
262
+ if (!inside(G.pointOf(F, lo, a, angle))) return 0;
263
+ for (let step = 0; step < 40; step++) {
264
+ const mid = (lo + hi) / 2;
265
+ if (inside(G.pointOf(F, mid, a, angle))) lo = mid;
266
+ else hi = mid;
267
+ }
268
+ return hi;
269
+ };
270
+ const low = Math.min(...Array.from({ length: 48 }, (_, index) => start((index / 48) * Math.PI * 2)));
271
+ const id = nameFor(name ?? `${piece}.pad`);
272
+ const face = ring && G.dot3(axis, V) > 0.15 ? Ln(G.circleOf(F, h, a * 0.55, P, 16), { tone: "faint" }) : "";
273
+ put(piece, G.add3(point, G.mul3(axis, h / 2)), S(G.saddleOf(F, a, start, h, P), { tone }) + face, { bias, move, name: id, shapes: [record(A.disc(F, low - 0.05, h, a, { name: id, owner, seated: true, cut: hosts }))] });
274
+ return G.add3(point, G.mul3(axis, h));
275
+ }
276
+
277
+ const CABLES = [];
278
+ function cable(piece, points, { move, bias = 0.2, r = 0.62, bend = 6, name, bundle, touch, clamps = [], shells = [false, false], free } = {}) {
279
+ const route = G.fillet(points, bend, 12, { P, tube: r });
280
+ CABLES.push({ piece, route });
281
+ const total = G.pathLength(route);
282
+ const SHELL = 2.2;
283
+ const gaps = [...clamps.flatMap(({ centre, axis, radius }) => crossingsOf(route, centre, axis, radius).map((at) => [at - CLAMP_HALF - 0.05, at + CLAMP_HALF + 0.05])), ...(shells[0] ? [[0, SHELL]] : []), ...(shells[1] ? [[total - SHELL, total]] : [])];
284
+ const id = tubeItems(piece, route, r, { name: name ?? `${piece}.cable`, tone: "mid", bias, move, maxLength: 14, spacing: 1, bundle, touch, gaps, free, limp: true, rims: shells.map((on) => (on ? r * 2 : r)) });
285
+ [0, 1].forEach((index) => {
286
+ const end = index ? route[route.length - 1] : route[0];
287
+ if (!shells[index]) {
288
+ put(piece, end, Dots([at(end)], { size: 0.5, tone: "hi" }), { bias: bias + 0.15, move, name: `${id}.end`, shapes: [A.ball(end, 0.5 / KS)] });
289
+ return;
290
+ }
291
+ const tangent = G.tangentAlong(route, index ? total : 0);
292
+ const F = G.frameAlong(end, tangent);
293
+ const [s0, s1] = index ? [-SHELL, 0] : [0, SHELL];
294
+ const outer = index ? s0 : s1;
295
+ put(piece, G.add3(end, G.mul3(tangent, (s0 + s1) / 2)), S(G.disc(s0, s1, r * 2, F, P, { bevel: 0.25 }), { tone: "mid" }) + (G.dot3(G.mul3(F.a, index ? -1 : 1), V) > 0.2 ? Ln(G.circleOf(F, outer, r * 1.1, P, 12), { tone: "faint" }) : ""), { bias: bias + 0.1, move, name: `${id}.shell`, shapes: [record(A.disc(F, s0, s1, r * 2, { name: `${id}.shell`, owner: id }))] });
296
+ });
297
+ return route;
298
+ }
299
+
300
+ const CLAMP_HALF = 0.9;
301
+ function clampAt(piece, centre, axis, r, { move, bias = 0.3, name = `${piece}.clamp`, bundle } = {}) {
302
+ const F = G.frameAlong(centre, axis);
303
+ const id = nameFor(name);
304
+ put(piece, centre, S(G.disc(-CLAMP_HALF, CLAMP_HALF, r, F, P), { tone: "mid" }), { bias, move, name: id, shapes: [record(A.disc(F, -CLAMP_HALF, CLAMP_HALF, r, { name: id, anchor: false, bundle, fitting: true }))] });
305
+ }
306
+
307
+
308
+ const back = [];
309
+ back.push(`<path class="iso-halo" d="${k.haloOf(DECK, -DECK_H - FOOT_H, DECK_H, P)}"/>`);
310
+ const FEET = [
311
+ [-132, -132],
312
+ [132, -132],
313
+ [-132, 132],
314
+ [132, 132],
315
+ [0, -132],
316
+ [0, 132],
317
+ [-132, 0],
318
+ [132, 0],
319
+ ].sort((a, b) => k.depthOf([a[0], a[1], 0], P) - k.depthOf([b[0], b[1], 0], P));
320
+ for (const [x, y] of FEET) {
321
+ back.push(S(k.cylinder(x, y, 10, -DECK_H - FOOT_H, 2, P, 28), { tone: "lo" }));
322
+ back.push(S(k.cylinder(x, y, 7.5, -DECK_H - FOOT_H + 2, FOOT_H - 2, P, 28), { tone: "mid" }));
323
+ }
324
+ back.push(S(k.slabOf(DECK, -DECK_H, DECK_H, P, 8, 1.6), { tone: "mid" }));
325
+ const FRAME = { owner: "stand.frame" };
326
+ record(A.box([DECK.x, DECK.y, -DECK_H], [DECK.x + DECK.w, DECK.y + DECK.d, 0], { name: "stand.deck", ...FRAME }));
327
+ back.push(Ln(k.planOutline(k.insetPlan(DECK, 7), 0, P), { tone: "lo" }));
328
+ const DECK_SCREWS = k.corners(DECK, 15);
329
+ back.push(Ln(DECK_SCREWS.map(([x, y]) => k.ring(x, y, 3, 0, P, 18)).join(""), { tone: "lo" }));
330
+ back.push(Dots(DECK_SCREWS.map(([x, y]) => at([x, y, 0])), { size: 0.55 }));
331
+ back.push(Ln(k.sideTicks(DECK.x + 20, DECK.x + DECK.w - 20, 10, 5, DECK.x + DECK.w, 0, [2.4, 4.6], P, "y").minor, { tone: "lo" }));
332
+ back.push(Ln(k.sideTicks(DECK.x + 20, DECK.x + DECK.w - 20, 10, 5, DECK.x + DECK.w, 0, [2.4, 4.6], P, "y").major, { tone: "mid" }));
333
+
334
+ const GRATE = { x: -98, y: -98, w: 196, d: 196, r: 6 };
335
+ back.push(Ln(k.planOutline(GRATE, 0, P), { tone: "lo" }));
336
+ for (let x = GRATE.x + 14; x < GRATE.x + GRATE.w - 4; x += 14) back.push(Ln(k.lineOnTop([x, GRATE.y], [x, GRATE.y + GRATE.d], 0, P), { tone: "faint" }));
337
+
338
+ const STRIP = { x: -128, y: DECK.y + DECK.d - 15, w: 196, d: 8, r: 1 };
339
+ back.push(S(k.slabOf(STRIP, 0, 0.8, P, 3), { tone: "lo", crease: "none" }));
340
+ const TICKS = k.topTicks(STRIP.x + 4, STRIP.x + STRIP.w - 4, 4, 5, STRIP.y + 1, 0.8, [2.2, 4.4], P);
341
+ back.push(Ln(TICKS.minor, { tone: "lo" }), Ln(TICKS.major, { tone: "mid" }));
342
+ back.push(Dots([at([STRIP.x + 2.2, STRIP.y + 4, 0.8]), at([STRIP.x + STRIP.w - 2.2, STRIP.y + 4, 0.8])], { size: 0.45 }));
343
+
344
+ const LABEL = { x: DECK.x + DECK.w - 21, y: -94, w: 12, d: 60, r: 1.5 };
345
+ back.push(S(k.slabOf(LABEL, 0, 1, P, 3), { tone: "mid" }));
346
+ back.push(Ln(k.planOutline(k.insetPlan(LABEL, 1.8), 1, P), { tone: "faint" }));
347
+ back.push(k.faceTextSvg(k.topMatrix([LABEL.x + 7.4, LABEL.y + LABEL.d - 5], 1, P, "y"), "RAPTOR 2", { size: 4.2, tone: "lo" }));
348
+ back.push(Ln([0, 1].map((row) => k.onTop([[LABEL.x + 3 + row * 2.2, LABEL.y + 5], [LABEL.x + 3 + row * 2.2, LABEL.y + (row ? 16 : 21)]], 1, P)).join(""), { tone: "lo", free: true }));
349
+ back.push(Dots([at([LABEL.x + LABEL.w / 2, LABEL.y + 2.6, 1]), at([LABEL.x + LABEL.w / 2, LABEL.y + LABEL.d - 2.6, 1])], { size: 0.45 }));
350
+
351
+ const deckTop = k.slabOf(k.insetPlan(DECK, 1), 0, 0.01, P, 8).fill;
352
+ const deckCentre = at([0, 0, 0]);
353
+ back.push(`<defs><radialGradient id="rp-deck" gradientUnits="userSpaceOnUse" cx="${deckCentre[0].toFixed(1)}" cy="${deckCentre[1].toFixed(1)}" r="${(150 * KS).toFixed(1)}" gradientTransform="translate(${deckCentre[0].toFixed(1)} ${deckCentre[1].toFixed(1)}) scale(1 ${k.cameraOf(P).sinE.toFixed(3)}) translate(${(-deckCentre[0]).toFixed(1)} ${(-deckCentre[1]).toFixed(1)})"><stop offset="0" class="rp-stop-0"/><stop offset=".28" class="rp-stop-1"/><stop offset=".7" class="rp-stop-2"/><stop offset="1" class="rp-stop-3"/></radialGradient></defs><path class="rp-deckglow" fill="url(#rp-deck)" d="${deckTop}"/>`);
354
+ back.push(`<path class="iso-halo rp-shadow" d="${k.haloOf({ x: -56, y: -56, w: 112, d: 112, r: 56 }, 0, 0.1, P)}"/>`);
355
+ back.push(S(G.disc(0, 4, 56, DECKF, P, { bevel: 1.2 }), { tone: "mid" }));
356
+ record(A.disc(DECKF, 0, 4, 56, { name: "stand.cone-plate", ...FRAME }));
357
+ back.push(Dots(G.dotsOf(DECKF, 4, 52, 28, P, { all: true }), { size: 0.45, tone: "mid" }));
358
+ const CONE_Q = 0.8;
359
+ const coneShare = (s) => Math.max(1e-4, 1 - (s - 4) / (CONE_TIP - 4));
360
+ const cone = (s) => 48 * coneShare(s) ** CONE_Q;
361
+ const coneSlope = (s) => ((-48 * CONE_Q) / (CONE_TIP - 4)) * coneShare(s) ** (CONE_Q - 1);
362
+ const CONE = Array.from({ length: 33 }, (_, index) => {
363
+ const s = 4 + (CONE_TIP - 4) * (1 - (1 - index / 32) ** 2);
364
+ return index === 32 ? [CONE_TIP, 0] : [s, cone(s), index ? 1 : 0];
365
+ });
366
+ back.push(S(G.solidOf(CONE, DECKF, P, { smooth: { slope: coneSlope, rows: 5 } }), { tone: "hi", inner: Ln([13, 22].map((s) => G.arcOf(DECKF, s, cone(s), P, { slope: coneSlope(s) })).join(""), { tone: "faint" }) }));
367
+ record(A.solid(DECKF, CONE, { name: "stand.cone", ...FRAME }));
368
+
369
+ const DELUGE = G.arcPoints(DECKF, 6.4, 68, 0, Math.PI * 2, 72);
370
+ for (const part of tubePieces(DELUGE, 2.1, P, { maxLength: 400, spacing: 2, closed: true })) back.push(tubeSvg(part, { tone: "mid" }));
371
+ back.push(Dots(G.dotsOf(DECKF, 8.6, 68, 36, P, { all: true }), { size: 0.5, tone: "hi" }));
372
+ record(A.tube(DELUGE, 2.1, { name: "stand.deluge", ...FRAME }));
373
+ const TEE_DEG = -12;
374
+ const TEE_R = 2.9;
375
+ const TEE_F = G.frameAlong(G.pointOf(DECKF, 6.4, 68, TEE_DEG * DEG), G.tangentOf(DECKF, TEE_DEG * DEG));
376
+ back.push(S(G.disc(-2.4, 2.4, TEE_R, TEE_F, P, { bevel: 0.3 }), { tone: "mid" }));
377
+ record(A.disc(TEE_F, -2.4, 2.4, TEE_R, { name: "stand.deluge-tee", ...FRAME }));
378
+ const FEED = G.fillet([G.pointOf(DECKF, 6.4, 68 + TEE_R - 0.2, TEE_DEG * DEG), [100, -14, 6.4], [139, -14, 6.4], [139, -14, 1.5]], 6);
379
+ for (const part of tubePieces(FEED, 2.1, P, { maxLength: 400, caps: [false, true] })) back.push(tubeSvg(part, { tone: "mid" }));
380
+ record(A.tube(FEED, 2.1, { name: "stand.deluge-feed", ...FRAME }));
381
+ back.push(S(k.slabOf({ x: 132, y: -22, w: 13, d: 16, r: 2 }, 0, 4, P, 3, 0.6), { tone: "mid" }));
382
+ back.push(Dots([at([135, -19, 4]), at([142, -9, 4])], { size: 0.45 }));
383
+
384
+ for (const [cx, cy] of COLUMN_AT) {
385
+ const base = { x: cx - 23, y: cy - 23, w: 46, d: 46, r: 3 };
386
+ back.push(S(k.slabOf(base, 0, 5, P, 3, 0.8), { tone: "mid" }));
387
+ record(A.box([base.x, base.y, 0], [base.x + base.w, base.y + base.d, 5], { name: "stand.column-base", ...FRAME }));
388
+ back.push(Dots(k.corners(base, 6).map(([x, y]) => at([x, y, 5])), { size: 0.55 }));
389
+ back.push(Ln(k.corners(base, 6).map(([x, y]) => k.ring(x, y, 2.4, 5, P, 14)).join(""), { tone: "lo" }));
390
+ const column = { x: cx - COLUMN / 2, y: cy - COLUMN / 2, w: COLUMN, d: COLUMN, r: 1.5 };
391
+ back.push(S(k.slabOf(column, 5, BEAM_Z - 5, P, 3, 0.8), { tone: "mid" }));
392
+ record(A.box([column.x, column.y, 5], [column.x + column.w, column.y + column.d, BEAM_Z], { name: "stand.column", ...FRAME }));
393
+ const edge = [cx + COLUMN / 2 - 1.5 + 1.5 * Math.SQRT1_2, cy + COLUMN / 2 - 1.5 + 1.5 * Math.SQRT1_2];
394
+ back.push(Ln(k.segment([...edge, 5], [...edge, BEAM_Z], P), { tone: "faint" }));
395
+ for (const z of [124, 248, 372]) {
396
+ const splice = k.insetPlan(column, -2.2);
397
+ back.push(S(k.slabOf(splice, z, 13, P, 3, 0.5), { tone: "mid" }));
398
+ record(A.box([splice.x, splice.y, z], [splice.x + splice.w, splice.y + splice.d, z + 13], { name: "stand.splice", ...FRAME }));
399
+ back.push(Dots([at([splice.x + splice.w, cy - 6, z + 4]), at([splice.x + splice.w, cy + 6, z + 4]), at([splice.x + splice.w, cy - 6, z + 9]), at([splice.x + splice.w, cy + 6, z + 9]), at([cx - 6, splice.y + splice.d, z + 4]), at([cx + 6, splice.y + splice.d, z + 4]), at([cx - 6, splice.y + splice.d, z + 9]), at([cx + 6, splice.y + splice.d, z + 9])], { size: 0.42 }));
400
+ }
401
+ }
402
+ const TRAY_X = COLUMN_AT[1][0] - COLUMN / 2 - 6;
403
+ back.push(S(k.slabOf({ x: TRAY_X, y: COLUMN_AT[1][1] + 2, w: 6, d: 9, r: 0.6 }, 28, BEAM_Z - 36, P, 2), { tone: "mid" }));
404
+ for (let z = 52; z < BEAM_Z - 20; z += 40) back.push(S(k.slabOf({ x: TRAY_X - 0.8, y: COLUMN_AT[1][1] + 1, w: 7.6, d: 11, r: 0.6 }, z, 3, P, 2), { tone: "mid" }));
405
+ const LADDER_Y = COLUMN_AT[0][1] + COLUMN / 2;
406
+ for (const x of [COLUMN_AT[0][0] - 9, COLUMN_AT[0][0] + 9]) back.push(S(k.slabOf({ x: x - 1.3, y: LADDER_Y + 8, w: 2.6, d: 2.6, r: 0.6 }, 18, BEAM_Z - 52, P, 2), { tone: "mid", crease: "none" }));
407
+ for (let z = 40; z < BEAM_Z - 38; z += 28) back.push(Ln(k.segment([COLUMN_AT[0][0] - 9, LADDER_Y + 10.6, z], [COLUMN_AT[0][0] + 9, LADDER_Y + 10.6, z], P), { tone: "mid" }));
408
+ for (const z of [52, BEAM_Z - 56]) for (const x of [COLUMN_AT[0][0] - 9, COLUMN_AT[0][0] + 9]) back.push(S(k.slabOf({ x: x - 1, y: LADDER_Y, w: 2, d: 9, r: 0.4 }, z, 2, P, 2), { tone: "mid", crease: "none" }));
409
+
410
+
411
+ const bell = (s) => 36 + 29 * (1 - Math.pow(s / 120, 1.6));
412
+ const bellSlope = (s) => (-29 * 1.6 * Math.pow(s / 120, 0.6)) / 120;
413
+ const BELL_AT = [3.2, 14, 28, 44, 60, 76, 92, 106, 116];
414
+ const BELL_STEPS = 96;
415
+ const BELL_SKIRT = BELL_AT.length - 2;
416
+ turned("nozzle", ENGINE, [[0, 66.4], [3.2, 66.4]], {}, { bevel: 0.6, steps: BELL_STEPS });
417
+ const BELL = G.bandsOf([[BELL_AT[0], bell(BELL_AT[0])], [BELL_AT[BELL_AT.length - 1], bell(BELL_AT[BELL_AT.length - 1])]], BELL_AT.slice(1, -1), ENGINE, P, { radius: bell, slope: bellSlope, steps: BELL_STEPS, rows: 8 });
418
+ BELL.bands.forEach((band, index) => {
419
+ const seam = index === 2 || index === 5 ? Ln(band.seam, { tone: "faint" }) : "";
420
+ put("nozzle", ex(ENGINE, (band.s0 + band.s1) / 2), S(index < BELL_SKIRT ? band.paths : { ...band.paths, outline: band.outline }, { tone: "hi", inner: seam }), { name: `nozzle.bell${index}`, shapes: [record(A.solid(ENGINE, band.profile, { radius: bell, name: `nozzle.bell${index}` }))] });
421
+ });
422
+ const BELL_SIDES = BELL.sidesOf(BELL_AT[0], BELL_AT[BELL_SKIRT], { ends: "first", count: 72 });
423
+ put("nozzle", ex(ENGINE, BELL_AT[BELL_SKIRT] - 1), Ln(BELL_SIDES.d, { tone: "hi" }), { bias: 0.2, name: "nozzle.sides", shapes: BELL_SIDES.points.map((points) => A.tube(points, 0.4 / KS)) });
424
+ put(
425
+ "nozzle",
426
+ ex(ENGINE, 118),
427
+ S(G.ringBand(116, 120, 30.5, 38, ENGINE, P), { tone: "hi" }) +
428
+ `<path class="rp-void" d="${G.circleOf(ENGINE, 120, 30.5, P)}"/>` +
429
+ Ln(G.arcOf(ENGINE, 120, 30.5, P, { inward: true, least: -1 }), { tone: "lo" }) +
430
+ Dots(G.dotsOf(ENGINE, 120, 36.4, 40, P, { all: true }), { size: 0.38, tone: "mid" }),
431
+ { name: "nozzle.lip", shapes: [record(A.ring(ENGINE, 116, 120, 30.5, 38, { name: "nozzle.lip" }))] },
432
+ );
433
+ const MANIFOLD_S = 109;
434
+ const MANIFOLD_TUBE = 4.6;
435
+ const TEE = 6.6;
436
+ const DOWN_R = 4.6;
437
+ const bellGap = (r) => Math.min(...Array.from({ length: 241 }, (_, index) => Math.hypot(index / 2 - MANIFOLD_S, bell(index / 2) - r)));
438
+ let MANIFOLD_R = 40;
439
+ while (bellGap(MANIFOLD_R) < TEE + 0.3) MANIFOLD_R += 0.05;
440
+ const PORTS = [12, 132, 252];
441
+ const MANIFOLD = G.arcPoints(ENGINE, MANIFOLD_S, MANIFOLD_R, PORTS[0] * DEG, (PORTS[0] + 360) * DEG, 120);
442
+ const MANIFOLD_LENGTH = G.pathLength(MANIFOLD);
443
+ const teeGap = Math.sqrt(TEE ** 2 - MANIFOLD_TUBE ** 2);
444
+ const portAt = (deg) => ((deg - PORTS[0]) / 360) * MANIFOLD_LENGTH;
445
+ tubeItems("nozzle", MANIFOLD, MANIFOLD_TUBE, { name: "nozzle.manifold", maxLength: 22, spacing: 1.2, closed: true, touch: ["nozzle.bell*"], gaps: [[0, teeGap], [MANIFOLD_LENGTH - teeGap, MANIFOLD_LENGTH], ...PORTS.slice(1).map((deg) => [portAt(deg) - teeGap, portAt(deg) + teeGap])] });
446
+ for (const deg of PORTS) ballAt("nozzle", polar(deg, MANIFOLD_R, MANIFOLD_S), TEE, { tone: "hi", name: "nozzle.tee", owner: "nozzle.manifold" });
447
+ for (const deg of PORTS.slice(1)) {
448
+ const F = G.frameAlong(polar(deg, MANIFOLD_R, MANIFOLD_S), dir(deg));
449
+ const s0 = Math.sqrt(TEE ** 2 - 2.6 ** 2);
450
+ put("nozzle", G.pointOf(F, 7, 0, 0), S(G.disc(s0, 7.6, 2.6, F, P), { tone: "hi" }), { name: "nozzle.stub", shapes: [record(A.disc(F, s0, 7.6, 2.6, { name: "nozzle.stub", owner: "nozzle.manifold" }))] });
451
+ put("nozzle", G.pointOf(F, 8.5, 0, 0), S(G.disc(7.6, 9.4, 4.4, F, P), { tone: "hi" }) + Dots(G.dotsOf(F, 9.4, 3.4, 6, P, { all: true }), { size: 0.38, tone: "mid" }), { name: "nozzle.blind", shapes: [record(A.disc(F, 7.6, 9.4, 4.4, { name: "nozzle.blind" }))] });
452
+ }
453
+ for (const deg of [72, 192, 312]) {
454
+ const base = polar(deg, bell(MANIFOLD_S) - 0.1, MANIFOLD_S);
455
+ const F = G.frameAlong(base, dir(deg));
456
+ const reach = MANIFOLD_R - MANIFOLD_TUBE + 0.12 - (bell(MANIFOLD_S) - 0.1);
457
+ put("nozzle", G.pointOf(F, reach / 2, 0, 0), S(G.disc(0, reach, 0.9, F, P), { tone: "mid" }), { name: "nozzle.pad", shapes: [record(A.disc(F, 0, reach, 0.9, { name: "nozzle.pad", owner: "nozzle.manifold" }))] });
458
+ }
459
+ for (const deg of [60, 200]) boss("nozzle", polar(deg, bell(40), 40), dir(deg), 1.8, 2.6);
460
+ boss("nozzle", polar(30, bell(70), 70), dir(30), 1.6, 2.2);
461
+ const LIP = G.arcOf(ENGINE, 0, 66.4, P, { least: -0.05 });
462
+ for (const deg of [96, 156]) boss("nozzle", polar(deg, bell(88) - 0.15, 88), G.unit3(G.sub3(dir(deg), [0, 0, bellSlope(88)])), 2.4, 3.2, { name: "nozzle.pad" });
463
+
464
+
465
+ const OTP_REACH = 7;
466
+ const OTP_FLANGE_R = 9.4;
467
+ const FTP_REACH = 7.5;
468
+ const FTP_FLANGE_R = 8.6;
469
+ const radiusOn = (profile, s) => {
470
+ let index = 1;
471
+ while (index < profile.length - 1 && profile[index][0] < s) index++;
472
+ const [s0, r0] = profile[index - 1];
473
+ const [s1, r1] = profile[index];
474
+ return r0 + ((r1 - r0) * (s - s0)) / (s1 - s0 || 1);
475
+ };
476
+ const slopeOn = (profile, s) => (radiusOn(profile, s + 0.01) - radiusOn(profile, s - 0.01)) / 0.02;
477
+ const onBody = (F, profile, deg, s) => ({ at: G.pointOf(F, s, radiusOn(profile, s), deg * DEG), normal: G.unit3(G.sub3(G.radialOf(F, deg * DEG), G.mul3(F.a, slopeOn(profile, s)))) });
478
+
479
+ turned("chamber", ENGINE, [[120, 38], [123.5, 38]], {}, { extra: Dots(G.dotsOf(ENGINE, 123.5, 36.4, 40, P, { all: true }), { size: 0.38, tone: "mid" }), bevel: 0.5 });
480
+ const throat = (s) => Math.sqrt(24.2 ** 2 + ((s < 146 ? 0.997 : 0.95) * (s - 146)) ** 2);
481
+ const throatSlope = (s) => ((s < 146 ? 0.994 : 0.9025) * (s - 146)) / throat(s);
482
+ const THROAT = [...Array.from({ length: 15 }, (_, index) => 123.5 + (22.5 * index) / 15), ...Array.from({ length: 11 }, (_, index) => 146 + (16 * index) / 10)].map((s, index, all) => [s, throat(s), index && index < all.length - 1 ? 1 : 0]);
483
+ turned("chamber", ENGINE, THROAT, {}, { smooth: { slope: throatSlope, rows: 5 }, inner: G.arcOf(ENGINE, 146, throat(146), P) });
484
+ const JACKETS = [[162, 171, 167], [174.2, 186, 180], [189.2, 194, 191.6]].map(([s0, s1, seam]) => band("chamber", ENGINE, s0, s1, 29.4, {}, { name: "chamber.jacket", inner: G.arcOf(ENGINE, seam, 29.4, P) }));
485
+ const RINGS = [171, 186].map((s) => band("chamber", ENGINE, s, s + 3.2, 31.6, {}, { name: "chamber.ring", extra: Dots(G.dotsOf(ENGINE, s + 3.2, 30.4, 24, P, { all: true, least: 0 }), { size: 0.34, tone: "lo" }) }));
486
+ const HEAD = band("chamber", ENGINE, 194, 205, 33.4, {}, { bias: 0.4, extra: Dots(G.dotsOf(ENGINE, 200.5, 33.4, 30, P, { fade: FADE }), { size: 0.4, tone: "mid" }) });
487
+ const INJECTOR = turned("chamber", ENGINE, [[205, 37.5], [210, 37.5]], {}, { bevel: 0.6, extra: Dots(G.dotsOf(ENGINE, 210, 35.6, 36, P, { all: true }), { size: 0.42, tone: "mid" }) });
488
+ const DOME_PROFILE = [[210, 34.4], [214, 34, 1], [218, 32, 1], [222, 27.5]];
489
+ const DOME = turned("chamber", ENGINE, DOME_PROFILE, {}, { extra: Ln(G.circleOf(ENGINE, 222, 23.5, P), { tone: "faint" }) });
490
+ const domeAt = (deg, s) => onBody(ENGINE, DOME_PROFILE, deg, s);
491
+ const JACKET_BOSS = Object.fromEntries([70, 110, 250].map((deg) => [deg, boss("chamber", polar(deg, 29.4, 180), dir(deg), 1.7, 3, { host: JACKETS[1] })]));
492
+ boss("chamber", polar(-150, 33.4, 200), dir(-150), 2.4, 3.6, { host: HEAD });
493
+ const DOME_BOSS = Object.fromEntries([-100, 175].map((deg) => {
494
+ const { at: point, normal } = domeAt(deg, 216);
495
+ return [deg, { face: boss("chamber", point, normal, 1.5, 2.6, { host: DOME }), normal }];
496
+ }));
497
+ boss("chamber", domeAt(28, 219).at, domeAt(28, 219).normal, 1.5, 2.4, { host: DOME });
498
+
499
+
500
+ turned("orpb", ENGINE, [[222, 25.5], [225.5, 25.5]], {}, { extra: Dots(G.dotsOf(ENGINE, 225.5, 24, 24, P, { all: true }), { size: 0.4, tone: "mid" }) });
501
+ const ORPB_BODY = band("orpb", ENGINE, 225.5, 242, 22.2, {}, { inner: [230, 236].map((s) => G.arcOf(ENGINE, s, 22.2, P)).join("") });
502
+ turned("orpb", ENGINE, [[242, 25.5], [246, 25.5]], {}, { extra: Dots(G.dotsOf(ENGINE, 246, 24, 24, P, { all: true }), { size: 0.4, tone: "mid" }) });
503
+ const IGNITER_DEG = 200;
504
+ const ORPB_IGNITER = boss("orpb", polar(IGNITER_DEG, 22.2, 233), dir(IGNITER_DEG), 2.6, 4.4, { host: ORPB_BODY, ring: false });
505
+ const ORPB_PLUG = boss("orpb", ORPB_IGNITER, dir(IGNITER_DEG), 2, 4);
506
+ const ORPB_EXCITER = boss("orpb", polar(100, 22.2, 228.5), dir(100), 1.3, 2.4, { host: ORPB_BODY, name: "orpb.exciter" });
507
+ const ORPB_SENSE = boss("orpb", polar(20, 22.2, 236), dir(20), 2, 3.2, { host: ORPB_BODY, name: "orpb.sense-boss" });
508
+ boss("orpb", polar(70, 22.2, 238), dir(70), 1.5, 2.6, { host: ORPB_BODY });
509
+
510
+
511
+ const OTP_RING = turned("otp", ENGINE, [[246, 25], [248, 27.4], [257, 27.4], [259, 24.5]], {}, {
512
+ extra: Dots(G.dotsOf(ENGINE, 257, 27.4, 36, P, { fade: FADE }), { size: 0.4, tone: "mid" }),
513
+ inner: G.arcOf(ENGINE, 252.5, 27.4, P),
514
+ });
515
+ band("otp", ENGINE, 259, 283, 20.6, {}, { name: "otp.housing", inner: [263, 279].map((s) => G.arcOf(ENGINE, s, 20.6, P)).join("") });
516
+ const VOLUTE = G.spiral(ENGINE, 271, 20.6 + 2.4 + 0.3, 20.6 + 6.6 + 0.3, -160 * DEG, 112 * DEG, 64);
517
+ const VOLUTE_END = VOLUTE[VOLUTE.length - 1];
518
+ const voluteTan = G.unit3(G.sub3(VOLUTE_END, VOLUTE[VOLUTE.length - 2]));
519
+ const OTP_OUTLET = G.add3(VOLUTE_END, G.mul3(voluteTan, OTP_REACH));
520
+ tubeItems("otp", [...VOLUTE, OTP_OUTLET], (t) => 2.4 + 4.2 * t, { name: "otp.volute", maxLength: 18, spacing: 1, bias: 0.3, touch: ["otp.housing"], ends: [false, true] });
521
+ const OTP_FLANGE_T = 2.6;
522
+ flangeAt("otp", G.add3(OTP_OUTLET, G.mul3(voluteTan, OTP_FLANGE_T / 2)), voluteTan, OTP_FLANGE_R, OTP_FLANGE_T, { bolts: 8, bias: 0.4, name: "otp.outlet" });
523
+ const LOX_START = G.add3(OTP_OUTLET, G.mul3(voluteTan, OTP_FLANGE_T));
524
+ turned("otp", ENGINE, [[283, 16.5], [286, 16.5]], {}, { extra: Dots(G.dotsOf(ENGINE, 286, 15, 16, P, { all: true }), { size: 0.38, tone: "mid" }) });
525
+ band("otp", ENGINE, 286, 292, 12.6);
526
+ const LUG_BALL = 3.6;
527
+ const lugBall = (foot) => polar(foot, 29.6 + LUG_BALL, 252.5);
528
+ for (const [, deg] of LUGS) {
529
+ const F = G.frameAlong(polar(deg, 0, 252.5), dir(deg));
530
+ const start = Math.sqrt(27.4 ** 2 - 2 ** 2);
531
+ put("otp", polar(deg, 28.5, 252.5), S(G.disc(start, 29.6, 2, F, P), { tone: "mid" }), { bias: 0.1, name: "otp.lug", shapes: [record(A.disc(F, start, 29.6, 2, { name: "otp.lug", seated: true, cut: OTP_RING }))] });
532
+ }
533
+ const OTP_SENSE = boss("otp", polar(80, 27.4, 252.5), dir(80), 1.7, 2.4, { host: OTP_RING });
534
+ const PURGE_C = boss("otp", polar(-120, 27.4, 252.5), dir(-120), 2.2, 3, { host: OTP_RING });
535
+ const PRESS_LOX = boss("otp", polar(200, 27.4, 252.5), dir(200), 2.7, 2, { host: OTP_RING });
536
+ const PURGE_F = boss("otp", polar(-100, 27.4, 252.5), dir(-100), 2, 2.4, { host: OTP_RING });
537
+
538
+
539
+ const puck = (s) => 18.4 - (s < 297.5 ? 5 : 3.4) * (Math.abs(s - 297.5) / 5.5) ** 4;
540
+ const puckSlope = (s) => ((s < 297.5 ? 5 : -3.4) * 4 * Math.abs(s - 297.5) ** 3) / 5.5 ** 4;
541
+ const PUCK = Array.from({ length: 23 }, (_, index) => {
542
+ const s = 292 + (11 * index) / 22;
543
+ return [s, puck(s), index && index < 22 ? 1 : 0];
544
+ });
545
+ const GIMBAL = turned("gimbal", ENGINE, PUCK, {}, { smooth: { slope: puckSlope, rows: 3 }, inner: G.arcOf(ENGINE, 297.5, 18.4, P) });
546
+ for (const deg of [AZIMUTH - 45, AZIMUTH + 45, AZIMUTH + 135, AZIMUTH - 135]) boss("gimbal", polar(deg, 18.4, 297.5), dir(deg), 1.9, 2.8, { bias: 0.3, host: GIMBAL });
547
+ turned("gimbal", ENGINE, [[303, 24], [310, 24]], {}, { bevel: 0.8, extra: Dots(G.dotsOf(ENGINE, 310, 21.2, 18, P, { all: true }), { size: 0.45, tone: "mid" }) + Ln(G.circleOf(ENGINE, 310, 15, P), { tone: "faint" }) });
548
+
549
+
550
+ const FTP_LOW_PROFILE = [[166, 3.5], [168, 8, 1], [171, 10.6, 1], [175, 11.6], [194, 11.6]];
551
+ const FTP_LOW = turned("ftp", FTP, FTP_LOW_PROFILE, {}, { inner: [180, 188].map((s) => G.arcOf(FTP, s, 11.6, P)).join("") });
552
+ const FTP_RING = turned("ftp", FTP, [[194, 15], [198, 15]], {}, { extra: Dots(G.dotsOf(FTP, 198, 13.6, 18, P, { all: true }), { size: 0.4, tone: "mid" }) });
553
+ const FTP_TURBINE = turned("ftp", FTP, [[198, 16], [200, 18.4], [214, 18.4], [216, 16]], {}, { extra: Dots(G.dotsOf(FTP, 200, 18.4, 28, P, { fade: FADE }), { size: 0.4, tone: "mid" }), inner: G.arcOf(FTP, 207, 18.4, P) });
554
+ const FTP_NECK = band("ftp", FTP, 216, 226, 11.6, {}, { extra: Faded(G.ribsOf(FTP, 216, 226, 11.6, 18, P, { fade: [0.1, 0.55] }), { tone: "faint" }) });
555
+ const FTP_HOUSING = band("ftp", FTP, 226, 250, 15.2, {}, { name: "ftp.housing", inner: [230, 246].map((s) => G.arcOf(FTP, s, 15.2, P)).join("") });
556
+ const FVOLUTE = G.spiral(FTP, 238, 15.2 + 2 + 0.3, 15.2 + 5.6 + 0.3, -140 * DEG, 36 * DEG, 48);
557
+ const FVOLUTE_END = FVOLUTE[FVOLUTE.length - 1];
558
+ const fvTan = G.unit3(G.sub3(FVOLUTE_END, FVOLUTE[FVOLUTE.length - 2]));
559
+ const FTP_OUTLET = G.add3(FVOLUTE_END, G.mul3(fvTan, FTP_REACH));
560
+ tubeItems("ftp", [...FVOLUTE, FTP_OUTLET], (t) => 2 + 3.6 * t, { name: "ftp.volute", maxLength: 16, spacing: 1, bias: 0.3, touch: ["ftp.housing"], ends: [false, true] });
561
+ const FTP_FLANGE_T = 2.4;
562
+ flangeAt("ftp", G.add3(FTP_OUTLET, G.mul3(fvTan, FTP_FLANGE_T / 2)), fvTan, FTP_FLANGE_R, FTP_FLANGE_T, { bolts: 8, bias: 0.4, name: "ftp.outlet" });
563
+ const FUEL_START = G.add3(FTP_OUTLET, G.mul3(fvTan, FTP_FLANGE_T));
564
+ band("ftp", FTP, 250, 262, 10.2, {}, { inner: G.arcOf(FTP, 256, 10.2, P) });
565
+ turned("ftp", FTP, [[262, 13.6], [265, 13.6]], {}, { bevel: 0.5, extra: Dots(G.dotsOf(FTP, 265, 12, 12, P, { all: true }), { size: 0.4, tone: "mid" }) });
566
+ const FRPB_IGNITER = boss("ftp", ftp(70, 11.6, 182), dir(70), 2.4, 4, { host: FTP_LOW, ring: false });
567
+ const FRPB_PLUG = boss("ftp", FRPB_IGNITER, dir(70), 1.9, 3.6, { name: "ftp.igniter-plug" });
568
+ const FRPB_EXCITER_AT = onBody(FTP, FTP_LOW_PROFILE, 40, 173);
569
+ const FRPB_EXCITER = boss("ftp", FRPB_EXCITER_AT.at, FRPB_EXCITER_AT.normal, 1.3, 2.4, { host: FTP_LOW, name: "ftp.exciter" });
570
+ const FTP_SENSE = boss("ftp", ftp(100, 18.4, 207), dir(100), 1.7, 2.6, { host: FTP_TURBINE });
571
+ const FTP_PUMP_SENSE = boss("ftp", ftp(110, 15.2, 233), dir(110), 1.7, 2.4, { host: FTP_HOUSING });
572
+ const PICKUP_DEG = 60;
573
+ const FTP_PICKUP = boss("ftp", ftp(PICKUP_DEG, 11.6, 221), dir(PICKUP_DEG), 1.4, 2.4, { name: "ftp.pickup", host: FTP_NECK });
574
+ const balance = [ftp(250, 15.2, 229), ftp(250, 19, 229), ftp(250, 21, 221), ftp(250, 22, 212), ftp(250, 18.4, 212)];
575
+ pipe("ftp", balance, 1.2, { flangeR: 1.6, bend: 3, name: "ftp.balance" });
576
+
577
+ function strut(piece, [hostA, a, na], [hostB, b, nb], { move, r = 1.1, ball = 2.2, stem = 1.3, lift = 1.2 } = {}) {
578
+ const id = nameFor(`${piece}.strut`);
579
+ const reach = Math.sqrt(ball ** 2 - stem ** 2);
580
+ const [ca, cb] = [[hostA, a, na], [hostB, b, nb]].map(([host, point, normal]) => {
581
+ const own = host.piece ?? piece;
582
+ const face = boss(own, point, normal, stem, lift, { host, move: own === piece ? move : undefined, name: `${own}.ball-stud`, owner: `${id}.stud`, ring: false });
583
+ const centre = G.add3(face, G.mul3(normal, reach));
584
+ ballAt(own, centre, ball, { move: own === piece ? move : undefined, tone: "hi", name: `${own}.ball-stud`, owner: `${id}.stud` });
585
+ return centre;
586
+ });
587
+ const axis = G.unit3(G.sub3(cb, ca));
588
+ const length = G.len3(G.sub3(cb, ca));
589
+ const F = G.frameAlong(ca, axis);
590
+ const end = Math.sqrt(ball ** 2 - r ** 2);
591
+ put(piece, G.lerp3(ca, cb, 0.5), S(G.disc(end, length - end, r, F, P), { tone: "hi", inner: Ln(G.arcOf(F, length * 0.5, r, P), { tone: "faint" }) }), { move, name: id, shapes: [record(A.disc(F, end, length - end, r, { name: id, owner: id }))] });
592
+ }
593
+ const STRUT_LOW = onBody(FTP, FTP_LOW_PROFILE, 214, 172.4);
594
+ strut("ftp", [FTP_HOUSING, ftp(196, 15.2, 244), dir(196)], [OTP_RING, polar(-24, 27.4, 250.6), dir(-24)]);
595
+ strut("ftp", [FTP_TURBINE, ftp(116, 18.4, 205), dir(116)], [INJECTOR, polar(8, 37.5, 207.5), dir(8)]);
596
+ strut("ftp", [FTP_LOW, STRUT_LOW.at, STRUT_LOW.normal], [RINGS[0], polar(-56, 31.6, 172.6), dir(-56)]);
597
+
598
+
599
+ const HOT_S = 199.4;
600
+ const HOT_R = 3.6;
601
+ const HOT_PAD = 5.2;
602
+ const HOT_A = boss("ftp", ftp(160, 18.4, HOT_S), dir(160), HOT_PAD, 1, { host: [FTP_TURBINE, FTP_RING], ring: false, name: "ftp.hot-pad" });
603
+ const HOT_B = boss("chamber", polar(FTP_DEG, 33.4, HOT_S), dir(FTP_DEG), HOT_PAD, 1, { host: HEAD, ring: false, name: "chamber.hot-pad" });
604
+ pipe("hotgas", [HOT_A, HOT_B], HOT_R, { flangeR: 1.3, bands: [0.5], maxLength: 12, name: "hotgas.duct", free: [true, true] });
605
+
606
+
607
+ const DOWN_ELBOW = G.add3(FUEL_START, G.mul3(fvTan, 30));
608
+ const DOWN_STUB = [MANIFOLD_S + Math.sqrt(TEE ** 2 - DOWN_R ** 2), MANIFOLD_S + 7.6, MANIFOLD_S + 8.8];
609
+ {
610
+ const F = G.frameAlong(polar(PORTS[0], MANIFOLD_R, 0), [0, 0, 1]);
611
+ put("nozzle", polar(PORTS[0], MANIFOLD_R, (DOWN_STUB[0] + DOWN_STUB[1]) / 2), S(G.disc(DOWN_STUB[0], DOWN_STUB[1], DOWN_R, F, P), { tone: "hi" }), { name: "nozzle.stub", shapes: [record(A.disc(F, DOWN_STUB[0], DOWN_STUB[1], DOWN_R, { name: "nozzle.stub", owner: "nozzle.manifold" }))] });
612
+ put("nozzle", polar(PORTS[0], MANIFOLD_R, (DOWN_STUB[1] + DOWN_STUB[2]) / 2), S(G.disc(DOWN_STUB[1], DOWN_STUB[2], DOWN_R * 1.4, F, P), { tone: "hi" }) + Dots(G.dotsOf(F, DOWN_STUB[2], DOWN_R * 1.2, 8, P, { all: true }), { size: 0.38, tone: "mid" }), { name: "nozzle.port-flange", shapes: [record(A.disc(F, DOWN_STUB[1], DOWN_STUB[2], DOWN_R * 1.4, { name: "nozzle.port-flange", owner: "nozzle.manifold" }))] });
613
+ }
614
+ const DOWN_ROUTE = [FUEL_START, DOWN_ELBOW, [DOWN_ELBOW[0], DOWN_ELBOW[1], Z_EXIT + 196], polar(12, 60, 152), polar(12, MANIFOLD_R, 130), polar(12, MANIFOLD_R, DOWN_STUB[2])];
615
+ const DOWN = pipe("downcomer", DOWN_ROUTE, DOWN_R, { bend: 1.6, flangeR: [FTP_FLANGE_R / DOWN_R, 1.4], bands: [0.5, 0.58, 0.66], bellows: [0.24, 0.3, 5], valve: [12.4, 3.6], maxLength: 18, name: "downcomer.duct", free: [true, true] });
616
+ const MFV = valveOn("downcomer", DOWN, 12.4, 3.6);
617
+
618
+
619
+ const LOX_S = 233.75;
620
+ const LOX_DEG = 150;
621
+ const LOX_FACE = boss("orpb", polar(LOX_DEG, 22.2, LOX_S), dir(LOX_DEG), 7, 1.2, { host: ORPB_BODY, ring: false, name: "orpb.lox-pad" });
622
+ const LOX = pipe("lox", [LOX_START, G.add3(LOX_START, G.mul3(voluteTan, 6)), polar(140, 52, 270), polar(140, 52, LOX_S), polar(LOX_DEG, 40, LOX_S), LOX_FACE], 5.6, {
623
+ bend: 1.5,
624
+ flangeR: [OTP_FLANGE_R / 5.6, 1.2],
625
+ valve: [0.5, 4.2],
626
+ maxLength: 14,
627
+ name: "lox.duct",
628
+ free: [true, true],
629
+ });
630
+ const LOX_RUN = R.tubes.filter((tube) => tube.owner === LOX.owner);
631
+ const MOV = valveOn("lox", LOX, 0.5, 4.2, { side: dir(110) });
632
+
633
+
634
+ const LINES = [];
635
+ function line(points, r, move, options = {}) {
636
+ LINES.push(points);
637
+ return pipe("plumbing", points, r, { move, flangeR: 1.7, maxLength: 16, name: "plumbing.line", ...options });
638
+ }
639
+ const ringAt = (s, r, from, to, steps = 0) => {
640
+ const count = steps || Math.max(2, Math.ceil(Math.abs(to - from) / 12));
641
+ return Array.from({ length: count + 1 }, (_, index) => polar(from + ((to - from) * index) / count, r, s));
642
+ };
643
+ const tubeAt = (runs, point) => runs.reduce((best, run) => {
644
+ const d = A.sdf(run, point);
645
+ return Math.abs(d) < Math.abs(best.d) ? { run, d } : best;
646
+ }, { run: null, d: Infinity }).run;
647
+
648
+ const JACKET_MOVE = out(-120, 50, -10);
649
+ const JACKET_S = 199;
650
+ const JACKET_FROM = boss("chamber", polar(-48, 33.4, JACKET_S), dir(-48), 4.4, 1, { host: HEAD, ring: false, name: "chamber.jacket-pad" });
651
+ const JACKET_TO = boss("ftp", ftp(168, 11.6, 181), dir(168), 4.4, 1, { host: FTP_LOW, ring: false, name: "ftp.jacket-pad" });
652
+ const JACKET_TEE = polar(-48, 41, JACKET_S);
653
+ const JACKET_BALL = 4.2;
654
+ const jacketLine = line([JACKET_FROM, polar(-48, 50, JACKET_S), G.add3(JACKET_TO, G.mul3(dir(168), 13)), JACKET_TO], 3, JACKET_MOVE, { name: "plumbing.jacket-to-frpb", bands: [0.55], flangeR: [1.3, 1.4], free: [true, true], gaps: [[G.len3(G.sub3(JACKET_TEE, JACKET_FROM)) - Math.sqrt(JACKET_BALL ** 2 - 9), G.len3(G.sub3(JACKET_TEE, JACKET_FROM)) + Math.sqrt(JACKET_BALL ** 2 - 9)]] });
655
+ ballAt("plumbing", JACKET_TEE, JACKET_BALL, { move: JACKET_MOVE, tone: "hi", name: "plumbing.jacket-tee", owner: jacketLine.owner });
656
+ const ORPB_FEED = boss("orpb", polar(-96, 22.2, 236), dir(-96), 3.2, 0.8, { host: ORPB_BODY, ring: false, name: "orpb.jacket-pad" });
657
+ line([G.add3(JACKET_TEE, [0, 0, Math.sqrt(JACKET_BALL ** 2 - 2.1 ** 2)]), G.add3(JACKET_TEE, [0, 0, 6]), polar(-52, 41, 209), polar(-70, 42, 216), polar(-92, 36, 230), polar(-96, 31, 236), ORPB_FEED], 2.1, JACKET_MOVE, { name: "plumbing.jacket-to-orpb", bands: [0.5], flanges: [false, true], flangeR: 1.4, free: [false, true] });
658
+
659
+ const TAP_S = 199.5;
660
+ const TAP_R = 41.5;
661
+ const TAP_AT = polar(LOX_DEG, 34, LOX_S - 5.6);
662
+ const LOX_FRPB_MOVE = out(86, 50, -16);
663
+ const LOX_TAP = boss("lox", TAP_AT, [0, 0, -1], 3.1, 1, { host: tubeAt(LOX_RUN, TAP_AT), ring: false, name: "lox.tap-pad" });
664
+ const FRPB_FEED_AT = onBody(FTP, FTP_LOW_PROFILE, 112, 172.6);
665
+ const FRPB_FEED = boss("ftp", FRPB_FEED_AT.at, FRPB_FEED_AT.normal, 3.1, 0.8, { host: FTP_LOW, ring: false, name: "ftp.feed-pad" });
666
+ line([LOX_TAP, polar(LOX_DEG, 34, 222), polar(LOX_DEG - 4, TAP_R, 210.5), ...ringAt(TAP_S, TAP_R, LOX_DEG - 8, 2), polar(-6, 50, 192), G.add3(FRPB_FEED, G.mul3(FRPB_FEED_AT.normal, 11)), FRPB_FEED], 2.1, LOX_FRPB_MOVE, { name: "plumbing.lox-to-frpb", bands: [0.18, 0.4, 0.62, 0.84], flangeR: 1.4, free: [true, true] });
667
+
668
+ const PRESS_LOX_MOVE = out(205, 30, -8);
669
+ line([PRESS_LOX, polar(200, 33, 252.5), polar(198, 36, 262), polar(188, 37, 285), polar(150, 46, 310), polar(135, 50, TOP + 13), polar(135, 50, TOP + 21)], 1.5, PRESS_LOX_MOVE, { name: "plumbing.press-lox", bands: [0.25, 0.5, 0.75], coupling: true, free: [true, true] });
670
+ line([polar(-62, 29.4, 182.5), polar(-62, 49.5, 182.5), polar(-58, 49.5, 222), polar(-52, 50, 270), polar(-46, 49, 300), polar(-45, 46, TOP + 13), polar(-45, 46, TOP + 21)], 1.5, out(-70, 46, -30), { name: "plumbing.press-ch4", bands: [0.3, 0.6], coupling: true, flangeR: [1.5, 1.7], free: [true, true] });
671
+ line([PURGE_C, polar(-120, 34, 252.5), polar(-120, 38, 250), polar(-140, 40, 228), G.add3(domeAt(-150, 212).at, G.mul3(domeAt(-150, 212).normal, 4)), domeAt(-150, 212).at], 1.1, out(-140, 45, -10), { name: "plumbing.purge-chamber", bands: [0.5], free: [true, true] });
672
+ line([PURGE_F, polar(-100, 40, 252.5), polar(-72, 44, 238), polar(-52, 44.5, 229), polar(-40, 52, 222), G.add3(ftp(200, 18.4, 210), G.mul3(dir(200), 8)), ftp(200, 18.4, 210)], 1.1, out(-80, 35, -15), { name: "plumbing.purge-ftp", bands: [0.35, 0.7], free: [true, true] });
673
+ const INLET_AT = G.add3(polar(LOX_DEG, 30, LOX_S), G.mul3(dir(LOX_DEG + 90), 5.6));
674
+ const LOX_INLET = boss("lox", INLET_AT, dir(LOX_DEG + 90), 2, 0.8, { host: tubeAt(LOX_RUN, INLET_AT), ring: false, name: "lox.inlet-pad" });
675
+ line([ORPB_PLUG, G.add3(ORPB_PLUG, G.mul3(dir(IGNITER_DEG), 3)), G.add3(LOX_INLET, G.mul3(dir(LOX_DEG + 90), 3)), LOX_INLET], 0.9, out(160, 40, -40), { name: "plumbing.igniter-orpb", flangeR: 2, bend: 3, free: [true, true] });
676
+ const IGNITER_FRPB_TURN = ftp(62, 26.5, 189);
677
+ const IGNITER_FRPB_TIP = elbow("ftp", FRPB_PLUG, dir(70), 1.9, IGNITER_FRPB_TURN, 2.3, 0.9 * 1.3, { name: "ftp.igniter-elbow", owner: "ftp.igniter-plug" });
678
+ line([IGNITER_FRPB_TIP, IGNITER_FRPB_TURN, ftp(52, 24, 196), ftp(52, 15, 196)], 0.9, PIECES.ftp.move, { name: "plumbing.igniter-frpb", flangeR: [1.3, 2], bend: 4 });
679
+
680
+ const BLOCK_C = polar(52, 45, 228);
681
+ const BLOCK = { x: BLOCK_C[0] - 6, y: BLOCK_C[1] - 4, w: 12, d: 8, r: 1 };
682
+ const BLOCK_MOVE = PIECES.controller.move;
683
+ boxAt("plumbing", BLOCK, BLOCK_C[2] - 6, 12, {}, {
684
+ name: "plumbing.block",
685
+ move: BLOCK_MOVE,
686
+ bias: 0.5,
687
+ extra: Ln(k.planOutline(k.insetPlan(BLOCK, 1.2), BLOCK_C[2] + 6, P, 2), { tone: "faint" }) + Dots([0, 1, 2, 3].map((index) => at([BLOCK.x + 2 + index * 2.6, BLOCK.y + BLOCK.d, BLOCK_C[2] + 2])), { size: 0.5, tone: "hi" }),
688
+ });
689
+ const BLOCK_LOW = BLOCK_C[2] - 6;
690
+ const BLOCK_TOP = BLOCK_C[2] + 6;
691
+ const below = (x) => [[x, BLOCK.y + 10.5, BLOCK_LOW - 15], [x, BLOCK.y + 6, BLOCK_LOW - 7], [x, BLOCK.y + 6, BLOCK_LOW]];
692
+ const above = (x) => [[x, BLOCK.y + 4, BLOCK_TOP + 6], [x, BLOCK.y + 4, BLOCK_TOP]];
693
+ const SENSE_DEG = 40;
694
+ const SENSE_BOSS = boss("chamber", domeAt(SENSE_DEG, 219).at, domeAt(SENSE_DEG, 219).normal, 1.7, 1.2, { name: "chamber.sense-boss", host: DOME });
695
+ const MANIFOLD_RUNS = R.tubes.filter((tube) => tube.owner === "nozzle.manifold");
696
+ const MANIFOLD_TOP = polar(64, MANIFOLD_R, MANIFOLD_S + MANIFOLD_TUBE);
697
+ const MANIFOLD_SENSE = boss("nozzle", MANIFOLD_TOP, [0, 0, 1], 1.8, 0.8, { host: tubeAt(MANIFOLD_RUNS, MANIFOLD_TOP), ring: false, name: "nozzle.sense-pad" });
698
+ const SENSE0_FACE = boss("chamber", polar(56, 29.4, 180), dir(56), 1.3, 1.4, { host: JACKETS[1], name: "chamber.sense-port" });
699
+ const SENSE0_TEE = G.add3(SENSE0_FACE, G.mul3(dir(56), Math.sqrt(1.7 ** 2 - 1.3 ** 2)));
700
+ ballAt("chamber", SENSE0_TEE, 1.7, { tone: "hi", name: "chamber.sense-tee", owner: "chamber.sense-port" });
701
+ const SENSE0_OUT = G.unit3(G.add3(dir(56), [0, 0, 1]));
702
+ const SENSE0_TIP = G.add3(SENSE0_TEE, G.mul3(SENSE0_OUT, Math.sqrt(1.7 ** 2 - (0.75 * 1.3) ** 2)));
703
+ const senses = [
704
+ [SENSE0_TIP, G.add3(SENSE0_TIP, G.mul3(SENSE0_OUT, 20)), ...below(BLOCK.x + 4)],
705
+ [MANIFOLD_SENSE, polar(64, MANIFOLD_R, 124), polar(64, 50, 150), polar(62, 50, 190), ...below(BLOCK.x + 1.5)],
706
+ [elbow("chamber", SENSE_BOSS, domeAt(SENSE_DEG, 219).normal, 1.7, [BLOCK.x + 6, BLOCK.y - 6, BLOCK_LOW + 5], 2.1, 0.75 * 1.3, { name: "chamber.sense-elbow", owner: "chamber.sense-boss" }), [BLOCK.x + 6, BLOCK.y - 6, BLOCK_LOW + 5], [BLOCK.x + 6, BLOCK.y, BLOCK_LOW + 5]],
707
+ [FTP_SENSE, G.add3(FTP_SENSE, G.mul3(dir(100), 6)), polar(20, 62, 205), ...below(BLOCK.x + 9)],
708
+ [OTP_SENSE, G.add3(OTP_SENSE, G.mul3(dir(80), 3)), polar(78, 44, 258), ...above(BLOCK.x + 2.5)],
709
+ [elbow("orpb", ORPB_SENSE, dir(20), 2, polar(34, 36, 239), 2.4, 0.75 * 1.3, { name: "orpb.sense-elbow", owner: "orpb.sense-boss" }), polar(34, 36, 239), ...above(BLOCK.x + 6)],
710
+ [FTP_PUMP_SENSE, G.add3(FTP_PUMP_SENSE, G.mul3(dir(110), 4)), polar(-5, 58, 258), polar(40, 44, 256), ...above(BLOCK.x + 9.5)],
711
+ ];
712
+ senses.forEach((points, index) => {
713
+ line(points, 0.75, BLOCK_MOVE, { name: `plumbing.sense${index}`, flangeR: [0, 2, 5].includes(index) ? [1.3, 2.2] : 2.2, bend: 4, flanges: [true, false], free: [true, false] });
714
+ const end = points[points.length - 1];
715
+ if (end[2] > BLOCK_C[2]) put("plumbing", end, Dots([at(end)], { size: 0.5, tone: "hi" }), { move: BLOCK_MOVE, bias: 0.4, name: `plumbing.port${index}`, shapes: [A.ball(end, 0.5 / KS)] });
716
+ });
717
+
718
+ const TRANSDUCERS = [
719
+ { face: JACKET_BOSS[110], axis: dir(110), to: { host: RINGS[0], deg: 110, s: 172.6, r: 31.6 } },
720
+ { face: JACKET_BOSS[250], axis: dir(250), to: { host: RINGS[0], deg: 250, s: 172.6, r: 31.6 } },
721
+ { face: DOME_BOSS[-100].face, axis: DOME_BOSS[-100].normal, to: { host: INJECTOR, deg: -100, s: 207.5, r: 37.5 } },
722
+ ];
723
+ TRANSDUCERS.forEach(({ face, axis, to }) => {
724
+ const F = G.frameAlong(face, axis);
725
+ const meridian = [[0, 0], [0, 1.9], [1.2, 2.2], [5.2, 2.2], [6, 1.5], [6, 0]];
726
+ const id = nameFor("plumbing.transducer");
727
+ put("plumbing", G.add3(face, G.mul3(axis, 3)), S(G.lathe(meridian, F, P, { steps: 20, smooth: { rows: 3 } }), { tone: "hi" }), { move: PIECES.chamber.move, bias: 0.3, name: id, shapes: [record(A.body(F, meridian, { name: id }))] });
728
+ const tip = G.add3(face, G.mul3(axis, 6));
729
+ const socket = boss("plumbing", polar(to.deg, to.r, to.s), dir(to.deg), 1.2, 2, { host: to.host, move: PIECES.chamber.move, name: "plumbing.socket" });
730
+ const out1 = G.add3(tip, G.mul3(axis, 3));
731
+ const near = G.add3(socket, G.mul3(dir(to.deg), 3));
732
+ const reach = Math.max(Math.hypot(out1[0], out1[1]), Math.hypot(near[0], near[1])) + 2;
733
+ cable("plumbing", [tip, out1, polar(to.deg, reach, out1[2] - Z_EXIT), polar(to.deg, reach, to.s), near, socket], { move: PIECES.chamber.move, r: 0.5, bend: 1.6, name: `plumbing.pigtail`, shells: [true, false] });
734
+ });
735
+
736
+
737
+ const BOX = { x: -24, y: 46, w: 26, d: 12, r: 1.6 };
738
+ const BOX_Z = Z_EXIT + 230;
739
+ const BOX_H = 28;
740
+ const CTRL = PIECES.controller.move;
741
+ boxAt("controller", BOX, BOX_Z, BOX_H, { inner: Ln(k.sideSeam(BOX, BOX_Z + BOX_H - 6, P, 3), { tone: "faint" }) }, {
742
+ name: "controller.box",
743
+ bevel: 0.8,
744
+ extra:
745
+ Ln(k.planOutline(k.insetPlan(BOX, 2), BOX_Z + BOX_H, P, 3), { tone: "faint" }) +
746
+ Ln([0, 1, 2].map((row) => k.onTop([[BOX.x + 4, BOX.y + 3.4 + row * 2.4], [BOX.x + (row === 1 ? 14 : 19), BOX.y + 3.4 + row * 2.4]], BOX_Z + BOX_H, P)).join(""), { tone: "lo", free: true }) +
747
+ Dots(k.corners(BOX, 2.2).map(([x, y]) => at([x, y, BOX_Z + BOX_H])), { size: 0.45 }),
748
+ });
749
+ const ARM = { x: -2, w: 4, z: Z_EXIT + 242.5, h: 3 };
750
+ const ARM_Y = Math.sqrt(25.5 ** 2 - (ARM.w / 2) ** 2);
751
+ boxAt("controller", { x: ARM.x, y: ARM_Y, w: ARM.w, d: BOX.y - ARM_Y, r: 0.4 }, ARM.z, ARM.h, {}, { bias: -1, bevel: 0.3, name: "controller.bracket" });
752
+ const JBOX = { x: -38, y: 46, w: 10, d: 10, r: 1.2 };
753
+ boxAt("controller", JBOX, Z_EXIT + 244, 12, {}, { name: "controller.jbox", extra: Ln(k.planOutline(k.insetPlan(JBOX, 1.4), Z_EXIT + 256, P, 2), { tone: "faint" }) + Dots(k.corners(JBOX, 1.8).map(([x, y]) => at([x, y, Z_EXIT + 256])), { size: 0.4 }) });
754
+ boxAt("controller", { x: JBOX.x + JBOX.w, y: JBOX.y + 3, w: BOX.x - JBOX.x - JBOX.w, d: 4, r: 0.3 }, Z_EXIT + 248, 4, {}, { bias: -0.5, bevel: 0.2, name: "controller.jbox-bracket" });
755
+ const CONNECTORS = [
756
+ [BOX.x + 4, BOX_Z + 6],
757
+ [BOX.x + 10, BOX_Z + 6],
758
+ [BOX.x + 16, BOX_Z + 6],
759
+ [BOX.x + 22, BOX_Z + 6],
760
+ [BOX.x + 8, BOX_Z + 17],
761
+ [BOX.x + 18, BOX_Z + 17],
762
+ ];
763
+ const CONNECT_Y = BOX.y + BOX.d;
764
+ for (const [x, z] of CONNECTORS) {
765
+ const F = G.frameOf([x, CONNECT_Y, z], [0, 1, 0], [1, 0, 0], [0, 0, 1]);
766
+ const id = nameFor("controller.connector");
767
+ put("controller", [x, CONNECT_Y + 2, z], S(G.disc(0, 4, 2.4, F, P), { tone: "hi" }) + Ln(G.circleOf(F, 4, 1.2, P, 14), { tone: "faint" }), { bias: 0.4, name: id, shapes: [record(A.disc(F, 0, 4, 2.4, { name: id }))] });
768
+ }
769
+ const plug = (index) => [CONNECTORS[index][0], CONNECT_Y + 4, CONNECTORS[index][1]];
770
+ const HARNESS_S = 214.6;
771
+ const UNDER = Z_EXIT + 222;
772
+ const DROP_Y = CONNECT_Y + 8.5;
773
+ const laneR = (lane) => 37.6 + lane * 1.4;
774
+ const laneS = (lane) => HARNESS_S + lane * 0.2;
775
+ const harnessRoute = (index, lane, to) => {
776
+ const start = plug(index);
777
+ const x = start[0];
778
+ const entry = Math.acos(x / laneR(lane)) / DEG;
779
+ const ring = ringAt(laneS(lane), laneR(lane), entry - 5, to.end, Math.max(2, Math.round((entry - 5 - to.end) / 10)));
780
+ return [start, [x, DROP_Y, start[2]], [x, DROP_Y, UNDER], [x, Math.sqrt(laneR(lane) ** 2 - x * x), UNDER], ...ring, ...to.points];
781
+ };
782
+ const DROP_IN = G.unit3([-DOWN_ELBOW[0], -DOWN_ELBOW[1], 0]);
783
+ const besideDrop = (s) => [DOWN_ELBOW[0] + DROP_IN[0] * (DOWN_R * 1.26 + 1.1), DOWN_ELBOW[1] + DROP_IN[1] * (DOWN_R * 1.26 + 1.1), Z_EXIT + s];
784
+ const PICKUP_ROUTE = [polar(0, 58, 228), G.add3(FTP_PICKUP, G.add3(G.mul3(dir(PICKUP_DEG), 8), G.mul3(sideOn(dir(PICKUP_DEG)), 9))), G.add3(FTP_PICKUP, G.mul3(dir(PICKUP_DEG), 8)), FTP_PICKUP];
785
+ const HARNESS = [
786
+ { name: "harness-frpb", index: 3, lane: 0, to: { end: 8, points: [polar(5, 50, 212), polar(2, 66, 196), ftp(96, 28, 186), ftp(56, 22, 174), G.add3(FRPB_EXCITER, G.mul3(FRPB_EXCITER_AT.normal, 7)), FRPB_EXCITER] } },
787
+ { name: "harness-pickup", index: 2, lane: 1, to: { end: 12, points: PICKUP_ROUTE } },
788
+ { name: "harness-mfv", index: 1, lane: 2, to: { end: 16, points: [polar(16, laneR(2), 268), [MFV.approach[0], MFV.approach[1], Z_EXIT + 268], MFV.approach, MFV.port] } },
789
+ { name: "cable-block", index: 0, lane: 3, to: { end: 55, points: [[BLOCK.x + 2, BLOCK.y + 2.5, BLOCK_LOW]] } },
790
+ ];
791
+ const CLAMPS = [88, 70, 34, 24].map((deg) => {
792
+ const lanes = deg > 55 ? 4 : 3;
793
+ return { deg, centre: polar(deg, (laneR(0) + laneR(lanes - 1)) / 2, (laneS(0) + laneS(lanes - 1)) / 2), axis: G.unit3(G.cross3(dir(deg), [0, 0, 1])), radius: (laneR(lanes - 1) - laneR(0)) / 2 + 1.25 };
794
+ });
795
+ for (const { name, index, lane, to } of HARNESS) cable("controller", harnessRoute(index, lane, to), { move: CTRL, bend: 4, name: `controller.${name}`, bundle: "harness", touch: ["controller.clamp*"], clamps: CLAMPS, shells: [false, true], free: [false, name !== "cable-block"] });
796
+ for (const { deg, centre, axis, radius } of CLAMPS) {
797
+ clampAt("controller", centre, axis, radius, { move: CTRL, bundle: "harness" });
798
+ const s = centre[2] - Z_EXIT;
799
+ const inner = Math.hypot(centre[0], centre[1]) - radius;
800
+ const root = polar(deg, radiusOn(DOME_PROFILE, s), s);
801
+ boss("chamber", root, dir(deg), 0.9, inner - radiusOn(DOME_PROFILE, s), { host: DOME, ring: false, name: "chamber.clamp-stud" });
802
+ }
803
+ const OVER = BOX_Z + BOX_H + 4;
804
+ cable("controller", [plug(5), [plug(5)[0], DROP_Y, plug(5)[2]], [plug(5)[0], DROP_Y, OVER], [plug(5)[0], BOX.y - 4, OVER], polar(104, 36, 240), polar(100, 30, 228.5), ORPB_EXCITER], { move: CTRL, name: "controller.cable-orpb", shells: [false, true], free: [false, true] });
805
+ const MOV_IN = G.add3(MOV.port, G.mul3(dir(110), 9));
806
+ cable("controller", [plug(4), [plug(4)[0], DROP_Y, plug(4)[2]], [MOV_IN[0], DROP_Y, MOV_IN[2]], MOV_IN, MOV.port], { move: CTRL, name: "controller.cable-mov", shells: [false, true], free: [false, true] });
807
+ cable("controller", [[JBOX.x + JBOX.w / 2, JBOX.y + JBOX.d / 2, Z_EXIT + 256], [JBOX.x + JBOX.w / 2, JBOX.y + JBOX.d / 2, Z_EXIT + 265], [BOX.x + 3, BOX.y + BOX.d / 2, Z_EXIT + 265], [BOX.x + 3, BOX.y + BOX.d / 2, BOX_Z + BOX_H]], { move: CTRL, name: "controller.cable-jbox", bend: 3 });
808
+ cable("stand", [[JBOX.x + 3, JBOX.y + 3, Z_EXIT + 256], [JBOX.x + 3, JBOX.y + 3, BEAM_Z]], { name: "stand.cable-press", shells: [true, true], free: [true, false] });
809
+
810
+
811
+ const FEED_TOP = BEAM_Z - 6;
812
+ pipe("stand", [ex(FTP, 265), ex(FTP, 290), [FTP_AT[0], FTP_AT[1], FEED_TOP]], 7.2, { bend: 2, bellows: [0.12, 0.32, 7], bands: [0.62], maxLength: 14, name: "stand.feed", free: [true, false] });
813
+ const BRACKET = { x: FTP_AT[0] - 13, y: FTP_AT[1] - 13, w: 26, d: 26, r: 2 };
814
+ boxAt("stand", BRACKET, FEED_TOP, 6, {}, { bias: 2, name: "stand.feed-bracket", extra: Dots(k.corners(BRACKET, 3).map(([x, y]) => at([x, y, FEED_TOP + 6])), { size: 0.45 }) });
815
+ for (const [deg, reach] of [[135, 50], [-45, 46]]) {
816
+ const quick = polar(deg, reach, TOP + 21);
817
+ boxAt("stand", { x: quick[0] - 4, y: quick[1] - 4, w: 8, d: 8, r: 1 }, quick[2], BEAM_Z - quick[2], {}, { bias: 1, name: "stand.quick-disconnect" });
818
+ }
819
+
820
+ const CLEVIS_BALL = 4.6;
821
+ for (const [deg, foot] of LUGS) {
822
+ const lug = lugBall(foot);
823
+ const top = [Math.cos(deg * DEG) * 92, Math.sin(deg * DEG) * 92, BEAM_Z];
824
+ const id = nameFor("stand.actuator");
825
+ boxAt("stand", { x: top[0] - 5, y: top[1] - 5, w: 10, d: 10, r: 1 }, BEAM_Z - 3, 3, {}, { name: `${id}.mount`, bevel: 0.4 });
826
+ const pivot = G.add3(top, [0, 0, -3 - CLEVIS_BALL]);
827
+ ballAt("stand", pivot, CLEVIS_BALL, { tone: "hi", name: `${id}.pivot`, owner: id });
828
+ const axis = G.unit3(G.sub3(lug, pivot));
829
+ const span = G.len3(G.sub3(lug, pivot));
830
+ const F = G.frameAlong(pivot, axis);
831
+ const clevisFrom = Math.sqrt(CLEVIS_BALL ** 2 - 3 ** 2);
832
+ put("stand", G.add3(pivot, G.mul3(axis, 5)), S(G.disc(clevisFrom, 9, 3, F, P), { tone: "mid" }), { name: `${id}.clevis`, shapes: [record(A.disc(F, clevisFrom, 9, 3, { name: `${id}.clevis`, owner: id }))] });
833
+ const side = G.unit3(G.cross3(axis, [0, 0, 1]));
834
+ const SF = { o: pivot, a: axis, u: side, v: G.cross3(axis, side) };
835
+ const head = [[9, 0], [9, 6.2], [11, 6.6], [13, 6.6], [13, 0]];
836
+ const barrel = [[15, 0], [15, 6.6], [35, 6.6], [37, 6.2], [39, 4], [39, 0]];
837
+ put("stand", G.add3(pivot, G.mul3(axis, 11)), S(G.lathe(head, F, P, { steps: 40 }), { tone: "hi" }), { name: `${id}.head`, shapes: [record(A.body(F, head, { name: `${id}.head`, owner: id }))] });
838
+ put("stand", G.add3(pivot, G.mul3(axis, 14)), S(G.prismOf(SF, G.stadium(7.4, 5, 10.9), 13, 15, P), { tone: "mid" }) + Dots([G.pointOf(SF, 15, 0, 0), G.add3(G.pointOf(SF, 15, 0, 0), G.mul3(side, 10.9))].map((p) => at(p)), { size: 0.42 }), { name: `${id}.gearbox`, shapes: [record(A.disc(F, 13, 15, 7.4, { name: `${id}.gearbox`, owner: id })), record(A.disc(G.frameAlong(G.add3(pivot, G.mul3(side, 10.9)), axis), 13, 15, 5, { name: `${id}.gearbox`, owner: id })), record(A.orientedBox(G.add3(G.add3(pivot, G.mul3(axis, 14)), G.mul3(side, 5.45)), [side, SF.v, axis], [5.45, 5, 1], { name: `${id}.gearbox`, owner: id }))] });
839
+ put("stand", G.add3(pivot, G.mul3(axis, 25)), S(G.lathe(barrel, F, P, { steps: 40 }), { tone: "hi", inner: Ln([21, 27, 32].map((s) => G.arcOf(F, s, 6.6, P)).join(""), { tone: "faint" }) }), { name: `${id}.barrel`, shapes: [record(A.body(F, barrel, { name: `${id}.barrel`, owner: id }))] });
840
+ const MF = G.frameAlong(G.add3(pivot, G.mul3(side, 10.9)), axis);
841
+ put("stand", G.pointOf(MF, 22, 0, 0), S(G.disc(15, 29, 4.2, MF, P), { tone: "mid" }) + Faded(G.ribsOf(MF, 15, 29, 4.2, 14, P, { fade: [0.1, 0.55] }), { tone: "lo" }), { bias: -0.5, name: `${id}.motor`, shapes: [record(A.disc(MF, 15, 29, 4.2, { name: `${id}.motor`, owner: id }))] });
842
+ const rodEnd = span - Math.sqrt(LUG_BALL ** 2 - 2.1 ** 2);
843
+ put("stand", G.add3(pivot, G.mul3(axis, (39 + rodEnd) / 2)), S(G.disc(39, rodEnd, 2.1, F, P), { tone: "hi" }), { name: `${id}.rod`, shapes: [record(A.disc(F, 39, rodEnd, 2.1, { name: `${id}.rod`, owner: id }))] });
844
+ ballAt("stand", lug, LUG_BALL, { name: `${id}.eye`, owner: id });
845
+ }
846
+
847
+ const BEAM_HALF = 14;
848
+ const along = G.unit3([1, -1, 0]);
849
+ const across = G.unit3([1, 1, 0]);
850
+ const beamEnd = 196;
851
+ const beamRing = [
852
+ G.add3(G.mul3(along, -beamEnd), G.mul3(across, -BEAM_HALF)),
853
+ G.add3(G.mul3(along, beamEnd), G.mul3(across, -BEAM_HALF)),
854
+ G.add3(G.mul3(along, beamEnd), G.mul3(across, BEAM_HALF)),
855
+ G.add3(G.mul3(along, -beamEnd), G.mul3(across, BEAM_HALF)),
856
+ ].map(([x, y]) => [x, y]);
857
+ const beamTop = BEAM_Z + BEAM_H;
858
+ const beamLine = (offset, z) => k.onTop([G.add3(G.mul3(along, -beamEnd), G.mul3(across, offset)), G.add3(G.mul3(along, beamEnd), G.mul3(across, offset))].map(([x, y]) => [x, y]), z, P);
859
+ const beamBolts = [];
860
+ for (let t = -176; t <= 176; t += 32) for (const side of [-1, 1]) beamBolts.push(at([...G.add3(G.mul3(along, t), G.mul3(across, side * 8.5)).slice(0, 2), beamTop]));
861
+ const beamFace = [];
862
+ for (let t = -168; t <= 168; t += 56) {
863
+ const p = G.add3(G.mul3(along, t), G.mul3(across, BEAM_HALF));
864
+ beamFace.push(k.segment([p[0], p[1], BEAM_Z], [p[0], p[1], beamTop], P));
865
+ }
866
+ put("stand", [0, 0, BEAM_Z - 13], S(k.cylinder(0, 0, 12.6, Z_EXIT + TOP, BEAM_Z - 8 - Z_EXIT - TOP, P, 40), { tone: "mid" }) + Ln(k.knurl(0, 0, 12.6, Z_EXIT + TOP + 4, Z_EXIT + TOP + 10, 40, P), { tone: "lo" }) + Ln(k.sideArc(0, 0, 12.6, Z_EXIT + TOP + 11.5, P, 24), { tone: "faint" }), { bias: 50, name: "stand.loadcell", shapes: [record(A.disc(DECKF, Z_EXIT + TOP, BEAM_Z - 8, 12.6, { name: "stand.loadcell" }))] });
867
+ put("stand", [0, 0, BEAM_Z - 4], S(k.cylinder(0, 0, 34, BEAM_Z - 8, 8, P, 72, 0.8), { tone: "mid" }) + Dots(G.dotsOf(DECKF, BEAM_Z, 29.5, 16, P, { fade: FADE }), { size: 0.4 }), { bias: 60, name: "stand.crosshead", shapes: [record(A.disc(DECKF, BEAM_Z - 8, BEAM_Z, 34, { name: "stand.crosshead" }))] });
868
+ const GLAND_DEG = AZIMUTH - 90;
869
+ const gland = [Math.cos(GLAND_DEG * DEG) * 12.6, Math.sin(GLAND_DEG * DEG) * 12.6, Z_EXIT + TOP + 7];
870
+ put("stand", gland, S(G.disc(0, 4, 2, G.frameAlong(gland, dir(GLAND_DEG)), P), { tone: "mid" }), { bias: 61, name: "stand.gland", shapes: [record(A.disc(G.frameAlong(gland, dir(GLAND_DEG)), 0, 4, 2, { name: "stand.gland" }))] });
871
+ tubeItems("stand", G.fillet([G.add3(gland, G.mul3(dir(GLAND_DEG), 4)), G.add3(gland, G.mul3(dir(GLAND_DEG), 11)), [...G.add3(gland, G.mul3(dir(GLAND_DEG), 11)).slice(0, 2), BEAM_Z - 8]], 4), 0.62, { name: "stand.loadcell-cable", tone: "mid", bias: 61, maxLength: 60, spacing: 1 });
872
+ put("stand", [0, 0, BEAM_Z + BEAM_H / 2], S(k.extrude(beamRing, BEAM_Z, BEAM_H, P, { bevel: 0.9 }), { tone: "mid" }) + Ln(beamLine(-BEAM_HALF + 5, beamTop) + beamLine(BEAM_HALF - 5, beamTop), { tone: "lo" }) + Ln(beamFace.join(""), { tone: "faint" }) + Dots(beamBolts, { size: 0.45 }), { bias: 200, name: "stand.beam", shapes: [record(A.orientedBox([0, 0, BEAM_Z + BEAM_H / 2], [along, across, [0, 0, 1]], [beamEnd, BEAM_HALF, BEAM_H / 2], { name: "stand.beam" }))] });
873
+
874
+
875
+ PIECES.plumbing.say = `plumbing · ${senses.length} sense lines into a block · ${LINES.length - senses.length} feed, purge and igniter lines`;
876
+ PIECES.controller.say = `engine controller · ${CABLES.filter((entry) => entry.piece === "controller").length} cables`;
877
+
878
+ const CROWD_OK = [["plumbing.lox-to-frpb", "controller.cable-block"], ["plumbing.lox-to-frpb", "controller.harness-frpb"]];
879
+ A.settle(R, P);
880
+ items.sort((a, b) => a.key - b.key);
881
+ const groups = items.map((item, index) => {
882
+ const [mx, my, mz] = item.move;
883
+ return `<g class="it" data-piece="${item.piece}"${item.ride !== item.piece ? ` data-ride="${item.ride}"` : ""} data-k="${item.key.toFixed(2)}" data-m="${mx.toFixed(1)},${my.toFixed(1)},${mz.toFixed(1)}" data-i="${index}">${item.svg}</g>`;
884
+ });
885
+
886
+ const LABEL_TEXT =
887
+ "SpaceX Raptor 2 hanging from a vertical test stand: a deck on feet with a flame cone and deluge ring, two columns, and a crosshead with a load cell and two gimbal actuators. Below the gimbal sit the oxygen turbopump, the oxygen-rich preburner, the injector and main chamber, and the bell nozzle with its cooling manifold; the methane turbopump and fuel-rich preburner hang on the right, joined by the hot-gas manifold, a downcomer, ducts, sense lines and the engine controller's harness.";
888
+
889
+ const backSvg = k.figureSvg({ width: W, height: H, label: LABEL_TEXT, body: back, className: "rp-back" });
890
+ const frontSvg = `<svg xmlns="http://www.w3.org/2000/svg" class="iso-svg rp-front" viewBox="0 0 ${W} ${H}" aria-hidden="true"><g class="rp-engine">${groups.join("")}</g></svg>`;
891
+
892
+ const unit = (vector) => {
893
+ const o = at([0, 0, 0]);
894
+ const p = at(vector);
895
+ return [p[0] - o[0], p[1] - o[1]];
896
+ };
897
+ const DATA = {
898
+ W,
899
+ H,
900
+ V,
901
+ EX: unit([1, 0, 0]),
902
+ EY: unit([0, 1, 0]),
903
+ EZ: unit([0, 0, 1]),
904
+ camera: glCamera(P),
905
+ exit: at([0, 0, Z_EXIT]),
906
+ zExit: Z_EXIT,
907
+ exitR: 64.6,
908
+ coneTip: CONE_TIP,
909
+ plumeLength: Z_EXIT - CONE_TIP,
910
+ chamberBar: CHAMBER_BAR,
911
+ vacuumTf: SEA_LEVEL_TF + BACK_TF,
912
+ backTf: BACK_TF,
913
+ columns: COLUMN_AT,
914
+ columnHalf: COLUMN / 2 + 0.4,
915
+ columnTop: BEAM_Z + BEAM_H,
916
+ pieces: Object.fromEntries(Object.entries(PIECES).map(([id, piece]) => [id, { name: piece.name, say: piece.say }])),
917
+ stagger: STAGGER,
918
+ spring: SPRING,
919
+ order: ["gimbal", "otp", "orpb", "lox", "controller", "chamber", "hotgas", "ftp", "downcomer", "plumbing", "nozzle"],
920
+ };
921
+
922
+ const PARTS = DATA.order.map((id, index) => `<button type="button" class="rp-part" data-part="${id}"><span class="rp-num">${String(index + 1).padStart(2, "0")}</span>${PIECES[id].name}</button>`).join("");
923
+ const corners = `<div class="iso-plate-corners" aria-hidden="true"><span class="iso-plate-corner" data-corner="fig">Fig 1</span><span class="iso-plate-corner" data-corner="title">Raptor 2 · test stand</span><span class="iso-plate-corner" data-corner="readout" data-readout>1,630 kg · cold</span></div>`;
924
+ const controls = `<div class="rp-controls"><button type="button" class="rp-fire" data-fire><span class="rp-lamp" aria-hidden="true"></span><span data-fire-label>Ignite</span></button><span class="rp-hint">Hover the engine to take it apart</span><span class="rp-live" aria-live="polite" data-live></span></div>`;
925
+ const glowSvg = `<svg xmlns="http://www.w3.org/2000/svg" class="iso-svg rp-glow" viewBox="0 0 ${W} ${H}" aria-hidden="true"><g class="rp-glow-g"><path class="rp-rim rp-rim-soft" d="${LIP}"/><path class="rp-rim" d="${LIP}"/></g></svg>`;
926
+ const stage = `<div class="rp-stage" tabindex="0" role="slider" aria-label="Raptor 2 on a test stand. Hover or use the arrow keys to take the engine apart part by part; press I or the Ignite button to fire it, Escape to put it back together." aria-valuemin="0" aria-valuemax="${DATA.order.length}" aria-valuenow="0" aria-valuetext="Assembled, cold">${backSvg}<canvas class="rp-plume" aria-hidden="true"></canvas>${frontSvg}${glowSvg}<canvas class="rp-veil" aria-hidden="true"></canvas><div class="rp-flash" aria-hidden="true"></div></div>`;
927
+ const caption =
928
+ "Raptor 2 is SpaceX's full-flow staged-combustion engine: liquid oxygen and methane at a mixture ratio of 3.6, 300 bar in the main chamber, 230 tf at sea level, 1,630 kg dry. Every drop of oxygen passes through an oxygen-rich preburner that spins the oxygen pump, and every drop of methane through a fuel-rich preburner that spins the methane pump; the two hot gases meet in the main chamber, which has no igniter of its own. The load cell above the gimbal reads the thrust while it burns.";
929
+ const body = `<figure class="iso-figure rp"><div class="iso-plate">${corners}${stage}${controls}</div><figcaption class="iso-legend"><span class="rp-parts">${PARTS}</span><span class="iso-caption">${caption}</span></figcaption></figure>`;
930
+
931
+ const CSS = readFileSync(join(HERE, "raptor.css"), "utf8");
932
+ const LIVE = readFileSync(join(HERE, "live.js"), "utf8").replace("__DATA__", JSON.stringify(DATA));
933
+ const page = k.pageHtml({ title: "Raptor 2", theme: THEME, body: `<style>${CSS}</style>${body}`, script: glScript() + LIVE, width: 980 });
934
+ const name = THEME === "light" ? "raptor-engine-light.html" : "raptor-engine.html";
935
+ writeFileSync(join(HERE, name), page);
936
+ console.log("wrote", name, `${(page.length / 1024).toFixed(0)} KB`, `${items.length} items`);
937
+
938
+ A.auditOrExit(R, P, { crowd: CROWD_OK });