prts-terrarchive 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 (287) hide show
  1. package/GAME_ASSETS.md +26 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +274 -0
  4. package/README.md +423 -0
  5. package/THIRD_PARTY_NOTICES.md +61 -0
  6. package/bin/bake-rhine-details.mjs +34 -0
  7. package/bin/install.js +273 -0
  8. package/bin/pack-map-assets.mjs +110 -0
  9. package/bin/preview-rhine.mjs +71 -0
  10. package/bin/rhine-detail-baker.ts +84 -0
  11. package/contracts/cloud_tools_v1.schema.json +243 -0
  12. package/contracts/corpus_i18n_v1.schema.json +179 -0
  13. package/contracts/corpus_tools_v1.schema.json +1801 -0
  14. package/cordis.patch.yml +32 -0
  15. package/docs/endfield-official-i18n.md +103 -0
  16. package/lib/client.js +3912 -0
  17. package/lib/endfield-map/map.js.br +0 -0
  18. package/lib/endfield-map/map.js.gz +0 -0
  19. package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.br +0 -0
  20. package/lib/endfield-map/resources/map-00b0d0744a1b4404.json.gz +0 -0
  21. package/lib/endfield-map/resources/map-01616f534aaaeca1.png +0 -0
  22. package/lib/endfield-map/resources/map-1562e0dff4be3d23.png +0 -0
  23. package/lib/endfield-map/resources/map-160fe040f080f6a3.png +0 -0
  24. package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.br +0 -0
  25. package/lib/endfield-map/resources/map-17b9d8be25dc2b39.json.gz +0 -0
  26. package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.br +0 -0
  27. package/lib/endfield-map/resources/map-1c4245c4e8b80c43.json.gz +0 -0
  28. package/lib/endfield-map/resources/map-1d30baaaef3e29cc.png +0 -0
  29. package/lib/endfield-map/resources/map-21a61386af79cd12.json.br +0 -0
  30. package/lib/endfield-map/resources/map-21a61386af79cd12.json.gz +0 -0
  31. package/lib/endfield-map/resources/map-24b920b8dab49b44.json.br +0 -0
  32. package/lib/endfield-map/resources/map-24b920b8dab49b44.json.gz +0 -0
  33. package/lib/endfield-map/resources/map-26caf8d71247abb7.png +0 -0
  34. package/lib/endfield-map/resources/map-2729942c89545e73.json.br +0 -0
  35. package/lib/endfield-map/resources/map-2729942c89545e73.json.gz +0 -0
  36. package/lib/endfield-map/resources/map-29063a9802f67014.json.br +0 -0
  37. package/lib/endfield-map/resources/map-29063a9802f67014.json.gz +0 -0
  38. package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.br +0 -0
  39. package/lib/endfield-map/resources/map-2f35d09a3ab60a63.json.gz +0 -0
  40. package/lib/endfield-map/resources/map-2f98a873da81425e.json.br +0 -0
  41. package/lib/endfield-map/resources/map-2f98a873da81425e.json.gz +0 -0
  42. package/lib/endfield-map/resources/map-3ab978ae44190f80.json.br +0 -0
  43. package/lib/endfield-map/resources/map-3ab978ae44190f80.json.gz +0 -0
  44. package/lib/endfield-map/resources/map-3b62c80d14208902.json.br +0 -0
  45. package/lib/endfield-map/resources/map-3b62c80d14208902.json.gz +0 -0
  46. package/lib/endfield-map/resources/map-3d486e887ae121ac.png +0 -0
  47. package/lib/endfield-map/resources/map-418dc4865608fce5.png +0 -0
  48. package/lib/endfield-map/resources/map-4366b84607335f76.json.br +0 -0
  49. package/lib/endfield-map/resources/map-4366b84607335f76.json.gz +0 -0
  50. package/lib/endfield-map/resources/map-4371259d3ab7aac7.png +0 -0
  51. package/lib/endfield-map/resources/map-48dcc0fd55e13f7f.png +0 -0
  52. package/lib/endfield-map/resources/map-4aabda0e41197570.json.br +0 -0
  53. package/lib/endfield-map/resources/map-4aabda0e41197570.json.gz +0 -0
  54. package/lib/endfield-map/resources/map-505f5687b287043c.json.br +0 -0
  55. package/lib/endfield-map/resources/map-505f5687b287043c.json.gz +0 -0
  56. package/lib/endfield-map/resources/map-5177e9d1d46f47e2.png +0 -0
  57. package/lib/endfield-map/resources/map-51baeb59b7f8c28d.png +0 -0
  58. package/lib/endfield-map/resources/map-549561b46e621874.png +0 -0
  59. package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.br +0 -0
  60. package/lib/endfield-map/resources/map-56c6a0bef799c8dc.json.gz +0 -0
  61. package/lib/endfield-map/resources/map-5a6963909423b674.png +0 -0
  62. package/lib/endfield-map/resources/map-5a9c533c0552d7b7.png +0 -0
  63. package/lib/endfield-map/resources/map-5d20ae9ffa9a02b0.png +0 -0
  64. package/lib/endfield-map/resources/map-626c8847393f60bf.png +0 -0
  65. package/lib/endfield-map/resources/map-63d43cbec3bed3b8.png +0 -0
  66. package/lib/endfield-map/resources/map-661b6ec56e722574.png +0 -0
  67. package/lib/endfield-map/resources/map-6ddd384eca190828.png +0 -0
  68. package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.br +0 -0
  69. package/lib/endfield-map/resources/map-6fc1aa9144f8156c.json.gz +0 -0
  70. package/lib/endfield-map/resources/map-738ebaa002c33cfc.png +0 -0
  71. package/lib/endfield-map/resources/map-758c13be6085ec07.json.br +0 -0
  72. package/lib/endfield-map/resources/map-758c13be6085ec07.json.gz +0 -0
  73. package/lib/endfield-map/resources/map-78929f26d6844509.png +0 -0
  74. package/lib/endfield-map/resources/map-7f09c142950a0f74.png +0 -0
  75. package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.br +0 -0
  76. package/lib/endfield-map/resources/map-8385e11f39e1a51c.json.gz +0 -0
  77. package/lib/endfield-map/resources/map-881343926a568f27.json.br +0 -0
  78. package/lib/endfield-map/resources/map-881343926a568f27.json.gz +0 -0
  79. package/lib/endfield-map/resources/map-8d1ade3f01d952b0.png +0 -0
  80. package/lib/endfield-map/resources/map-8fef3495bb7ed60f.png +0 -0
  81. package/lib/endfield-map/resources/map-907a7ecf8a0ce352.png +0 -0
  82. package/lib/endfield-map/resources/map-9113d5951c4db903.json.br +0 -0
  83. package/lib/endfield-map/resources/map-9113d5951c4db903.json.gz +0 -0
  84. package/lib/endfield-map/resources/map-94ad6634436224e3.json.br +0 -0
  85. package/lib/endfield-map/resources/map-94ad6634436224e3.json.gz +0 -0
  86. package/lib/endfield-map/resources/map-99722e61da5f3e53.png +0 -0
  87. package/lib/endfield-map/resources/map-9be9596cc059abe3.png +0 -0
  88. package/lib/endfield-map/resources/map-a4db9a4763e47fbf.png +0 -0
  89. package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.br +0 -0
  90. package/lib/endfield-map/resources/map-a4e8ef07159997e7.json.gz +0 -0
  91. package/lib/endfield-map/resources/map-ab35d06875ee418f.png +0 -0
  92. package/lib/endfield-map/resources/map-ac51bb3e7623cd8b.png +0 -0
  93. package/lib/endfield-map/resources/map-ac830a66296bc83a.png +0 -0
  94. package/lib/endfield-map/resources/map-b3fd04209718e55e.json.br +0 -0
  95. package/lib/endfield-map/resources/map-b3fd04209718e55e.json.gz +0 -0
  96. package/lib/endfield-map/resources/map-b8164f1af7c5d895.png +0 -0
  97. package/lib/endfield-map/resources/map-b8cde93b965384d8.json.br +0 -0
  98. package/lib/endfield-map/resources/map-b8cde93b965384d8.json.gz +0 -0
  99. package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.br +0 -0
  100. package/lib/endfield-map/resources/map-b8e48511502e0e8d.json.gz +0 -0
  101. package/lib/endfield-map/resources/map-bfc58277d5ba649d.png +0 -0
  102. package/lib/endfield-map/resources/map-c013f82048c6891a.png +0 -0
  103. package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.br +0 -0
  104. package/lib/endfield-map/resources/map-c49c947841ae4ab5.json.gz +0 -0
  105. package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.br +0 -0
  106. package/lib/endfield-map/resources/map-c4d1b1c6d6706c81.json.gz +0 -0
  107. package/lib/endfield-map/resources/map-c607f8b4c8ad8967.png +0 -0
  108. package/lib/endfield-map/resources/map-c6f20276b8781326.json.br +0 -0
  109. package/lib/endfield-map/resources/map-c6f20276b8781326.json.gz +0 -0
  110. package/lib/endfield-map/resources/map-d51adf5952af1e8e.png +0 -0
  111. package/lib/endfield-map/resources/map-d695eface22f8b74.json.br +0 -0
  112. package/lib/endfield-map/resources/map-d695eface22f8b74.json.gz +0 -0
  113. package/lib/endfield-map/resources/map-db11d394456a7465.png +0 -0
  114. package/lib/endfield-map/resources/map-dcc4d218581e3dc6.png +0 -0
  115. package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.br +0 -0
  116. package/lib/endfield-map/resources/map-ddd43dbb8ab8a4e3.json.gz +0 -0
  117. package/lib/endfield-map/resources/map-defe5db7dabf748a.png +0 -0
  118. package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.br +0 -0
  119. package/lib/endfield-map/resources/map-e1aeb833e22520aa.json.gz +0 -0
  120. package/lib/endfield-map/resources/map-e25ee9af6e493500.png +0 -0
  121. package/lib/endfield-map/resources/map-e3824082cd6f747d.json.br +0 -0
  122. package/lib/endfield-map/resources/map-e3824082cd6f747d.json.gz +0 -0
  123. package/lib/endfield-map/resources/map-e694a91e630b1bd6.png +0 -0
  124. package/lib/endfield-map/resources/map-eaa93af79d12a87d.png +0 -0
  125. package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.br +0 -0
  126. package/lib/endfield-map/resources/map-ec9575ee65d7766f.json.gz +0 -0
  127. package/lib/endfield-map/resources/map-ede421a329dc78a4.png +0 -0
  128. package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.br +0 -0
  129. package/lib/endfield-map/resources/map-eff3bce8f6888fc5.json.gz +0 -0
  130. package/lib/endfield-map/resources/map-fb5f4a59a181452c.png +0 -0
  131. package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.br +0 -0
  132. package/lib/endfield-map/resources/map-fdeaa17eeba90d8e.json.gz +0 -0
  133. package/lib/rhine/assets/archive-assembly.glb +0 -0
  134. package/lib/rhine/assets/archive-cassette.glb +0 -0
  135. package/lib/rhine/fonts/MiSans-Bold.woff2 +0 -0
  136. package/lib/rhine/fonts/MiSans-Demibold.woff2 +0 -0
  137. package/lib/rhine/fonts/MiSans-Light.woff2 +0 -0
  138. package/lib/rhine/fonts/MiSans-Regular.woff2 +0 -0
  139. package/lib/rhine/fonts/MiSans-license.pdf +0 -0
  140. package/lib/rhine/fonts/NOTICE.txt +6 -0
  141. package/lib/rhine/licenses/RhineLabUI-MIT.txt +21 -0
  142. package/lib/rhine/licenses/entities-BSD-2-Clause.txt +11 -0
  143. package/lib/rhine/licenses/linkify-it-MIT.txt +22 -0
  144. package/lib/rhine/licenses/markdown-it-MIT.txt +22 -0
  145. package/lib/rhine/licenses/mdurl-MIT.txt +45 -0
  146. package/lib/rhine/licenses/punycode.js-MIT.txt +20 -0
  147. package/lib/rhine/licenses/rolling-number-MIT.txt +21 -0
  148. package/lib/rhine/licenses/three-MIT.txt +21 -0
  149. package/lib/rhine/licenses/uc.micro-MIT.txt +20 -0
  150. package/lib/rhine/rhine.css +1 -0
  151. package/lib/rhine/rhine.js +5106 -0
  152. package/lib/skins/common.css +31 -0
  153. package/lib/skins/endfield-aic.css +141 -0
  154. package/lib/skins/prts-agent.css +136 -0
  155. package/lib/skins/rhine-lab.css +70 -0
  156. package/package.json +112 -0
  157. package/presets/definition.js +26 -0
  158. package/presets/prts/agent.cordis.yml +25 -0
  159. package/presets/prts/preset.yml +3 -0
  160. package/presets/register.js +86 -0
  161. package/resources/entity-routing.json +21 -0
  162. package/skills/prts-investigation/SKILL.md +121 -0
  163. package/skills/prts-retrieval/SKILL.md +35 -0
  164. package/skills/prts-retrieval/references/cloud-tools.md +13 -0
  165. package/skills/prts-retrieval/references/diagnostics.md +15 -0
  166. package/skills/prts-retrieval/references/local-tools.md +24 -0
  167. package/skills/prts-retrieval/references/module-arknights.md +23 -0
  168. package/skills/prts-retrieval/references/module-dual.md +15 -0
  169. package/skills/prts-retrieval/references/module-endfield.md +21 -0
  170. package/skills/prts-retrieval/references/retrieval-process.md +44 -0
  171. package/skills/prts-retrieval/references/retrieval-recipes-dual.md +34 -0
  172. package/skills/prts-retrieval/references/retrieval-recipes-endfield.md +37 -0
  173. package/skills/prts-retrieval/references/retrieval-recipes.md +39 -0
  174. package/skills/prts-retrieval/references/tools-runtime.md +48 -0
  175. package/skills/prts-retrieval/references/wiki-schema.md +38 -0
  176. package/src/archive-presentation.js +287 -0
  177. package/src/cloud-projection.js +124 -0
  178. package/src/cloud.js +498 -0
  179. package/src/entity-recognizer.js +311 -0
  180. package/src/entity-routing.js +82 -0
  181. package/src/evidence-state.js +332 -0
  182. package/src/i18n.js +365 -0
  183. package/src/index.js +1261 -0
  184. package/src/installer.js +1362 -0
  185. package/src/investigation-review.js +87 -0
  186. package/src/investigation-store.js +579 -0
  187. package/src/investigation-tools.js +113 -0
  188. package/src/localization-format.js +23 -0
  189. package/src/manual-search.js +103 -0
  190. package/src/read.js +1097 -0
  191. package/src/release-compatibility.js +73 -0
  192. package/src/release-watch.js +40 -0
  193. package/src/search-projection.js +139 -0
  194. package/src/search.js +1522 -0
  195. package/src/skill.js +98 -0
  196. package/src/source-map.js +348 -0
  197. package/src/state.js +314 -0
  198. package/src/store.js +1569 -0
  199. package/src/timeline.js +386 -0
  200. package/src/ui.js +1353 -0
  201. package/src/wiki.js +111 -0
  202. package/tsconfig.rhine.json +15 -0
  203. package/ui/rhine/archive-navigation-limit.ts +31 -0
  204. package/ui/rhine/archive-rack.ts +83 -0
  205. package/ui/rhine/assets/evidence-observatory.webp +0 -0
  206. package/ui/rhine/board-plane-transform.ts +24 -0
  207. package/ui/rhine/catalogue.ts +72 -0
  208. package/ui/rhine/deferred-preparation.ts +146 -0
  209. package/ui/rhine/evidence-board-controls.css +22 -0
  210. package/ui/rhine/evidence-board-history.ts +73 -0
  211. package/ui/rhine/evidence-board-model.ts +189 -0
  212. package/ui/rhine/evidence-board-panel.ts +762 -0
  213. package/ui/rhine/evidence-board-scene.ts +548 -0
  214. package/ui/rhine/evidence-board.css +191 -0
  215. package/ui/rhine/evidence-card-art.ts +470 -0
  216. package/ui/rhine/evidence-card-style.ts +17 -0
  217. package/ui/rhine/evidence-inbox-scene.ts +43 -0
  218. package/ui/rhine/evidence-inbox.css +29 -0
  219. package/ui/rhine/evidence-inbox.ts +104 -0
  220. package/ui/rhine/index.ts +12 -0
  221. package/ui/rhine/interaction.css +20 -0
  222. package/ui/rhine/investigation-board-plane.ts +5 -0
  223. package/ui/rhine/investigation-board.css +34 -0
  224. package/ui/rhine/investigation-board.ts +348 -0
  225. package/ui/rhine/investigation-edits.ts +46 -0
  226. package/ui/rhine/investigation-layout.ts +29 -0
  227. package/ui/rhine/label-prefetch.ts +34 -0
  228. package/ui/rhine/looping-title.ts +40 -0
  229. package/ui/rhine/manual-search.css +41 -0
  230. package/ui/rhine/manual-search.ts +89 -0
  231. package/ui/rhine/original/appearance.ts +180 -0
  232. package/ui/rhine/original/archive-label.ts +84 -0
  233. package/ui/rhine/original/archive-lighting.ts +39 -0
  234. package/ui/rhine/original/archive-loop.ts +82 -0
  235. package/ui/rhine/original/archive-refill.ts +64 -0
  236. package/ui/rhine/original/array-detail-data.ts +2 -0
  237. package/ui/rhine/original/array-detail.ts +41 -0
  238. package/ui/rhine/original/array-interior.ts +411 -0
  239. package/ui/rhine/original/array-occlusion.ts +166 -0
  240. package/ui/rhine/original/array-shell.ts +38 -0
  241. package/ui/rhine/original/array-visibility.ts +309 -0
  242. package/ui/rhine/original/asset-url.ts +4 -0
  243. package/ui/rhine/original/brand.ts +12 -0
  244. package/ui/rhine/original/cassette-lod.ts +81 -0
  245. package/ui/rhine/original/data.ts +21 -0
  246. package/ui/rhine/original/decryption.ts +185 -0
  247. package/ui/rhine/original/detail-signature.ts +18 -0
  248. package/ui/rhine/original/glass-reveal.ts +45 -0
  249. package/ui/rhine/original/internal-optics.ts +70 -0
  250. package/ui/rhine/original/label-renderer.ts +119 -0
  251. package/ui/rhine/original/label-textures.ts +117 -0
  252. package/ui/rhine/original/model-viewer.ts +658 -0
  253. package/ui/rhine/original/motion.ts +116 -0
  254. package/ui/rhine/original/opaque-backfaces.ts +64 -0
  255. package/ui/rhine/original/program-preparation.ts +111 -0
  256. package/ui/rhine/original/quality-renderer.ts +97 -0
  257. package/ui/rhine/original/quality-settings.ts +111 -0
  258. package/ui/rhine/original/render-quality.ts +163 -0
  259. package/ui/rhine/original/scene.ts +2051 -0
  260. package/ui/rhine/original/shelf-interior.ts +142 -0
  261. package/ui/rhine/original/ui-transitions.ts +111 -0
  262. package/ui/rhine/original/viewer-camera.ts +92 -0
  263. package/ui/rhine/original/viewport-layout.ts +41 -0
  264. package/ui/rhine/original-ui.css +2334 -0
  265. package/ui/rhine/readability.css +46 -0
  266. package/ui/rhine/reading-object.ts +96 -0
  267. package/ui/rhine/reading-origin.ts +28 -0
  268. package/ui/rhine/reading-room.css +55 -0
  269. package/ui/rhine/report-markdown.ts +98 -0
  270. package/ui/rhine/scene-edge-navigation.ts +84 -0
  271. package/ui/rhine/scene.ts +1726 -0
  272. package/ui/rhine/shelf-layout.ts +38 -0
  273. package/ui/rhine/snapshot-update-gate.ts +22 -0
  274. package/ui/rhine/source-reading.ts +15 -0
  275. package/ui/rhine/source-selection.css +164 -0
  276. package/ui/rhine/temporary-performance-observers.ts +163 -0
  277. package/ui/rhine/temporary-performance-panel.ts +124 -0
  278. package/ui/rhine/temporary-performance.css +19 -0
  279. package/ui/rhine/temporary-performance.ts +552 -0
  280. package/ui/rhine/temporary-render-diagnostics.ts +105 -0
  281. package/ui/rhine/tool-activity.ts +69 -0
  282. package/ui/rhine/types.ts +219 -0
  283. package/ui/rhine/visible-scene-hit.ts +13 -0
  284. package/ui/rhine/workbench.css +818 -0
  285. package/ui/rhine/workbench.ts +2245 -0
  286. package/ui/rhine/workspace-travel.ts +32 -0
  287. package/vite.rhine.config.ts +19 -0
@@ -0,0 +1,658 @@
1
+ import * as THREE from "three";
2
+ import { OrbitControls } from "three/addons/controls/OrbitControls.js";
3
+ import { createArchiveLighting } from "./archive-lighting";
4
+ import { damp } from "./motion";
5
+ import { ViewerCameraMotion } from "./viewer-camera";
6
+ // TEMPORARY RHINE PROFILER
7
+ import type { RenderPerformanceCapture } from "../temporary-performance";
8
+ import { normalizeQuality, type RenderQuality } from "./render-quality";
9
+ import {
10
+ applyTextureQuality,
11
+ createViewerPipeline,
12
+ resizeQuality,
13
+ } from "./quality-renderer";
14
+
15
+ const PARTS = [
16
+ { id: "fasteners", label: "紧固件", en: "FASTENERS", depth: 2.75 },
17
+ { id: "cover", label: "透明盖板", en: "OPTICAL COVER", depth: 1.85 },
18
+ {
19
+ id: "optical-lenses",
20
+ label: "折射环组",
21
+ en: "REFRACTIVE RINGS",
22
+ depth: 0.75,
23
+ },
24
+ { id: "optical-core", label: "光学核心", en: "OPTICAL CORE", depth: -0.15 },
25
+ { id: "substrate", label: "信息基板", en: "SUBSTRATE", depth: -1.1 },
26
+ { id: "carrier", label: "背板与框架", en: "CARRIER", depth: -2.05 },
27
+ ] as const;
28
+
29
+ type ModelSource = { model: THREE.Group; dispose: () => void; setClarity?: (value: number) => void };
30
+ export class ModelViewer {
31
+ readonly root: HTMLElement;
32
+ private canvasHost: HTMLElement;
33
+ private renderer: THREE.WebGLRenderer;
34
+ private unregisterPerformance?: () => void;
35
+ private pipeline: ReturnType<typeof createViewerPipeline>;
36
+ private quality = normalizeQuality(undefined);
37
+ private appliedQuality = "";
38
+ private scene = new THREE.Scene();
39
+ private camera = new THREE.PerspectiveCamera(34, 16 / 9, 0.3, 120);
40
+ private controlCamera = this.camera.clone();
41
+ private cameraMotion = new ViewerCameraMotion(this.camera);
42
+ private controls: OrbitControls;
43
+ private source?: ModelSource;
44
+ private groups = new Map<string, THREE.Group>();
45
+ private spread = { value: 0, velocity: 0 };
46
+ private targetSpread = 0;
47
+ private clarity = { value: 1, velocity: 0 };
48
+ private targetClarity = 1;
49
+ private lastTime = 0;
50
+ private request = 0;
51
+ private reduced = false;
52
+ private loading = false;
53
+ private closing = false;
54
+ private transitions: Animation[] = [];
55
+ private transitionId = 0;
56
+ private status = "";
57
+ private opener: HTMLElement | null = null;
58
+ private siblings: { node: HTMLElement; inert: boolean }[] = [];
59
+ private initialCamera = new THREE.Vector3(7.2, 3.8, 12);
60
+ private onClose: () => void;
61
+ private provider?: () => Promise<ModelSource>;
62
+ isOpen = false;
63
+
64
+ constructor(
65
+ parent: HTMLElement,
66
+ onClose: () => void,
67
+ private onSound: (
68
+ sound: "explode" | "assemble" | "tick",
69
+ ) => void = () => {},
70
+ private performanceProbe?: RenderPerformanceCapture,
71
+ ) {
72
+ this.onClose = onClose;
73
+ this.root = document.createElement("section");
74
+ this.root.className = "model-viewer";
75
+ this.root.hidden = true;
76
+ this.root.setAttribute("role", "dialog");
77
+ this.root.setAttribute("aria-modal", "true");
78
+ this.root.setAttribute("aria-labelledby", "viewer-title");
79
+ this.root.innerHTML = `
80
+ <div class="viewer-canvas"></div>
81
+ <div class="scene-atmosphere viewer-atmosphere" aria-hidden="true"></div>
82
+ <header class="viewer-header">
83
+ <button class="viewer-back" data-viewer="close">← <span>返回档案</span><kbd>ESC</kbd></button>
84
+ <div class="viewer-heading"><span>RHINE LAB / OBJECT STUDY</span><h2 id="viewer-title">档案模型</h2><p id="viewer-file"></p></div>
85
+ <span class="viewer-index">360<span>°</span></span>
86
+ </header>
87
+ <div class="viewer-surface" role="group" aria-label="玻璃模式"><button data-viewer="clear" aria-pressed="true">清晰</button><button data-viewer="frosted" aria-pressed="false">磨砂</button></div>
88
+ <aside class="viewer-parts" aria-label="模型装配结构"><div>ASSEMBLY / 装配结构</div>${PARTS.map((p, i) => `<p><span>${String(i + 1).padStart(2, "0")}</span><strong>${p.label}</strong><small>${p.en}</small></p>`).join("")}</aside>
89
+ <div class="viewer-loading" role="status"><span>正在载入模型…</span><button data-viewer="retry" hidden>重新载入 ↗</button></div>
90
+ <footer class="viewer-footer">
91
+ <div class="viewer-help"><span>拖动旋转</span><span>↑ ↓ ← → 平移</span><span>滚轮缩放</span></div>
92
+ <div class="viewer-actions"><button data-viewer="explode" aria-pressed="false"><span>+</span> 拆解档案</button><button data-viewer="assemble" aria-pressed="true"><span>−</span> 一键重组</button></div>
93
+ <button class="viewer-reset" data-viewer="reset">复位视角 <span>↗</span></button>
94
+ </footer>
95
+ <div class="viewer-state" aria-live="polite">已组装</div>`;
96
+ parent.appendChild(this.root);
97
+ this.canvasHost = this.root.querySelector(".viewer-canvas")!;
98
+ this.renderer = new THREE.WebGLRenderer({
99
+ antialias: true,
100
+ powerPreference: "high-performance",
101
+ });
102
+ this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
103
+ this.renderer.toneMappingExposure = 1.05;
104
+ this.renderer.domElement.tabIndex = 0;
105
+ this.renderer.domElement.setAttribute(
106
+ "aria-label",
107
+ "档案三维模型:拖动旋转,方向键平移,滚轮或加减键缩放,Home 复位",
108
+ );
109
+ this.canvasHost.appendChild(this.renderer.domElement);
110
+ this.scene.background = new THREE.Color("#eae5e1");
111
+ this.scene.fog = new THREE.Fog("#eae5e1", 13.5, 26.5);
112
+ // Render-target textures belong to their WebGL context. Recreate the main
113
+ // scene's light room here so this renderer receives its actual illumination.
114
+ createArchiveLighting(this.renderer, this.scene);
115
+ this.camera.position.copy(this.initialCamera);
116
+ this.controlCamera.copy(this.camera);
117
+ this.controls = new OrbitControls(
118
+ this.controlCamera,
119
+ this.renderer.domElement,
120
+ );
121
+ this.controls.enableDamping = false;
122
+ this.pipeline = createViewerPipeline(
123
+ this.renderer,
124
+ this.scene,
125
+ this.camera,
126
+ );
127
+ this.pipeline.smaa.enabled = false;
128
+ this.unregisterPerformance = performanceProbe?.register('viewer', this.renderer, () => ({
129
+ open: this.isOpen, loading: this.loading, quality: { ...this.quality },
130
+ buffers: { output: { width: this.renderer.domElement.width, height: this.renderer.domElement.height },
131
+ composer: { width: this.pipeline.composer.readBuffer.width, height: this.pipeline.composer.readBuffer.height },
132
+ transmissionScale: this.renderer.transmissionResolutionScale, transmissionTarget: null },
133
+ parts: this.groups.size, ready: Boolean(this.source), spread: this.spread.value, clarity: this.clarity.value,
134
+ content: [...this.groups].map(([part, group]) => ({ part, children: group.children.length, visible: group.visible })),
135
+ passes: { scene: true, smaa: this.quality.antialias === 'smaa' },
136
+ }), () => this.pipeline.composer.passes.map((pass, index) => ({
137
+ name: index === 0 ? 'viewer-scene' : pass === this.pipeline.smaa ? 'viewer-smaa' : 'viewer-output', pass,
138
+ })));
139
+
140
+ this.controls.rotateSpeed = 0.65;
141
+ this.controls.zoomSpeed = 0.7;
142
+ this.controls.panSpeed = 0.7;
143
+ this.controls.minDistance = 5;
144
+ this.controls.maxDistance = 28;
145
+ this.controls.maxTargetRadius = 5;
146
+ this.controls.screenSpacePanning = true;
147
+ this.controls.touches.ONE = THREE.TOUCH.ROTATE;
148
+ this.controls.touches.TWO = THREE.TOUCH.DOLLY_PAN;
149
+ this.controls.enabled = false;
150
+ this.controls.update();
151
+ this.cameraMotion.snap(this.controlCamera, this.controls.target);
152
+ this.controls.addEventListener("start", () => this.interruptReset());
153
+ this.root.addEventListener("click", (event) => {
154
+ if (this.closing) return;
155
+ const action = (event.target as HTMLElement).closest<HTMLElement>(
156
+ "[data-viewer]",
157
+ )?.dataset.viewer;
158
+ if (action === "close") this.close();
159
+ if (action === "retry") void this.load();
160
+ if (this.loading || !this.source) return;
161
+ if (action === "clear" || action === "frosted") {
162
+ this.setSurface(action === "clear");
163
+ this.onSound("tick");
164
+ }
165
+ if (action === "explode" && this.targetSpread !== 1) {
166
+ this.setExploded(true);
167
+ this.onSound("explode");
168
+ }
169
+ if (action === "assemble" && this.targetSpread !== 0) {
170
+ this.setExploded(false);
171
+ this.onSound("assemble");
172
+ }
173
+ if (action === "reset") {
174
+ this.resetView();
175
+ this.onSound("tick");
176
+ }
177
+ });
178
+ this.root.addEventListener("keydown", (event) => this.keydown(event));
179
+ }
180
+
181
+ open(
182
+ id: string,
183
+ title: string,
184
+ provider: () => Promise<ModelSource>,
185
+ reduced: boolean,
186
+ ) {
187
+ if (this.isOpen) return;
188
+ this.isOpen = true;
189
+ this.closing = false;
190
+ this.reduced = reduced;
191
+ this.provider = provider;
192
+ this.opener = document.activeElement as HTMLElement | null;
193
+ this.siblings = [...this.root.parentElement!.children]
194
+ .filter(
195
+ (node): node is HTMLElement =>
196
+ node instanceof HTMLElement && node !== this.root,
197
+ )
198
+ .map((node) => ({ node, inert: node.inert }));
199
+ this.siblings.forEach(({ node }) => (node.inert = true));
200
+ this.root.hidden = false;
201
+ this.root.dataset.transition = "opening";
202
+ this.root.querySelector("#viewer-title")!.textContent = title;
203
+ this.root.querySelector("#viewer-file")!.textContent =
204
+ "FILE " + id + " / INTERNAL DATABASE";
205
+ this.spread = { value: 0, velocity: 0 };
206
+ this.targetSpread = 0;
207
+ this.clarity = { value: 1, velocity: 0 };
208
+ this.setSurface(true);
209
+ this.lastTime = 0;
210
+ this.root.dataset.exploded = "false";
211
+ this.resetView(false);
212
+ this.resize();
213
+ this.renderer.domElement.focus({ preventScroll: true });
214
+ this.enter();
215
+ void this.load();
216
+ }
217
+
218
+ private async load() {
219
+ if (!this.provider || this.loading) return;
220
+ const ticket = ++this.request;
221
+ const finishLoad = this.performanceProbe?.beginLoad('viewer-setup');
222
+ let loadStatus: 'complete' | 'error' | 'aborted' = 'error';
223
+ this.loading = true;
224
+ this.controls.enabled = false;
225
+ const loading = this.root.querySelector<HTMLElement>(".viewer-loading")!;
226
+ loading.hidden = false;
227
+ loading.querySelector("span")!.textContent = "正在载入模型…";
228
+ loading.querySelector<HTMLElement>("button")!.hidden = true;
229
+ this.setButtonsDisabled(true);
230
+ try {
231
+ const source = await this.provider();
232
+ if (!this.isOpen || this.closing || ticket !== this.request) {
233
+ source.dispose(); loadStatus = 'aborted';
234
+ return;
235
+ }
236
+ this.source = source;
237
+ for (const part of PARTS) {
238
+ const group = new THREE.Group();
239
+ group.name = part.id;
240
+ this.groups.set(part.id, group);
241
+ }
242
+ for (const child of [...source.model.children]) {
243
+ const group = this.groups.get(child.userData.assemblyPart ?? "cover");
244
+ group?.add(child);
245
+ }
246
+ for (const group of this.groups.values()) source.model.add(group);
247
+ source.model.position.set(0, -1.85, 0);
248
+ this.scene.add(source.model);
249
+ applyTextureQuality(source.model, this.renderer, this.quality);
250
+ this.loading = false;
251
+ loading.hidden = true;
252
+ this.controls.enabled = true;
253
+ this.setButtonsDisabled(false);
254
+ this.setExploded(false);
255
+ this.setStatus("已组装");
256
+ // Render before revealing the canvas so a new model never flashes in.
257
+ this.update(this.lastTime);
258
+ if (!this.reduced)
259
+ this.transitions.push(
260
+ this.canvasHost.animate(
261
+ [
262
+ { opacity: 0, transform: "scale(0.97)" },
263
+ { opacity: 1, transform: "scale(1)" },
264
+ ],
265
+ { duration: 380, easing: "cubic-bezier(0.22, 1, 0.36, 1)" },
266
+ ),
267
+ );
268
+ loadStatus = 'complete';
269
+ } catch (error) {
270
+ if (!this.isOpen || this.closing || ticket !== this.request) return;
271
+ this.loading = false;
272
+ loading.querySelector("span")!.textContent = "模型载入失败,请重试";
273
+ loading.querySelector<HTMLElement>("button")!.hidden = false;
274
+ console.error("Model viewer failed to load", error);
275
+ } finally { finishLoad?.(loadStatus); }
276
+ }
277
+
278
+ private enter() {
279
+ const ticket = ++this.transitionId;
280
+ this.transitions.forEach((animation) => animation.cancel());
281
+ this.transitions = [];
282
+ if (this.reduced) {
283
+ this.root.dataset.transition = "open";
284
+ return;
285
+ }
286
+ const fade = this.root.animate([{ opacity: 0 }, { opacity: 1 }], {
287
+ duration: 320,
288
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
289
+ });
290
+ this.transitions.push(fade);
291
+ for (const selector of [
292
+ ".viewer-header",
293
+ ".viewer-footer",
294
+ ".viewer-state",
295
+ ]) {
296
+ const element = this.root.querySelector<HTMLElement>(selector)!;
297
+ this.transitions.push(
298
+ element.animate(
299
+ [
300
+ { opacity: 0, translate: "0 10px" },
301
+ { opacity: 1, translate: "0 0" },
302
+ ],
303
+ {
304
+ duration: 300,
305
+ delay: 60,
306
+ fill: "backwards",
307
+ easing: "cubic-bezier(0.22, 1, 0.36, 1)",
308
+ },
309
+ ),
310
+ );
311
+ }
312
+ void fade.finished
313
+ .then(() => {
314
+ if (ticket === this.transitionId) this.root.dataset.transition = "open";
315
+ })
316
+ .catch(() => {});
317
+ }
318
+
319
+ close() {
320
+ if (!this.isOpen || this.closing) return;
321
+ this.closing = true;
322
+ this.request++;
323
+ this.loading = false;
324
+ this.controls.enabled = false;
325
+ this.setButtonsDisabled(true);
326
+ const ticket = ++this.transitionId;
327
+ // Capture the current fade when Escape interrupts opening.
328
+ const opacity = getComputedStyle(this.root).opacity;
329
+ const canvasStyle = getComputedStyle(this.canvasHost);
330
+ const canvasOpacity = canvasStyle.opacity;
331
+ const transform = canvasStyle.transform;
332
+ this.transitions.forEach((animation) => animation.cancel());
333
+ this.transitions = [];
334
+ this.root.dataset.transition = "closing";
335
+ if (this.reduced) {
336
+ this.finishClose();
337
+ return;
338
+ }
339
+ const fade = this.root.animate([{ opacity }, { opacity: 0 }], {
340
+ duration: 220,
341
+ easing: "cubic-bezier(0.4, 0, 1, 1)",
342
+ fill: "forwards",
343
+ });
344
+ this.transitions.push(
345
+ fade,
346
+ this.canvasHost.animate(
347
+ [
348
+ { transform, opacity: canvasOpacity },
349
+ { transform: "scale(0.97)", opacity: 0 },
350
+ ],
351
+ {
352
+ duration: 220,
353
+ easing: "cubic-bezier(0.4, 0, 1, 1)",
354
+ fill: "forwards",
355
+ },
356
+ ),
357
+ );
358
+ void fade.finished
359
+ .then(() => {
360
+ if (ticket === this.transitionId) this.finishClose();
361
+ })
362
+ .catch(() => {});
363
+ }
364
+
365
+ private finishClose() {
366
+ // Keep rendering and retain modal focus until the visible exit completes.
367
+ this.isOpen = false;
368
+ this.closing = false;
369
+ this.root.hidden = true;
370
+ this.transitions.forEach((animation) => animation.cancel());
371
+ this.transitions = [];
372
+ if (this.source) {
373
+ this.scene.remove(this.source.model);
374
+ this.source.dispose();
375
+ this.source = undefined;
376
+ }
377
+ this.groups.clear();
378
+ this.siblings.forEach(({ node, inert }) => (node.inert = inert));
379
+ this.siblings = [];
380
+ this.opener?.focus({ preventScroll: true });
381
+ this.onClose();
382
+ }
383
+
384
+ private setButtonsDisabled(disabled: boolean) {
385
+ for (const action of ["explode", "assemble", "reset", "clear", "frosted"]) {
386
+ this.root.querySelector<HTMLButtonElement>(
387
+ `[data-viewer="${action}"]`,
388
+ )!.disabled = disabled;
389
+ }
390
+ }
391
+ private setSurface(clear: boolean) {
392
+ this.targetClarity = clear ? 1 : 0;
393
+ this.root.dataset.surface = clear ? "clear" : "frosted";
394
+ this.root.querySelector('[data-viewer="clear"]')!.setAttribute("aria-pressed", String(clear));
395
+ this.root.querySelector('[data-viewer="frosted"]')!.setAttribute("aria-pressed", String(!clear));
396
+ if (this.reduced) this.clarity = { value: this.targetClarity, velocity: 0 };
397
+ }
398
+ private setExploded(value: boolean) {
399
+ this.targetSpread = value ? 1 : 0;
400
+ this.root.dataset.exploded = String(value);
401
+ this.root
402
+ .querySelector('[data-viewer="explode"]')!
403
+ .setAttribute("aria-pressed", String(value));
404
+ this.root
405
+ .querySelector('[data-viewer="assemble"]')!
406
+ .setAttribute("aria-pressed", String(!value));
407
+ this.setStatus(
408
+ value ? "正在拆解" : this.spread.value > 0.001 ? "正在重组" : "已组装",
409
+ );
410
+ if (this.reduced) this.spread = { value: this.targetSpread, velocity: 0 };
411
+ }
412
+ private setStatus(value: string) {
413
+ if (value !== this.status) {
414
+ this.status = value;
415
+ this.root.querySelector(".viewer-state")!.textContent = value;
416
+ }
417
+ }
418
+ private resetView(animated = true) {
419
+ this.controls.enabled = false;
420
+ this.controls.enableDamping = false;
421
+ this.controls.update();
422
+ this.controls.target.set(0, 0, 0);
423
+ this.controlCamera.position.copy(this.initialCamera);
424
+ this.controls.enableDamping = false;
425
+ this.controls.update();
426
+ if (animated && !this.reduced) this.cameraMotion.reset();
427
+ else this.cameraMotion.snap(this.controlCamera, this.controls.target);
428
+ this.controls.enabled =
429
+ this.isOpen && !this.loading && Boolean(this.source);
430
+ }
431
+ private interruptReset() {
432
+ if (!this.cameraMotion.resetting) return;
433
+ this.cameraMotion.interruptReset(this.controlCamera, this.controls.target);
434
+ this.controls.update();
435
+ }
436
+ private keydown(event: KeyboardEvent) {
437
+ event.stopPropagation();
438
+ if (event.key === "Escape") {
439
+ event.preventDefault();
440
+ this.close();
441
+ return;
442
+ }
443
+ if (this.closing) {
444
+ event.preventDefault();
445
+ return;
446
+ }
447
+ if (event.key === "Tab") {
448
+ const elements = [
449
+ ...this.root.querySelectorAll<HTMLElement>(
450
+ 'button:not([disabled]):not([hidden]),canvas[tabindex="0"]',
451
+ ),
452
+ ];
453
+ const first = elements[0],
454
+ last = elements.at(-1);
455
+ if (event.shiftKey && document.activeElement === first) {
456
+ event.preventDefault();
457
+ last?.focus();
458
+ }
459
+ if (!event.shiftKey && document.activeElement === last) {
460
+ event.preventDefault();
461
+ first?.focus();
462
+ }
463
+ return;
464
+ }
465
+ if (!this.source || this.loading) return;
466
+ if (event.key === "Home") {
467
+ event.preventDefault();
468
+ this.resetView();
469
+ this.onSound("tick");
470
+ return;
471
+ }
472
+ if (["+", "=", "-"].includes(event.key)) {
473
+ event.preventDefault();
474
+ this.interruptReset();
475
+ const distance = this.controlCamera.position.distanceTo(
476
+ this.controls.target,
477
+ );
478
+ const next = THREE.MathUtils.clamp(
479
+ distance * (event.key === "-" ? 1.12 : 1 / 1.12),
480
+ 5,
481
+ 28,
482
+ );
483
+ this.controlCamera.position
484
+ .sub(this.controls.target)
485
+ .multiplyScalar(next / distance)
486
+ .add(this.controls.target);
487
+ this.controls.update();
488
+ return;
489
+ }
490
+ if (
491
+ ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)
492
+ ) {
493
+ event.preventDefault();
494
+ this.interruptReset();
495
+ const right = new THREE.Vector3().setFromMatrixColumn(
496
+ this.camera.matrix,
497
+ 0,
498
+ );
499
+ const up = new THREE.Vector3().setFromMatrixColumn(this.camera.matrix, 1);
500
+ const delta = new THREE.Vector3();
501
+ const step =
502
+ this.controlCamera.position.distanceTo(this.controls.target) * 0.025;
503
+ if (event.key === "ArrowLeft") delta.addScaledVector(right, -step);
504
+ if (event.key === "ArrowRight") delta.addScaledVector(right, step);
505
+ if (event.key === "ArrowUp") delta.addScaledVector(up, step);
506
+ if (event.key === "ArrowDown") delta.addScaledVector(up, -step);
507
+ // Clamp the requested focus before moving the camera by the same amount.
508
+ // This preserves orbit radius at the panning limit.
509
+ const previous = this.controls.target.clone();
510
+ this.controls.target.add(delta);
511
+ this.controls.target.clampLength(0, this.controls.maxTargetRadius);
512
+ this.controlCamera.position.add(
513
+ this.controls.target.clone().sub(previous),
514
+ );
515
+ this.controls.update();
516
+ }
517
+ }
518
+
519
+ setQuality(quality: RenderQuality) {
520
+ const key = JSON.stringify(quality);
521
+ if (this.appliedQuality === key) return;
522
+ this.appliedQuality = key;
523
+ this.quality = normalizeQuality(quality);
524
+ this.pipeline.smaa.enabled = this.quality.antialias === "smaa";
525
+ applyTextureQuality(this.scene, this.renderer, this.quality);
526
+ this.resize();
527
+ }
528
+
529
+ resize() {
530
+ if (!this.isOpen) return;
531
+ const width = this.canvasHost.clientWidth,
532
+ height = this.canvasHost.clientHeight;
533
+ resizeQuality(
534
+ this.renderer,
535
+ this.pipeline.composer,
536
+ this.canvasHost,
537
+ this.quality,
538
+ );
539
+ this.camera.aspect = width / height;
540
+ this.camera.updateProjectionMatrix();
541
+ this.controlCamera.aspect = this.camera.aspect;
542
+ this.controlCamera.updateProjectionMatrix();
543
+ const touch = matchMedia("(pointer: coarse)").matches;
544
+ const help = this.root.querySelector(".viewer-help")!;
545
+ help.innerHTML = touch
546
+ ? "<span>单指旋转</span><span>双指缩放 / 平移</span>"
547
+ : "<span>拖动旋转</span><span>↑ ↓ ← → 平移</span><span>滚轮缩放</span>";
548
+ }
549
+
550
+ update(time: number, rafTime: number | null = null) {
551
+ if (!this.isOpen) return;
552
+ // TEMPORARY RHINE PROFILER: one-off renders are timed without counting as RAF ticks.
553
+ const measured = this.performanceProbe?.running ? this.performanceProbe.begin(rafTime, this.renderer, 'viewer') : undefined;
554
+ let failed = true;
555
+ try { this.updateFrame(time, measured ? stage => this.performanceProbe!.checkpoint(measured, stage) : undefined); failed = false; }
556
+ finally {
557
+ if (measured) measured.sample.counters = { modelReady: Boolean(this.source), loading: this.loading,
558
+ parts: this.groups.size, exploded: this.targetSpread === 1, clearSurface: this.targetClarity === 1,
559
+ smaaEnabled: this.quality.antialias === 'smaa' };
560
+ this.performanceProbe?.end(measured, failed);
561
+ }
562
+ }
563
+
564
+ private updateFrame(time: number, checkpoint?: (stage: string) => void) {
565
+ const dt = Math.min(this.lastTime ? time - this.lastTime : 1 / 60, 0.05);
566
+ this.lastTime = time;
567
+ if (this.source) {
568
+ damp(this.clarity, this.targetClarity, 8, dt);
569
+ if (Math.abs(this.clarity.value - this.targetClarity) < .0001 && Math.abs(this.clarity.velocity) < .001)
570
+ this.clarity = { value: this.targetClarity, velocity: 0 };
571
+ this.source.setClarity?.(this.clarity.value);
572
+ damp(this.spread, this.targetSpread, this.reduced ? 45 : 5.5, dt);
573
+ if (
574
+ Math.abs(this.spread.value - this.targetSpread) < 0.0001 &&
575
+ Math.abs(this.spread.velocity) < 0.001
576
+ ) {
577
+ this.spread = { value: this.targetSpread, velocity: 0 };
578
+ this.setStatus(this.targetSpread ? "已拆解" : "已组装");
579
+ }
580
+ for (const part of PARTS) {
581
+ this.groups.get(part.id)!.position.z = part.depth * this.spread.value;
582
+ }
583
+ }
584
+ this.controls.update();
585
+ this.cameraMotion.update(
586
+ this.controlCamera,
587
+ this.controls.target,
588
+ dt,
589
+ this.reduced,
590
+ );
591
+ const portrait = this.root.closest<HTMLElement>("[data-layout]")?.dataset.layout === "portrait";
592
+ const zoom = portrait ? Math.min(1.15, this.camera.aspect / 0.85) / (1 + 0.08 * this.spread.value) : 1;
593
+ if (this.camera.zoom !== zoom) {
594
+ this.camera.zoom = zoom;
595
+ this.controlCamera.zoom = zoom;
596
+ this.camera.updateProjectionMatrix();
597
+ this.controlCamera.updateProjectionMatrix();
598
+ }
599
+ // Match the detail scene's gentle haze without washing out the object as
600
+ // the user zooms. The assembled model is centered on the world origin.
601
+ const fog = this.scene.fog as THREE.Fog;
602
+ const objectDistance = this.camera.position.length();
603
+ fog.near = Math.max(0, objectDistance - 1);
604
+ fog.far = objectDistance + 12;
605
+ checkpoint?.("viewerUpdate");
606
+ if (this.quality.antialias === "smaa") this.pipeline.composer.render();
607
+ else this.renderer.render(this.scene, this.camera);
608
+ checkpoint?.('viewerRender');
609
+ this.root.dataset.stats = JSON.stringify({
610
+ ready: Boolean(this.source),
611
+ clarity: this.clarity.value,
612
+ targetClarity: this.targetClarity,
613
+ spread: this.spread.value,
614
+ target: this.targetSpread,
615
+ distance: this.camera.position.distanceTo(this.cameraMotion.focus),
616
+ targetPosition: this.cameraMotion.focus.toArray(),
617
+ requestedTarget: this.controls.target.toArray(),
618
+ requestedDistance: this.controlCamera.position.distanceTo(
619
+ this.controls.target,
620
+ ),
621
+ cameraPosition: this.camera.position.toArray(),
622
+ resetting: this.cameraMotion.resetting,
623
+ azimuth: this.controls.getAzimuthalAngle(),
624
+ polar: this.controls.getPolarAngle(),
625
+ parts: [...this.groups].map(([id, group]) => ({
626
+ id,
627
+ z: group.position.z,
628
+ meshes: group.children.length,
629
+ })),
630
+ });
631
+ }
632
+
633
+ /** Host plugin teardown: retire pending loads and this independent WebGL context. */
634
+ dispose() {
635
+ this.request++;
636
+ this.transitionId++;
637
+ this.isOpen = false;
638
+ this.closing = true;
639
+ this.transitions.forEach(animation => animation.cancel());
640
+ this.transitions = [];
641
+ if (this.source) {
642
+ this.scene.remove(this.source.model);
643
+ this.source.dispose();
644
+ this.source = undefined;
645
+ }
646
+ this.groups.clear();
647
+ this.siblings.forEach(({ node, inert }) => { node.inert = inert; });
648
+ this.siblings = [];
649
+ this.controls.dispose();
650
+ for (const pass of this.pipeline.composer.passes) pass.dispose?.();
651
+ this.pipeline.composer.dispose();
652
+ this.scene.environment?.dispose();
653
+ this.unregisterPerformance?.();
654
+ this.renderer.dispose();
655
+ this.renderer.forceContextLoss();
656
+ this.root.remove();
657
+ }
658
+ }