@johnmorrisdotca/uchusen-rockets 0.0.0-stage → 0.2.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 (340) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE +21 -0
  3. package/LICENSES/CC-BY-4.0.txt +396 -0
  4. package/LICENSES/MIT.txt +21 -0
  5. package/NOTICE.md +66 -0
  6. package/README.md +171 -2
  7. package/dist/assets/dragon-dragon-1-apart.painted.svg +1 -0
  8. package/dist/assets/dragon-dragon-1-apart.svg +1 -0
  9. package/dist/assets/dragon-dragon-1-apart.thick.svg +1 -0
  10. package/dist/assets/dragon-dragon-1-capsule.glb +0 -0
  11. package/dist/assets/dragon-dragon-1-capsule.painted.svg +1 -0
  12. package/dist/assets/dragon-dragon-1-capsule.svg +1 -0
  13. package/dist/assets/dragon-dragon-1-capsule.thick.svg +1 -0
  14. package/dist/assets/dragon-dragon-1-trunk.glb +0 -0
  15. package/dist/assets/dragon-dragon-1-trunk.painted.svg +1 -0
  16. package/dist/assets/dragon-dragon-1-trunk.svg +1 -0
  17. package/dist/assets/dragon-dragon-1-trunk.thick.svg +1 -0
  18. package/dist/assets/dragon-dragon-1.glb +0 -0
  19. package/dist/assets/dragon-dragon-1.painted.svg +1 -0
  20. package/dist/assets/dragon-dragon-1.svg +1 -0
  21. package/dist/assets/dragon-dragon-1.thick.svg +1 -0
  22. package/dist/assets/dragon-dragon-2-apart.painted.svg +1 -0
  23. package/dist/assets/dragon-dragon-2-apart.svg +1 -0
  24. package/dist/assets/dragon-dragon-2-apart.thick.svg +1 -0
  25. package/dist/assets/dragon-dragon-2-capsule.glb +0 -0
  26. package/dist/assets/dragon-dragon-2-capsule.painted.svg +1 -0
  27. package/dist/assets/dragon-dragon-2-capsule.svg +1 -0
  28. package/dist/assets/dragon-dragon-2-capsule.thick.svg +1 -0
  29. package/dist/assets/dragon-dragon-2-trunk.glb +0 -0
  30. package/dist/assets/dragon-dragon-2-trunk.painted.svg +1 -0
  31. package/dist/assets/dragon-dragon-2-trunk.svg +1 -0
  32. package/dist/assets/dragon-dragon-2-trunk.thick.svg +1 -0
  33. package/dist/assets/dragon-dragon-2.glb +0 -0
  34. package/dist/assets/dragon-dragon-2.painted.svg +1 -0
  35. package/dist/assets/dragon-dragon-2.svg +1 -0
  36. package/dist/assets/dragon-dragon-2.thick.svg +1 -0
  37. package/dist/assets/falcon-9-block-5-apart.painted.svg +1 -0
  38. package/dist/assets/falcon-9-block-5-apart.svg +1 -0
  39. package/dist/assets/falcon-9-block-5-apart.thick.svg +1 -0
  40. package/dist/assets/falcon-9-block-5-dragon-2-apart.painted.svg +1 -0
  41. package/dist/assets/falcon-9-block-5-dragon-2-apart.svg +1 -0
  42. package/dist/assets/falcon-9-block-5-dragon-2-apart.thick.svg +1 -0
  43. package/dist/assets/falcon-9-block-5-dragon-2-capsule.glb +0 -0
  44. package/dist/assets/falcon-9-block-5-dragon-2-capsule.painted.svg +1 -0
  45. package/dist/assets/falcon-9-block-5-dragon-2-capsule.svg +1 -0
  46. package/dist/assets/falcon-9-block-5-dragon-2-capsule.thick.svg +1 -0
  47. package/dist/assets/falcon-9-block-5-dragon-2-first-stage.glb +0 -0
  48. package/dist/assets/falcon-9-block-5-dragon-2-first-stage.painted.svg +1 -0
  49. package/dist/assets/falcon-9-block-5-dragon-2-first-stage.svg +1 -0
  50. package/dist/assets/falcon-9-block-5-dragon-2-first-stage.thick.svg +1 -0
  51. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.glb +0 -0
  52. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.painted.svg +1 -0
  53. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.svg +1 -0
  54. package/dist/assets/falcon-9-block-5-dragon-2-second-stage.thick.svg +1 -0
  55. package/dist/assets/falcon-9-block-5-dragon-2-trunk.glb +0 -0
  56. package/dist/assets/falcon-9-block-5-dragon-2-trunk.painted.svg +1 -0
  57. package/dist/assets/falcon-9-block-5-dragon-2-trunk.svg +1 -0
  58. package/dist/assets/falcon-9-block-5-dragon-2-trunk.thick.svg +1 -0
  59. package/dist/assets/falcon-9-block-5-dragon-2.glb +0 -0
  60. package/dist/assets/falcon-9-block-5-dragon-2.painted.svg +1 -0
  61. package/dist/assets/falcon-9-block-5-dragon-2.svg +1 -0
  62. package/dist/assets/falcon-9-block-5-dragon-2.thick.svg +1 -0
  63. package/dist/assets/falcon-9-block-5-fairing.glb +0 -0
  64. package/dist/assets/falcon-9-block-5-fairing.painted.svg +1 -0
  65. package/dist/assets/falcon-9-block-5-fairing.svg +1 -0
  66. package/dist/assets/falcon-9-block-5-fairing.thick.svg +1 -0
  67. package/dist/assets/falcon-9-block-5-first-stage.glb +0 -0
  68. package/dist/assets/falcon-9-block-5-first-stage.painted.svg +1 -0
  69. package/dist/assets/falcon-9-block-5-first-stage.svg +1 -0
  70. package/dist/assets/falcon-9-block-5-first-stage.thick.svg +1 -0
  71. package/dist/assets/falcon-9-block-5-second-stage.glb +0 -0
  72. package/dist/assets/falcon-9-block-5-second-stage.painted.svg +1 -0
  73. package/dist/assets/falcon-9-block-5-second-stage.svg +1 -0
  74. package/dist/assets/falcon-9-block-5-second-stage.thick.svg +1 -0
  75. package/dist/assets/falcon-9-block-5.glb +0 -0
  76. package/dist/assets/falcon-9-block-5.painted.svg +1 -0
  77. package/dist/assets/falcon-9-block-5.svg +1 -0
  78. package/dist/assets/falcon-9-block-5.thick.svg +1 -0
  79. package/dist/assets/falcon-9-ft-apart.painted.svg +1 -0
  80. package/dist/assets/falcon-9-ft-apart.svg +1 -0
  81. package/dist/assets/falcon-9-ft-apart.thick.svg +1 -0
  82. package/dist/assets/falcon-9-ft-fairing.glb +0 -0
  83. package/dist/assets/falcon-9-ft-fairing.painted.svg +1 -0
  84. package/dist/assets/falcon-9-ft-fairing.svg +1 -0
  85. package/dist/assets/falcon-9-ft-fairing.thick.svg +1 -0
  86. package/dist/assets/falcon-9-ft-first-stage.glb +0 -0
  87. package/dist/assets/falcon-9-ft-first-stage.painted.svg +1 -0
  88. package/dist/assets/falcon-9-ft-first-stage.svg +1 -0
  89. package/dist/assets/falcon-9-ft-first-stage.thick.svg +1 -0
  90. package/dist/assets/falcon-9-ft-second-stage.glb +0 -0
  91. package/dist/assets/falcon-9-ft-second-stage.painted.svg +1 -0
  92. package/dist/assets/falcon-9-ft-second-stage.svg +1 -0
  93. package/dist/assets/falcon-9-ft-second-stage.thick.svg +1 -0
  94. package/dist/assets/falcon-9-ft.glb +0 -0
  95. package/dist/assets/falcon-9-ft.painted.svg +1 -0
  96. package/dist/assets/falcon-9-ft.svg +1 -0
  97. package/dist/assets/falcon-9-ft.thick.svg +1 -0
  98. package/dist/assets/falcon-9-v1.0-apart.painted.svg +1 -0
  99. package/dist/assets/falcon-9-v1.0-apart.svg +1 -0
  100. package/dist/assets/falcon-9-v1.0-apart.thick.svg +1 -0
  101. package/dist/assets/falcon-9-v1.0-fairing.glb +0 -0
  102. package/dist/assets/falcon-9-v1.0-fairing.painted.svg +1 -0
  103. package/dist/assets/falcon-9-v1.0-fairing.svg +1 -0
  104. package/dist/assets/falcon-9-v1.0-fairing.thick.svg +1 -0
  105. package/dist/assets/falcon-9-v1.0-first-stage.glb +0 -0
  106. package/dist/assets/falcon-9-v1.0-first-stage.painted.svg +1 -0
  107. package/dist/assets/falcon-9-v1.0-first-stage.svg +1 -0
  108. package/dist/assets/falcon-9-v1.0-first-stage.thick.svg +1 -0
  109. package/dist/assets/falcon-9-v1.0-second-stage.glb +0 -0
  110. package/dist/assets/falcon-9-v1.0-second-stage.painted.svg +1 -0
  111. package/dist/assets/falcon-9-v1.0-second-stage.svg +1 -0
  112. package/dist/assets/falcon-9-v1.0-second-stage.thick.svg +1 -0
  113. package/dist/assets/falcon-9-v1.0.glb +0 -0
  114. package/dist/assets/falcon-9-v1.0.painted.svg +1 -0
  115. package/dist/assets/falcon-9-v1.0.svg +1 -0
  116. package/dist/assets/falcon-9-v1.0.thick.svg +1 -0
  117. package/dist/assets/falcon-9-v1.1-apart.painted.svg +1 -0
  118. package/dist/assets/falcon-9-v1.1-apart.svg +1 -0
  119. package/dist/assets/falcon-9-v1.1-apart.thick.svg +1 -0
  120. package/dist/assets/falcon-9-v1.1-fairing.glb +0 -0
  121. package/dist/assets/falcon-9-v1.1-fairing.painted.svg +1 -0
  122. package/dist/assets/falcon-9-v1.1-fairing.svg +1 -0
  123. package/dist/assets/falcon-9-v1.1-fairing.thick.svg +1 -0
  124. package/dist/assets/falcon-9-v1.1-first-stage.glb +0 -0
  125. package/dist/assets/falcon-9-v1.1-first-stage.painted.svg +1 -0
  126. package/dist/assets/falcon-9-v1.1-first-stage.svg +1 -0
  127. package/dist/assets/falcon-9-v1.1-first-stage.thick.svg +1 -0
  128. package/dist/assets/falcon-9-v1.1-second-stage.glb +0 -0
  129. package/dist/assets/falcon-9-v1.1-second-stage.painted.svg +1 -0
  130. package/dist/assets/falcon-9-v1.1-second-stage.svg +1 -0
  131. package/dist/assets/falcon-9-v1.1-second-stage.thick.svg +1 -0
  132. package/dist/assets/falcon-9-v1.1.glb +0 -0
  133. package/dist/assets/falcon-9-v1.1.painted.svg +1 -0
  134. package/dist/assets/falcon-9-v1.1.svg +1 -0
  135. package/dist/assets/falcon-9-v1.1.thick.svg +1 -0
  136. package/dist/assets/falcon-heavy-falcon-heavy-apart.painted.svg +1 -0
  137. package/dist/assets/falcon-heavy-falcon-heavy-apart.svg +1 -0
  138. package/dist/assets/falcon-heavy-falcon-heavy-apart.thick.svg +1 -0
  139. package/dist/assets/falcon-heavy-falcon-heavy-centre.glb +0 -0
  140. package/dist/assets/falcon-heavy-falcon-heavy-centre.painted.svg +1 -0
  141. package/dist/assets/falcon-heavy-falcon-heavy-centre.svg +1 -0
  142. package/dist/assets/falcon-heavy-falcon-heavy-centre.thick.svg +1 -0
  143. package/dist/assets/falcon-heavy-falcon-heavy-side-left.glb +0 -0
  144. package/dist/assets/falcon-heavy-falcon-heavy-side-left.painted.svg +1 -0
  145. package/dist/assets/falcon-heavy-falcon-heavy-side-left.svg +1 -0
  146. package/dist/assets/falcon-heavy-falcon-heavy-side-left.thick.svg +1 -0
  147. package/dist/assets/falcon-heavy-falcon-heavy-side-right.glb +0 -0
  148. package/dist/assets/falcon-heavy-falcon-heavy-side-right.painted.svg +1 -0
  149. package/dist/assets/falcon-heavy-falcon-heavy-side-right.svg +1 -0
  150. package/dist/assets/falcon-heavy-falcon-heavy-side-right.thick.svg +1 -0
  151. package/dist/assets/falcon-heavy-falcon-heavy.glb +0 -0
  152. package/dist/assets/falcon-heavy-falcon-heavy.painted.svg +1 -0
  153. package/dist/assets/falcon-heavy-falcon-heavy.svg +1 -0
  154. package/dist/assets/falcon-heavy-falcon-heavy.thick.svg +1 -0
  155. package/dist/assets/grasshopper-grasshopper.glb +0 -0
  156. package/dist/assets/grasshopper-grasshopper.painted.svg +1 -0
  157. package/dist/assets/grasshopper-grasshopper.svg +1 -0
  158. package/dist/assets/grasshopper-grasshopper.thick.svg +1 -0
  159. package/dist/assets/models/dragon-dragon-1-capsule.glb +0 -0
  160. package/dist/assets/models/dragon-dragon-1-trunk.glb +0 -0
  161. package/dist/assets/models/dragon-dragon-2-capsule.glb +0 -0
  162. package/dist/assets/models/dragon-dragon-2-trunk.glb +0 -0
  163. package/dist/assets/models/falcon-9-block-5-fairing.glb +0 -0
  164. package/dist/assets/models/falcon-9-block-5-first-stage.glb +0 -0
  165. package/dist/assets/models/falcon-9-block-5-second-stage.glb +0 -0
  166. package/dist/assets/models/falcon-heavy-falcon-heavy-centre.glb +0 -0
  167. package/dist/assets/models/falcon-heavy-falcon-heavy-side-left.glb +0 -0
  168. package/dist/assets/models/falcon-heavy-falcon-heavy-side-right.glb +0 -0
  169. package/dist/assets/models/grasshopper-grasshopper-vehicle.glb +0 -0
  170. package/dist/assets/models/starship-2019-ship.glb +0 -0
  171. package/dist/assets/models/starship-v1-flight1-booster.glb +0 -0
  172. package/dist/assets/models/starship-v1-flight1-ship.glb +0 -0
  173. package/dist/assets/models/starship-v3-booster.glb +0 -0
  174. package/dist/assets/models/starship-v3-ship.glb +0 -0
  175. package/dist/assets/parts/flap-aft.glb +0 -0
  176. package/dist/assets/parts/flap-aft.painted.svg +1 -0
  177. package/dist/assets/parts/flap-aft.svg +1 -0
  178. package/dist/assets/parts/flap-aft.thick.svg +1 -0
  179. package/dist/assets/parts/flap-forward.glb +0 -0
  180. package/dist/assets/parts/flap-forward.painted.svg +1 -0
  181. package/dist/assets/parts/flap-forward.svg +1 -0
  182. package/dist/assets/parts/flap-forward.thick.svg +1 -0
  183. package/dist/assets/parts/grid-fin-four.glb +0 -0
  184. package/dist/assets/parts/grid-fin-four.painted.svg +1 -0
  185. package/dist/assets/parts/grid-fin-four.svg +1 -0
  186. package/dist/assets/parts/grid-fin-four.thick.svg +1 -0
  187. package/dist/assets/parts/grid-fin-three.glb +0 -0
  188. package/dist/assets/parts/grid-fin-three.painted.svg +1 -0
  189. package/dist/assets/parts/grid-fin-three.svg +1 -0
  190. package/dist/assets/parts/grid-fin-three.thick.svg +1 -0
  191. package/dist/assets/parts/merlin-sea-level.glb +0 -0
  192. package/dist/assets/parts/merlin-sea-level.painted.svg +1 -0
  193. package/dist/assets/parts/merlin-sea-level.svg +1 -0
  194. package/dist/assets/parts/merlin-sea-level.thick.svg +1 -0
  195. package/dist/assets/parts/raptor-sea-level.glb +0 -0
  196. package/dist/assets/parts/raptor-sea-level.painted.svg +1 -0
  197. package/dist/assets/parts/raptor-sea-level.svg +1 -0
  198. package/dist/assets/parts/raptor-sea-level.thick.svg +1 -0
  199. package/dist/assets/parts/raptor-vacuum.glb +0 -0
  200. package/dist/assets/parts/raptor-vacuum.painted.svg +1 -0
  201. package/dist/assets/parts/raptor-vacuum.svg +1 -0
  202. package/dist/assets/parts/raptor-vacuum.thick.svg +1 -0
  203. package/dist/assets/starship-2019.glb +0 -0
  204. package/dist/assets/starship-2019.painted.svg +1 -0
  205. package/dist/assets/starship-2019.svg +1 -0
  206. package/dist/assets/starship-2019.thick.svg +1 -0
  207. package/dist/assets/starship-v1-apart.painted.svg +1 -0
  208. package/dist/assets/starship-v1-apart.svg +1 -0
  209. package/dist/assets/starship-v1-apart.thick.svg +1 -0
  210. package/dist/assets/starship-v1-booster.glb +0 -0
  211. package/dist/assets/starship-v1-booster.painted.svg +1 -0
  212. package/dist/assets/starship-v1-booster.svg +1 -0
  213. package/dist/assets/starship-v1-booster.thick.svg +1 -0
  214. package/dist/assets/starship-v1-flight1-apart.painted.svg +1 -0
  215. package/dist/assets/starship-v1-flight1-apart.svg +1 -0
  216. package/dist/assets/starship-v1-flight1-apart.thick.svg +1 -0
  217. package/dist/assets/starship-v1-flight1-booster.glb +0 -0
  218. package/dist/assets/starship-v1-flight1-booster.painted.svg +1 -0
  219. package/dist/assets/starship-v1-flight1-booster.svg +1 -0
  220. package/dist/assets/starship-v1-flight1-booster.thick.svg +1 -0
  221. package/dist/assets/starship-v1-flight1-ship.glb +0 -0
  222. package/dist/assets/starship-v1-flight1-ship.painted.svg +1 -0
  223. package/dist/assets/starship-v1-flight1-ship.svg +1 -0
  224. package/dist/assets/starship-v1-flight1-ship.thick.svg +1 -0
  225. package/dist/assets/starship-v1-flight1.glb +0 -0
  226. package/dist/assets/starship-v1-flight1.painted.svg +1 -0
  227. package/dist/assets/starship-v1-flight1.svg +1 -0
  228. package/dist/assets/starship-v1-flight1.thick.svg +1 -0
  229. package/dist/assets/starship-v1-ship.glb +0 -0
  230. package/dist/assets/starship-v1-ship.painted.svg +1 -0
  231. package/dist/assets/starship-v1-ship.svg +1 -0
  232. package/dist/assets/starship-v1-ship.thick.svg +1 -0
  233. package/dist/assets/starship-v1.glb +0 -0
  234. package/dist/assets/starship-v1.painted.svg +1 -0
  235. package/dist/assets/starship-v1.svg +1 -0
  236. package/dist/assets/starship-v1.thick.svg +1 -0
  237. package/dist/assets/starship-v2-apart.painted.svg +1 -0
  238. package/dist/assets/starship-v2-apart.svg +1 -0
  239. package/dist/assets/starship-v2-apart.thick.svg +1 -0
  240. package/dist/assets/starship-v2-booster.glb +0 -0
  241. package/dist/assets/starship-v2-booster.painted.svg +1 -0
  242. package/dist/assets/starship-v2-booster.svg +1 -0
  243. package/dist/assets/starship-v2-booster.thick.svg +1 -0
  244. package/dist/assets/starship-v2-ship.glb +0 -0
  245. package/dist/assets/starship-v2-ship.painted.svg +1 -0
  246. package/dist/assets/starship-v2-ship.svg +1 -0
  247. package/dist/assets/starship-v2-ship.thick.svg +1 -0
  248. package/dist/assets/starship-v2.glb +0 -0
  249. package/dist/assets/starship-v2.painted.svg +1 -0
  250. package/dist/assets/starship-v2.svg +1 -0
  251. package/dist/assets/starship-v2.thick.svg +1 -0
  252. package/dist/assets/starship-v3-apart.painted.svg +1 -0
  253. package/dist/assets/starship-v3-apart.svg +1 -0
  254. package/dist/assets/starship-v3-apart.thick.svg +1 -0
  255. package/dist/assets/starship-v3-booster.glb +0 -0
  256. package/dist/assets/starship-v3-booster.painted.svg +1 -0
  257. package/dist/assets/starship-v3-booster.svg +1 -0
  258. package/dist/assets/starship-v3-booster.thick.svg +1 -0
  259. package/dist/assets/starship-v3-ship.glb +0 -0
  260. package/dist/assets/starship-v3-ship.painted.svg +1 -0
  261. package/dist/assets/starship-v3-ship.svg +1 -0
  262. package/dist/assets/starship-v3-ship.thick.svg +1 -0
  263. package/dist/assets/starship-v3.glb +0 -0
  264. package/dist/assets/starship-v3.painted.svg +1 -0
  265. package/dist/assets/starship-v3.svg +1 -0
  266. package/dist/assets/starship-v3.thick.svg +1 -0
  267. package/dist/assets/starship-v4-apart.painted.svg +1 -0
  268. package/dist/assets/starship-v4-apart.svg +1 -0
  269. package/dist/assets/starship-v4-apart.thick.svg +1 -0
  270. package/dist/assets/starship-v4-booster.glb +0 -0
  271. package/dist/assets/starship-v4-booster.painted.svg +1 -0
  272. package/dist/assets/starship-v4-booster.svg +1 -0
  273. package/dist/assets/starship-v4-booster.thick.svg +1 -0
  274. package/dist/assets/starship-v4-ship.glb +0 -0
  275. package/dist/assets/starship-v4-ship.painted.svg +1 -0
  276. package/dist/assets/starship-v4-ship.svg +1 -0
  277. package/dist/assets/starship-v4-ship.thick.svg +1 -0
  278. package/dist/assets/starship-v4.glb +0 -0
  279. package/dist/assets/starship-v4.painted.svg +1 -0
  280. package/dist/assets/starship-v4.svg +1 -0
  281. package/dist/assets/starship-v4.thick.svg +1 -0
  282. package/dist/build/glb.d.ts +12 -0
  283. package/dist/build/glb.js +157 -0
  284. package/dist/build/index.d.ts +59 -0
  285. package/dist/build/index.js +118 -0
  286. package/dist/build/lineup.d.ts +40 -0
  287. package/dist/build/lineup.js +97 -0
  288. package/dist/crossfade.d.ts +6 -0
  289. package/dist/crossfade.js +11 -0
  290. package/dist/data/credits.d.ts +87 -0
  291. package/dist/data/credits.js +82 -0
  292. package/dist/data/dragon.d.ts +2 -0
  293. package/dist/data/dragon.js +45 -0
  294. package/dist/data/falcon.d.ts +5 -0
  295. package/dist/data/falcon.js +123 -0
  296. package/dist/data/grasshopper.d.ts +2 -0
  297. package/dist/data/grasshopper.js +21 -0
  298. package/dist/data/sources.d.ts +181 -0
  299. package/dist/data/sources.js +26 -0
  300. package/dist/data/starship.d.ts +2 -0
  301. package/dist/data/starship.js +183 -0
  302. package/dist/data/types.d.ts +48 -0
  303. package/dist/data/types.js +8 -0
  304. package/dist/draw/svg.d.ts +26 -0
  305. package/dist/draw/svg.js +262 -0
  306. package/dist/element/paint.d.ts +9 -0
  307. package/dist/element/paint.js +138 -0
  308. package/dist/element/scene.d.ts +118 -0
  309. package/dist/element/scene.js +861 -0
  310. package/dist/element.d.ts +80 -0
  311. package/dist/element.js +526 -0
  312. package/dist/fit.d.ts +43 -0
  313. package/dist/fit.js +93 -0
  314. package/dist/framing.d.ts +42 -0
  315. package/dist/framing.js +56 -0
  316. package/dist/index.d.ts +23 -0
  317. package/dist/index.js +13 -0
  318. package/dist/layout.d.ts +17 -0
  319. package/dist/layout.js +33 -0
  320. package/dist/looks.d.ts +25 -0
  321. package/dist/looks.js +36 -0
  322. package/dist/model/falcon.d.ts +49 -0
  323. package/dist/model/falcon.js +208 -0
  324. package/dist/model/materials.d.ts +113 -0
  325. package/dist/model/materials.js +19 -0
  326. package/dist/model/pieces.d.ts +91 -0
  327. package/dist/model/pieces.js +223 -0
  328. package/dist/model/registry.d.ts +8 -0
  329. package/dist/model/registry.js +104 -0
  330. package/dist/model/starship.d.ts +67 -0
  331. package/dist/model/starship.js +503 -0
  332. package/dist/parts.d.ts +45 -0
  333. package/dist/parts.js +1 -0
  334. package/dist/react.d.ts +314 -0
  335. package/dist/react.js +8 -0
  336. package/dist/rockets.d.ts +27 -0
  337. package/dist/rockets.js +36 -0
  338. package/dist/tilt.d.ts +21 -0
  339. package/dist/tilt.js +23 -0
  340. package/package.json +66 -4
@@ -0,0 +1,861 @@
1
+ // The 3D view (three.js, MIT), loaded only when the element comes on screen. One canvas with one rocket, drawn three ways: the line drawing, the thick lines, and painted with true materials. Switching look
2
+ // changes materials and never the camera, so the angle stays. Moving between together, apart and one part alone is an animated move of each part, and under reduced motion it is a cut. It draws only while
3
+ // something moves, never while out of view or the tab is hidden, and settles after a while with no one touching it.
4
+ import { ACESFilmicToneMapping, BackSide, CanvasTexture, Color, DirectionalLight, DoubleSide, Group, HemisphereLight, LineSegments, LinearMipmapLinearFilter, MathUtils, Mesh, MeshBasicMaterial, MeshStandardMaterial, PerspectiveCamera, PlaneGeometry, PMREMGenerator, RepeatWrapping, Scene, ShaderMaterial, SphereGeometry, Spherical, SRGBColorSpace, Timer, Vector2, Vector3, WebGLRenderer, } from "three";
5
+ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
6
+ import { LineMaterial } from "three/addons/lines/LineMaterial.js";
7
+ import { LineSegments2 } from "three/addons/lines/LineSegments2.js";
8
+ import { LineSegmentsGeometry } from "three/addons/lines/LineSegmentsGeometry.js";
9
+ import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
10
+ import { directionOf, FOV_DEG, frameFor, PICTURE_H, turnedBy } from "../framing.js";
11
+ import { LOOKS } from "../looks.js";
12
+ import { MATERIALS } from "../model/materials.js";
13
+ import { paintGlow, paintSky, paintSteel, paintTiles } from "./paint.js";
14
+ const IDLE_STOP_MS = 90_000;
15
+ const MOVE_MS = 1400;
16
+ const VIEW_MS = 700;
17
+ const autoRotateOf = (spin) => (spin.seconds <= 0 ? 0 : (60 / spin.seconds) * (spin.way === "cw" ? -1 : 1));
18
+ const LINE_VERTEX = /* glsl */ `
19
+ varying vec3 vNormal;
20
+ varying vec2 vUv;
21
+ void main() {
22
+ vNormal = normalize(normalMatrix * normal);
23
+ vUv = uv;
24
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
25
+ }`;
26
+ const LINE_FRAGMENT = /* glsl */ `
27
+ uniform vec3 edge;
28
+ uniform vec3 middle;
29
+ uniform vec3 hexColor;
30
+ uniform float hexMetres;
31
+ uniform float hexOn;
32
+ varying vec3 vNormal;
33
+ varying vec2 vUv;
34
+ float hexLine(vec2 p) {
35
+ vec2 s = vec2(1.0, 1.7320508);
36
+ vec2 a = mod(p, s) - s * 0.5;
37
+ vec2 b = mod(p + s * 0.5, s) - s * 0.5;
38
+ vec2 g = dot(a, a) < dot(b, b) ? a : b;
39
+ vec2 q = abs(g);
40
+ return max(dot(q, s * 0.5), q.x);
41
+ }
42
+ void main() {
43
+ vec3 n = normalize(vNormal);
44
+ float facing = pow(abs(n.z), 1.4);
45
+ vec3 c = mix(edge, middle, facing);
46
+ if (hexOn > 0.5) {
47
+ vec2 metres = vec2(vUv.x * 28.27, vUv.y * 100.0) / hexMetres;
48
+ float d = hexLine(metres);
49
+ float w = fwidth(d) * 1.1;
50
+ float line = 1.0 - smoothstep(0.5 - w, 0.5, d);
51
+ c = mix(c, hexColor, (1.0 - line) * 0.34);
52
+ }
53
+ gl_FragColor = vec4(c, 1.0);
54
+ #include <colorspace_fragment>
55
+ }`;
56
+ const OUTLINE_VERTEX = /* glsl */ `
57
+ uniform float width;
58
+ uniform vec2 size;
59
+ void main() {
60
+ vec4 clip = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
61
+ vec3 n = normalize(normalMatrix * normal);
62
+ vec2 away = normalize(n.xy + vec2(1e-5));
63
+ clip.xy += away * width * 2.0 / size * clip.w;
64
+ gl_Position = clip;
65
+ }`;
66
+ const OUTLINE_FRAGMENT = /* glsl */ `
67
+ uniform vec3 color;
68
+ void main() {
69
+ gl_FragColor = vec4(color, 1.0);
70
+ #include <colorspace_fragment>
71
+ }`;
72
+ /** The materials of an engine's bell, in this package's own meshes. */
73
+ const NOZZLE_MATERIALS = new Set(["bell", "bell-inside", "engine-metal"]);
74
+ const asVec = (v) => [v.x, v.y, v.z];
75
+ export class RocketViewer {
76
+ canvas;
77
+ frames = 0;
78
+ holder;
79
+ options;
80
+ renderer;
81
+ environment;
82
+ scene = new Scene();
83
+ camera = new PerspectiveCamera(FOV_DEG, 3 / 4, 1, 4000);
84
+ root = new Group();
85
+ parts = [];
86
+ built = null;
87
+ look = "line";
88
+ show = "together";
89
+ generation = 0;
90
+ materialCache = new Map();
91
+ outlineMaterial;
92
+ textures = [];
93
+ steelMap = null;
94
+ tileMap = null;
95
+ glow = null;
96
+ controls;
97
+ clock = new Timer();
98
+ frame = 0;
99
+ visible = false;
100
+ lastActive = performance.now();
101
+ needsDraw = true;
102
+ pose = null;
103
+ tilt = { azimuth: 0, polar: 0, applied: { azimuth: 0, polar: 0 } };
104
+ driven = null;
105
+ drivenDirty = false;
106
+ lastDistance = 0;
107
+ resize;
108
+ disposed = false;
109
+ constructor(holder, options) {
110
+ this.holder = holder;
111
+ this.options = options;
112
+ this.renderer = new WebGLRenderer({ antialias: true, alpha: true, powerPreference: "low-power" });
113
+ this.renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
114
+ this.renderer.outputColorSpace = SRGBColorSpace;
115
+ this.renderer.toneMapping = ACESFilmicToneMapping;
116
+ this.renderer.toneMappingExposure = 1;
117
+ this.canvas = this.renderer.domElement;
118
+ this.canvas.className = "canvas";
119
+ this.canvas.setAttribute("part", "canvas");
120
+ this.canvas.tabIndex = 0;
121
+ this.canvas.setAttribute("role", "img");
122
+ this.canvas.setAttribute("aria-roledescription", "3D view");
123
+ this.setLabel(options.label);
124
+ this.canvas.addEventListener("webglcontextlost", (event) => {
125
+ event.preventDefault();
126
+ if (!this.disposed)
127
+ this.options.onLost();
128
+ });
129
+ holder.appendChild(this.canvas);
130
+ const pmrem = new PMREMGenerator(this.renderer);
131
+ const skyTexture = new CanvasTexture(paintSky());
132
+ skyTexture.colorSpace = SRGBColorSpace;
133
+ const skyScene = new Scene();
134
+ const sky = new Mesh(new SphereGeometry(50, 48, 24), new MeshBasicMaterial({ map: skyTexture, side: BackSide }));
135
+ skyScene.add(sky);
136
+ this.environment = pmrem.fromScene(skyScene, 0.012).texture;
137
+ pmrem.dispose();
138
+ sky.geometry.dispose();
139
+ sky.material.dispose();
140
+ skyTexture.dispose();
141
+ this.scene.environment = this.environment;
142
+ this.scene.environmentIntensity = 0.95;
143
+ const key = new DirectionalLight("#fff6e6", 1.6);
144
+ key.position.set(3, 5, 4);
145
+ const rim = new DirectionalLight("#cfe2ff", 1.1);
146
+ rim.position.set(-4, 1, -5);
147
+ const low = new DirectionalLight("#ffffff", 0.35);
148
+ low.position.set(1, -4, 2);
149
+ this.scene.add(key, rim, low, new HemisphereLight("#d7e6ff", "#22252b", 0.35), this.root);
150
+ this.outlineMaterial = new ShaderMaterial({
151
+ vertexShader: OUTLINE_VERTEX,
152
+ fragmentShader: OUTLINE_FRAGMENT,
153
+ side: BackSide,
154
+ uniforms: { width: { value: 1 }, size: { value: new Vector2(1, 1) }, color: { value: new Color("#8fa1b8") } },
155
+ });
156
+ this.controls = new OrbitControls(this.camera, this.canvas);
157
+ this.controls.enableDamping = true;
158
+ this.controls.dampingFactor = 0.08;
159
+ this.controls.enablePan = false;
160
+ this.controls.enableZoom = false;
161
+ this.controls.rotateSpeed = 0.7;
162
+ this.controls.minPolarAngle = 0.04;
163
+ this.controls.maxPolarAngle = Math.PI - 0.04;
164
+ this.canvas.style.touchAction = "pan-y";
165
+ this.controls.addEventListener("start", () => this.touched());
166
+ this.controls.addEventListener("change", () => this.wake());
167
+ this.canvas.addEventListener("focus", () => (this.controls.enableZoom = true));
168
+ this.canvas.addEventListener("blur", () => (this.controls.enableZoom = false));
169
+ this.canvas.addEventListener("pointerdown", () => this.canvas.focus({ preventScroll: true }));
170
+ this.canvas.addEventListener("dblclick", () => {
171
+ if (!this.driven)
172
+ this.setAngle(this.homeAngle, true);
173
+ });
174
+ this.canvas.addEventListener("keydown", (event) => this.key(event));
175
+ document.addEventListener("visibilitychange", this.onVisibility);
176
+ this.resize = new ResizeObserver(() => this.fit());
177
+ this.resize.observe(holder);
178
+ this.fit();
179
+ }
180
+ homeAngle = { az: 32, el: 9 };
181
+ setLabel(label) {
182
+ this.canvas.setAttribute("aria-label", `3D view of ${label}. Drag or use the arrow keys to turn it; double-click or Home puts it back.`);
183
+ }
184
+ /** Loads a rocket and shows it. The camera's angle stays when a rocket is already showing. */
185
+ async load(built, show, look, angle) {
186
+ const generation = ++this.generation;
187
+ const bytes = built.glb().bytes;
188
+ const gltf = await new GLTFLoader().parseAsync(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength), "");
189
+ if (this.disposed || generation !== this.generation)
190
+ return;
191
+ for (const part of this.parts)
192
+ part.holder.removeFromParent();
193
+ this.disposeTree(this.root);
194
+ this.root.clear();
195
+ this.parts = [];
196
+ this.built = built;
197
+ this.show = show;
198
+ const places = built.placesOf(show);
199
+ for (const part of built.parts) {
200
+ const source = gltf.scene.getObjectByName(part.slot);
201
+ if (!source)
202
+ continue;
203
+ const holder = new Group();
204
+ holder.name = part.slot;
205
+ const code = new Group();
206
+ holder.add(code);
207
+ const view = {
208
+ slot: part.slot,
209
+ holder,
210
+ code,
211
+ model: null,
212
+ modelFile: part.record.painted?.file ?? null,
213
+ ownNozzle: Boolean(part.record.painted?.ownNozzle),
214
+ modelState: "none",
215
+ nozzleShown: false,
216
+ faces: [],
217
+ outlines: [],
218
+ feature: [],
219
+ weld: [],
220
+ from: [0, 0, 0],
221
+ to: [0, 0, 0],
222
+ hideAtEnd: false,
223
+ };
224
+ this.takeMeshes(source, code, view);
225
+ const place = places.find((p) => p.slot === part.slot);
226
+ holder.position.set(...(place?.at ?? [0, 0, 0]));
227
+ holder.visible = place?.shown ?? false;
228
+ this.root.add(holder);
229
+ this.parts.push(view);
230
+ }
231
+ this.addGlow();
232
+ this.setLook(look);
233
+ this.frameTo(angle ?? this.currentAngle(), false);
234
+ this.canvas.dataset.ready = "true";
235
+ this.canvas.dataset.parts = this.parts.map((part) => part.slot).join(" ");
236
+ this.drivenDirty = this.driven !== null;
237
+ this.wake();
238
+ }
239
+ /** Moves the parts from the packed nodes into `into`, keeping each mesh's own placement. */
240
+ takeMeshes(source, into, view) {
241
+ const box = source.children[0];
242
+ if (!box)
243
+ return;
244
+ const carrier = new Group();
245
+ carrier.position.copy(box.position);
246
+ carrier.scale.copy(box.scale);
247
+ into.add(carrier);
248
+ const meshes = [];
249
+ box.traverse((node) => {
250
+ if (node instanceof Mesh || node instanceof LineSegments)
251
+ meshes.push(node);
252
+ });
253
+ for (const node of meshes) {
254
+ if (node instanceof LineSegments) {
255
+ const named = node.material.name;
256
+ const fat = this.fatLines(node);
257
+ carrier.add(fat);
258
+ (named === "line-feature" ? view.feature : view.weld).push(fat);
259
+ }
260
+ else if (node instanceof Mesh) {
261
+ const mesh = new Mesh(node.geometry);
262
+ const name = node.material.name;
263
+ mesh.name = name;
264
+ carrier.add(mesh);
265
+ view.faces.push({ mesh, name, hasUv: Boolean(node.geometry.getAttribute("uv")) });
266
+ const outline = new Mesh(node.geometry, this.outlineMaterial);
267
+ outline.name = "outline";
268
+ carrier.add(outline);
269
+ view.outlines.push(outline);
270
+ }
271
+ }
272
+ }
273
+ /** Lines of a set width in pixels, whatever the distance. */
274
+ fatLines(node) {
275
+ const position = node.geometry.getAttribute("position");
276
+ const index = node.geometry.getIndex();
277
+ const pairs = new Float32Array((index ? index.count : position.count) * 3);
278
+ for (let i = 0; i < (index ? index.count : position.count); i++) {
279
+ const v = index ? index.getX(i) : i;
280
+ pairs[i * 3] = position.getX(v);
281
+ pairs[i * 3 + 1] = position.getY(v);
282
+ pairs[i * 3 + 2] = position.getZ(v);
283
+ }
284
+ const geometry = new LineSegmentsGeometry();
285
+ geometry.setPositions(pairs);
286
+ const fat = new LineSegments2(geometry, new LineMaterial({ color: 0x8fa1b8, linewidth: 1, transparent: true }));
287
+ fat.computeLineDistances();
288
+ return fat;
289
+ }
290
+ /** The drawing's own colours for a material in a line look: a body that darkens to its edges, as the drawing's gradient does. */
291
+ lineMaterial(name, look, hasUv) {
292
+ const key = `${look}/${name}/${hasUv}`;
293
+ const known = this.materialCache.get(key);
294
+ if (known)
295
+ return known;
296
+ const fill = (name in MATERIALS ? MATERIALS[name].fill : "metal");
297
+ const [edge, middle] = LOOKS[look].fills[fill];
298
+ const made = new ShaderMaterial({
299
+ vertexShader: LINE_VERTEX,
300
+ fragmentShader: LINE_FRAGMENT,
301
+ side: DoubleSide,
302
+ polygonOffset: true,
303
+ polygonOffsetFactor: 1.5,
304
+ polygonOffsetUnits: 2,
305
+ uniforms: {
306
+ edge: { value: new Color(edge) },
307
+ middle: { value: new Color(middle) },
308
+ hexColor: { value: new Color(LOOKS[look].line) },
309
+ hexOn: { value: name === "tile" && hasUv ? 1 : 0 },
310
+ hexMetres: { value: 1 },
311
+ },
312
+ });
313
+ this.materialCache.set(key, made);
314
+ return made;
315
+ }
316
+ /** True materials: a stainless hull with its panels, black tiles, dark bells. */
317
+ paintedMaterial(name, hasUv) {
318
+ const key = `painted/${name}/${hasUv}`;
319
+ const known = this.materialCache.get(key);
320
+ if (known)
321
+ return known;
322
+ const spec = name in MATERIALS ? MATERIALS[name] : MATERIALS.steel;
323
+ const made = new MeshStandardMaterial({ color: spec.color, metalness: spec.metal, roughness: spec.rough, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: 1, polygonOffsetUnits: 1 });
324
+ if (name === "steel" && hasUv) {
325
+ this.steelMap ??= this.repeating(paintSteel(8, 8), 2, 100 / (8 * 1.83));
326
+ made.map = this.steelMap;
327
+ made.color.set("#dfe2e6");
328
+ made.roughness = 0.33;
329
+ }
330
+ if (name === "tile" && hasUv) {
331
+ // the mosaic repeats every 28 flats, round the hull and up it
332
+ this.tileMap ??= this.repeating(paintTiles(28), 2, 100 / 14.14);
333
+ made.map = this.tileMap;
334
+ made.color.set("#ffffff");
335
+ }
336
+ this.dullTile(made, name);
337
+ this.materialCache.set(key, made);
338
+ return made;
339
+ }
340
+ /** Black tiles are matt: without this a bright sky washes them grey. */
341
+ dullTile(material, name) {
342
+ if (name !== "tile" && name !== "tile-plain")
343
+ return;
344
+ material.envMap = this.environment;
345
+ material.envMapIntensity = 0.28;
346
+ material.roughness = Math.max(material.roughness, 0.72);
347
+ }
348
+ repeating(picture, u, v) {
349
+ const texture = new CanvasTexture(picture);
350
+ texture.colorSpace = SRGBColorSpace;
351
+ texture.wrapS = texture.wrapT = RepeatWrapping;
352
+ texture.repeat.set(u, v);
353
+ texture.minFilter = LinearMipmapLinearFilter;
354
+ texture.anisotropy = this.renderer.capabilities.getMaxAnisotropy();
355
+ this.textures.push(texture);
356
+ return texture;
357
+ }
358
+ /** One of the three looks. The camera does not move. */
359
+ setLook(look) {
360
+ this.look = look;
361
+ const spec = LOOKS[look];
362
+ const painted = look === "painted";
363
+ this.outlineMaterial.uniforms.color.value.set(spec.line);
364
+ this.outlineMaterial.uniforms.width.value = spec.outlinePx * this.renderer.getPixelRatio();
365
+ for (const part of this.parts) {
366
+ for (const { mesh, name, hasUv } of part.faces)
367
+ mesh.material = painted ? this.paintedMaterial(name, hasUv) : this.lineMaterial(name, look, hasUv);
368
+ for (const outline of part.outlines)
369
+ outline.visible = !painted;
370
+ for (const lines of part.feature) {
371
+ const material = lines.material;
372
+ material.color.set(painted ? "#2b2e33" : spec.line);
373
+ material.linewidth = spec.featurePx;
374
+ material.opacity = painted ? 0.5 : 1;
375
+ material.alphaToCoverage = false;
376
+ }
377
+ for (const lines of part.weld) {
378
+ const material = lines.material;
379
+ material.color.set(painted ? "#2b2e33" : spec.line);
380
+ material.linewidth = spec.weldPx;
381
+ material.opacity = spec.weldOpacity;
382
+ }
383
+ this.useModel(part, painted);
384
+ }
385
+ if (this.glow)
386
+ this.glow.visible = !painted;
387
+ this.canvas.dataset.look = look;
388
+ this.reportPainted();
389
+ this.wake();
390
+ }
391
+ /** Which painted parts are in: "model" when all come from a licensed model, "code" when this package's own paint, "loading" while one is on its way. */
392
+ reportPainted() {
393
+ if (this.look !== "painted") {
394
+ this.canvas.dataset.painted = "";
395
+ return;
396
+ }
397
+ const states = this.parts.map((part) => (part.modelFile ? part.modelState : "code"));
398
+ const state = states.some((s) => s === "loading" || s === "none") ? "loading" : states.every((s) => s === "ready") ? "model" : states.includes("ready") ? "mixed" : "code";
399
+ this.canvas.dataset.painted = state;
400
+ this.options.onPainted(state);
401
+ }
402
+ /** The licensed model of a part, fetched the first time the painted look asks for it. Until it is in, and if it fails, this package's own paint shows. */
403
+ useModel(part, painted) {
404
+ if (!part.modelFile)
405
+ return;
406
+ if (part.model) {
407
+ part.model.visible = painted;
408
+ this.showCode(part, painted);
409
+ return;
410
+ }
411
+ part.code.visible = true;
412
+ if (!painted || part.modelState !== "none")
413
+ return;
414
+ part.modelState = "loading";
415
+ const generation = this.generation;
416
+ new GLTFLoader()
417
+ .loadAsync(`${this.options.assets}models/${part.modelFile}`)
418
+ .then((gltf) => {
419
+ if (this.disposed || generation !== this.generation)
420
+ return;
421
+ part.model = gltf.scene;
422
+ // the same sheen as this package's own steel, whichever author's hull it is
423
+ gltf.scene.traverse((node) => {
424
+ if (!(node instanceof Mesh))
425
+ return;
426
+ const material = node.material;
427
+ if (material.name === "steel")
428
+ material.roughness = 0.46;
429
+ this.dullTile(material, material.name);
430
+ if (material.name === "tile")
431
+ material.color.setScalar(0.42);
432
+ });
433
+ part.holder.add(gltf.scene);
434
+ part.modelState = "ready";
435
+ this.useModel(part, this.look === "painted");
436
+ this.reportPainted();
437
+ this.wake();
438
+ })
439
+ .catch(() => {
440
+ part.modelState = "failed";
441
+ this.reportPainted();
442
+ });
443
+ }
444
+ /** Behind a licensed model this package's code shows nothing, except the nozzle the model lacks, and not while the stage is nested in the one below. */
445
+ showCode(part, painted) {
446
+ const nozzleOnly = painted && part.ownNozzle;
447
+ part.code.visible = !painted || nozzleOnly;
448
+ const nozzle = nozzleOnly && this.show !== "together";
449
+ for (const { mesh, name } of part.faces)
450
+ mesh.visible = !nozzleOnly || (nozzle && NOZZLE_MATERIALS.has(name));
451
+ for (const lines of [...part.feature, ...part.weld])
452
+ lines.visible = !nozzleOnly;
453
+ part.nozzleShown = nozzle;
454
+ this.canvas.dataset.nozzle = this.parts.some((one) => one.nozzleShown) ? "own" : "";
455
+ }
456
+ /** Together, apart, or one part alone: an animated move, or a cut under reduced motion. */
457
+ setShow(show) {
458
+ if (!this.built || show === this.show)
459
+ return;
460
+ this.show = show;
461
+ for (const part of this.parts)
462
+ if (part.model)
463
+ this.showCode(part, this.look === "painted");
464
+ const places = this.built.placesOf(show);
465
+ const fromAngle = this.currentAngle();
466
+ const framing = this.framingFor(fromAngle);
467
+ if (this.options.reducedMotion) {
468
+ for (const part of this.parts) {
469
+ const place = places.find((p) => p.slot === part.slot);
470
+ if (!place)
471
+ continue;
472
+ part.holder.position.set(...place.at);
473
+ part.holder.visible = place.shown;
474
+ }
475
+ this.pose = null;
476
+ this.applyFraming(framing);
477
+ this.wake();
478
+ return;
479
+ }
480
+ for (const part of this.parts) {
481
+ const place = places.find((p) => p.slot === part.slot);
482
+ if (!place)
483
+ continue;
484
+ part.from = asVec(part.holder.position);
485
+ const height = this.built.parts.find((p) => p.slot === part.slot)?.record.height ?? 50;
486
+ // a part leaving goes off upwards, or downwards if it is the lower one; a part arriving comes from where it left
487
+ if (!place.shown) {
488
+ part.to = part.holder.visible ? [part.from[0], part.from[1] + (part.slot === this.parts[0].slot ? -height * 1.6 : height * 1.6), part.from[2]] : part.from;
489
+ part.hideAtEnd = true;
490
+ }
491
+ else {
492
+ if (!part.holder.visible) {
493
+ const away = part.slot === this.parts[0].slot ? -height * 1.6 : height * 1.6;
494
+ part.from = [place.at[0], place.at[1] + away, place.at[2]];
495
+ part.holder.position.set(...part.from);
496
+ part.holder.visible = true;
497
+ }
498
+ part.to = place.at;
499
+ part.hideAtEnd = false;
500
+ }
501
+ }
502
+ this.startPose(framing, MOVE_MS, true);
503
+ }
504
+ framingFor(angle) {
505
+ if (!this.built)
506
+ throw new Error("nothing is loaded");
507
+ const { min, max } = this.built.boundsOf(this.show);
508
+ return frameFor(min, max, angle, this.camera.aspect);
509
+ }
510
+ applyFraming(framing) {
511
+ this.camera.position.set(...framing.position);
512
+ this.controls.target.set(...framing.target);
513
+ this.limits(framing.distance);
514
+ this.controls.update();
515
+ }
516
+ limits(distance) {
517
+ this.fitGlow();
518
+ this.camera.near = Math.max(0.2, distance / 40);
519
+ this.camera.far = distance * 8;
520
+ this.camera.updateProjectionMatrix();
521
+ this.controls.minDistance = distance * 0.12;
522
+ this.controls.maxDistance = distance * 1.8;
523
+ this.updateHexSize(distance);
524
+ this.syncSizes();
525
+ }
526
+ /** The tile mosaic is drawn at a constant size on screen, as the drawing's is. */
527
+ updateHexSize(distance) {
528
+ const metresPerPicturePx = (2 * distance * Math.tan(MathUtils.degToRad(FOV_DEG / 2))) / PICTURE_H;
529
+ const value = Math.max(0.3, metresPerPicturePx * 9);
530
+ for (const material of this.materialCache.values())
531
+ if (material instanceof ShaderMaterial && "hexMetres" in material.uniforms)
532
+ material.uniforms.hexMetres.value = value;
533
+ }
534
+ startPose(framing, ms, moveParts) {
535
+ // said when it starts, not when a frame happens to be drawn: a slow machine may draw none in the middle of a move
536
+ this.canvas.dataset.moving = "true";
537
+ if (moveParts)
538
+ this.canvas.dataset.moves = String(Number(this.canvas.dataset.moves ?? 0) + 1);
539
+ this.pose = {
540
+ from: { target: this.controls.target.clone(), offset: this.camera.position.clone().sub(this.controls.target) },
541
+ to: { target: new Vector3(...framing.target), offset: new Vector3(...framing.position).sub(new Vector3(...framing.target)) },
542
+ start: performance.now(),
543
+ ms,
544
+ moveParts,
545
+ };
546
+ this.limits(Math.max(framing.distance, this.camera.position.distanceTo(this.controls.target)));
547
+ this.wake();
548
+ }
549
+ finishParts() {
550
+ for (const part of this.parts) {
551
+ if (part.hideAtEnd)
552
+ part.holder.visible = false;
553
+ part.hideAtEnd = false;
554
+ }
555
+ if (this.built)
556
+ this.limits(this.framingFor(this.currentAngle()).distance);
557
+ }
558
+ /** Where the camera is looking from. */
559
+ currentAngle() {
560
+ const offset = this.camera.position.clone().sub(this.controls.target);
561
+ if (offset.lengthSq() === 0)
562
+ return this.homeAngle;
563
+ const s = new Spherical().setFromVector3(offset);
564
+ return { az: MathUtils.radToDeg(s.theta), el: 90 - MathUtils.radToDeg(s.phi) };
565
+ }
566
+ /** Turns to look from an angle. */
567
+ setAngle(angle, animate) {
568
+ if (!this.built)
569
+ return;
570
+ if (this.driven) {
571
+ this.homeAngle = angle;
572
+ this.syncDriven();
573
+ return;
574
+ }
575
+ this.controls.autoRotate = false;
576
+ const framing = this.framingFor(angle);
577
+ if (animate && !this.options.reducedMotion)
578
+ this.startPose(framing, VIEW_MS, false);
579
+ else
580
+ this.applyFraming(framing);
581
+ this.wake();
582
+ }
583
+ frameTo(angle, animate) {
584
+ this.homeAngle = angle;
585
+ this.setAngleKeepingSpin(angle, animate);
586
+ }
587
+ setAngleKeepingSpin(angle, animate) {
588
+ if (!this.built)
589
+ return;
590
+ const framing = this.framingFor(angle);
591
+ if (animate && !this.options.reducedMotion)
592
+ this.startPose(framing, VIEW_MS, false);
593
+ else
594
+ this.applyFraming(framing);
595
+ }
596
+ setHome(angle) {
597
+ this.homeAngle = angle;
598
+ }
599
+ setVisible(visible) {
600
+ this.visible = visible;
601
+ if (visible)
602
+ this.wake();
603
+ }
604
+ setSpin(spin) {
605
+ this.controls.autoRotateSpeed = autoRotateOf(spin);
606
+ this.controls.autoRotate = !this.driven && !this.options.reducedMotion && spin.seconds > 0;
607
+ this.canvas.dataset.spin = `${spin.seconds}${spin.way}`;
608
+ this.wake();
609
+ }
610
+ /** Hands the turn, tip, zoom and pan to the page, as the family's view controls report them: this view's own hands (drag, wheel, keys, spin) stand down, and the camera is the chosen view's angle turned and tipped by them. `null` takes them back. */
611
+ drive(choice) {
612
+ this.driven = choice;
613
+ this.controls.enabled = choice === null;
614
+ if (choice)
615
+ this.controls.autoRotate = false;
616
+ this.canvas.style.touchAction = choice ? "auto" : "pan-y";
617
+ this.syncDriven();
618
+ }
619
+ /** The camera is put where the choice says at once, seen or not, so a page that asks where it looks from is told the truth; a move between parts moves it by itself. */
620
+ syncDriven() {
621
+ this.drivenDirty = false;
622
+ this.lens();
623
+ if (!this.pose)
624
+ this.applyDriven();
625
+ this.needsDraw = true;
626
+ this.wake();
627
+ }
628
+ drivenAngle() {
629
+ if (!this.driven)
630
+ return null;
631
+ const turned = turnedBy(this.homeAngle, this.driven.yaw, this.driven.tilt);
632
+ const lean = this.tilt.applied;
633
+ return { az: turned.az + MathUtils.radToDeg(lean.azimuth), el: MathUtils.clamp(turned.el - MathUtils.radToDeg(lean.polar), -87, 87) };
634
+ }
635
+ /** The camera stands where the view's angle, turned and tipped, puts it, as far off as the whole rocket needs from the view's own angle, so turning never makes it breathe. */
636
+ applyDriven() {
637
+ const angle = this.drivenAngle();
638
+ if (!angle || !this.built)
639
+ return;
640
+ const framing = this.framingFor(this.homeAngle);
641
+ const direction = directionOf(angle);
642
+ this.camera.position.set(framing.target[0] + direction[0] * framing.distance, framing.target[1] + direction[1] * framing.distance, framing.target[2] + direction[2] * framing.distance);
643
+ this.controls.target.set(...framing.target);
644
+ if (Math.abs(framing.distance - this.lastDistance) > 1e-6) {
645
+ this.lastDistance = framing.distance;
646
+ this.limits(framing.distance);
647
+ }
648
+ this.controls.update();
649
+ }
650
+ /** Zoom is the camera's, and the pan moves the picture by pixels, so the view stays sharp at any zoom. */
651
+ lens() {
652
+ const { zoom = 1, panX = 0, panY = 0 } = this.driven ?? {};
653
+ const w = Math.max(1, this.holder.clientWidth);
654
+ const h = Math.max(1, this.holder.clientHeight);
655
+ this.camera.zoom = zoom;
656
+ if (panX !== 0 || panY !== 0)
657
+ this.camera.setViewOffset(w, h, -panX, -panY, w, h);
658
+ else
659
+ this.camera.clearViewOffset();
660
+ this.camera.updateProjectionMatrix();
661
+ }
662
+ setTilt(azimuthDeg, polarDeg) {
663
+ this.tilt.azimuth = MathUtils.degToRad(MathUtils.clamp(azimuthDeg, -25, 25));
664
+ this.tilt.polar = MathUtils.degToRad(MathUtils.clamp(polarDeg, -12, 12));
665
+ this.wake();
666
+ }
667
+ dispose() {
668
+ this.disposed = true;
669
+ cancelAnimationFrame(this.frame);
670
+ document.removeEventListener("visibilitychange", this.onVisibility);
671
+ this.resize.disconnect();
672
+ this.controls.dispose();
673
+ this.disposeTree(this.root);
674
+ for (const material of this.materialCache.values())
675
+ material.dispose();
676
+ this.outlineMaterial.dispose();
677
+ for (const texture of this.textures)
678
+ texture.dispose();
679
+ this.environment.dispose();
680
+ this.renderer.dispose();
681
+ this.renderer.forceContextLoss();
682
+ this.canvas.remove();
683
+ }
684
+ disposeTree(tree) {
685
+ tree.traverse((node) => {
686
+ const geometry = node.geometry;
687
+ geometry?.dispose();
688
+ if (node instanceof LineSegments2)
689
+ node.material.dispose();
690
+ });
691
+ }
692
+ addGlow() {
693
+ if (!this.built || this.glow)
694
+ return;
695
+ const texture = new CanvasTexture(paintGlow("rgba(108,208,255,0.16)", "rgba(108,208,255,0)"));
696
+ this.textures.push(texture);
697
+ const plane = new Mesh(new PlaneGeometry(1, 1), new MeshBasicMaterial({ map: texture, transparent: true, depthWrite: false }));
698
+ plane.rotation.x = -Math.PI / 2;
699
+ plane.position.y = -0.02;
700
+ this.glow = plane;
701
+ this.scene.add(plane);
702
+ this.fitGlow();
703
+ }
704
+ /** The pool of light is as wide as what stands in it. */
705
+ fitGlow() {
706
+ if (!this.built || !this.glow)
707
+ return;
708
+ const { min, max } = this.built.boundsOf(this.show);
709
+ const span = Math.max(max[0] - min[0], max[2] - min[2]);
710
+ this.glow.scale.setScalar(span * 3);
711
+ this.glow.position.x = (min[0] + max[0]) / 2;
712
+ this.glow.position.z = (min[2] + max[2]) / 2;
713
+ }
714
+ onVisibility = () => {
715
+ if (!document.hidden)
716
+ this.wake();
717
+ };
718
+ touched() {
719
+ this.controls.autoRotate = false;
720
+ this.pose = null;
721
+ this.wake();
722
+ }
723
+ wake() {
724
+ this.lastActive = performance.now();
725
+ this.needsDraw = true;
726
+ if (!this.frame && this.visible && !document.hidden && !this.disposed)
727
+ this.frame = requestAnimationFrame(this.tick);
728
+ }
729
+ tick = (now) => {
730
+ this.frame = 0;
731
+ if (!this.visible || document.hidden || this.disposed)
732
+ return;
733
+ this.clock.update(now);
734
+ const dt = Math.min(this.clock.getDelta(), 0.1);
735
+ const ambient = !this.options.reducedMotion && now - this.lastActive < IDLE_STOP_MS;
736
+ const t = this.tilt;
737
+ const dA = (t.azimuth - t.applied.azimuth) * 0.12;
738
+ const dP = (t.polar - t.applied.polar) * 0.12;
739
+ if (Math.abs(dA) > 1e-5 || Math.abs(dP) > 1e-5) {
740
+ if (this.driven)
741
+ this.drivenDirty = true;
742
+ else
743
+ this.orbit(dA, dP);
744
+ t.applied.azimuth += dA;
745
+ t.applied.polar += dP;
746
+ this.needsDraw = true;
747
+ }
748
+ if (this.drivenDirty) {
749
+ this.drivenDirty = false;
750
+ this.lens();
751
+ if (!this.pose)
752
+ this.applyDriven();
753
+ this.needsDraw = true;
754
+ }
755
+ if (this.pose) {
756
+ const { from, to, start, ms, moveParts } = this.pose;
757
+ const k = Math.min(1, (performance.now() - start) / ms);
758
+ const e = k < 0.5 ? 4 * k * k * k : 1 - (-2 * k + 2) ** 3 / 2;
759
+ this.controls.target.lerpVectors(from.target, to.target, e);
760
+ // round the circle, not through the rocket
761
+ const a = new Spherical().setFromVector3(from.offset);
762
+ const b = new Spherical().setFromVector3(to.offset);
763
+ let dTheta = b.theta - a.theta;
764
+ while (dTheta > Math.PI)
765
+ dTheta -= 2 * Math.PI;
766
+ while (dTheta < -Math.PI)
767
+ dTheta += 2 * Math.PI;
768
+ const s = new Spherical(MathUtils.lerp(a.radius, b.radius, e), MathUtils.lerp(a.phi, b.phi, e), a.theta + dTheta * e);
769
+ const aim = this.drivenAngle();
770
+ if (aim)
771
+ s.setFromVector3(new Vector3(...directionOf(aim)).setLength(s.radius));
772
+ this.camera.position.copy(this.controls.target).add(new Vector3().setFromSpherical(s));
773
+ if (moveParts)
774
+ for (const part of this.parts) {
775
+ // lifts clear first, then across, then down: a stage does not slide through another
776
+ const across = Math.min(1, Math.max(0, (e - 0.15) / 0.7));
777
+ part.holder.position.set(MathUtils.lerp(part.from[0], part.to[0], across), MathUtils.lerp(part.from[1], part.to[1], e), MathUtils.lerp(part.from[2], part.to[2], across));
778
+ }
779
+ this.needsDraw = true;
780
+ if (k >= 1) {
781
+ const done = this.pose.moveParts;
782
+ this.pose = null;
783
+ if (done)
784
+ this.finishParts();
785
+ this.drivenDirty = this.driven !== null;
786
+ }
787
+ }
788
+ if (!ambient)
789
+ this.controls.autoRotate = false;
790
+ const changed = this.controls.update(dt);
791
+ if (this.needsDraw || changed || this.pose) {
792
+ this.syncSizes();
793
+ this.renderer.render(this.scene, this.camera);
794
+ this.needsDraw = false;
795
+ this.frames++;
796
+ this.canvas.dataset.frames = String(this.frames);
797
+ const angle = this.currentAngle();
798
+ this.canvas.dataset.azimuth = angle.az.toFixed(2);
799
+ this.canvas.dataset.elevation = angle.el.toFixed(2);
800
+ this.canvas.dataset.moving = this.pose ? "true" : "false";
801
+ if (this.driven) {
802
+ this.canvas.dataset.zoom = this.driven.zoom.toFixed(2);
803
+ this.canvas.dataset.pan = `${Math.round(this.driven.panX)},${Math.round(this.driven.panY)}`;
804
+ }
805
+ if (this.glow)
806
+ this.glow.visible = this.look !== "painted" && this.camera.position.y > 0;
807
+ }
808
+ if ((ambient && this.controls.autoRotate) || changed || this.pose || this.needsDraw || Math.abs(dA) > 1e-5)
809
+ this.frame = requestAnimationFrame(this.tick);
810
+ };
811
+ syncSizes() {
812
+ const size = this.renderer.getDrawingBufferSize(new Vector2());
813
+ this.outlineMaterial.uniforms.size.value.copy(size);
814
+ }
815
+ fit() {
816
+ const w = Math.max(1, this.holder.clientWidth);
817
+ const h = Math.max(1, this.holder.clientHeight);
818
+ this.renderer.setSize(w, h, false);
819
+ this.camera.aspect = w / h;
820
+ this.lens();
821
+ this.syncSizes();
822
+ if (this.built && !this.pose)
823
+ this.applyFraming(this.framingFor(this.currentAngle()));
824
+ this.wake();
825
+ }
826
+ orbit(dAzimuth, dPolar) {
827
+ const offset = this.camera.position.clone().sub(this.controls.target);
828
+ const s = new Spherical().setFromVector3(offset);
829
+ s.theta += dAzimuth;
830
+ s.phi = MathUtils.clamp(s.phi + dPolar, this.controls.minPolarAngle, this.controls.maxPolarAngle);
831
+ this.camera.position.copy(this.controls.target).add(new Vector3().setFromSpherical(s));
832
+ }
833
+ key(event) {
834
+ if (this.driven)
835
+ return;
836
+ const step = MathUtils.degToRad(15);
837
+ const moves = {
838
+ ArrowLeft: () => this.orbit(-step, 0),
839
+ ArrowRight: () => this.orbit(step, 0),
840
+ ArrowUp: () => this.orbit(0, -step * 0.66),
841
+ ArrowDown: () => this.orbit(0, step * 0.66),
842
+ "+": () => this.zoom(0.85),
843
+ "=": () => this.zoom(0.85),
844
+ "-": () => this.zoom(1 / 0.85),
845
+ Home: () => this.setAngle(this.homeAngle, true),
846
+ "0": () => this.setAngle(this.homeAngle, true),
847
+ };
848
+ const move = moves[event.key];
849
+ if (!move)
850
+ return;
851
+ event.preventDefault();
852
+ this.touched();
853
+ move();
854
+ this.wake();
855
+ }
856
+ zoom(factor) {
857
+ const offset = this.camera.position.clone().sub(this.controls.target);
858
+ const length = MathUtils.clamp(offset.length() * factor, this.controls.minDistance, this.controls.maxDistance);
859
+ this.camera.position.copy(this.controls.target).add(offset.setLength(length));
860
+ }
861
+ }