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,38 @@
1
+ // One bounded page spans both physical tiers; source order fills bottom first.
2
+ export const SHELF_LEVELS = 2;
3
+ export const SHELF_SLOTS_PER_LEVEL = 12;
4
+ export const SHELF_PAGE_SIZE = SHELF_LEVELS * SHELF_SLOTS_PER_LEVEL;
5
+ export const SHELF_SLOT_SPACING = 0.78;
6
+ export const SHELF_LEVEL_SPACING = 4.6;
7
+ export const SHELF_WIDTH = 5.35;
8
+ export const SHELF_DEPTH = (SHELF_SLOTS_PER_LEVEL - 1) * SHELF_SLOT_SPACING + 1.05;
9
+ export const SHELF_HEIGHT = (SHELF_LEVELS - 1) * SHELF_LEVEL_SPACING + 4.3;
10
+ // Clear the entire cassette width before turning or changing tiers.
11
+ export const SHELF_EXIT_DISTANCE = SHELF_WIDTH + 0.65;
12
+
13
+ // The reading station stays outside the rack's full rotation envelope and in
14
+ // front of its nearest row. Moving the camera cannot make an internal target safe.
15
+ export const SHELF_READING_POSITION = {
16
+ x: -SHELF_EXIT_DISTANCE, y: SHELF_HEIGHT / 2 - 1.85, z: SHELF_DEPTH / 2 + 3.2,
17
+ };
18
+ export const SHELF_WITHDRAW_END = 0.38;
19
+ export function shelfReadingPath(index: number, progress: number, focus = 0) {
20
+ const slot = shelfSlot(index);
21
+ const smooth = (v: number) => { const t = Math.max(0, Math.min(1, v)); return t * t * t * (t * (t * 6 - 15) + 10); };
22
+ const turn = smooth((progress - SHELF_WITHDRAW_END) / (1 - SHELF_WITHDRAW_END));
23
+ if (progress < SHELF_WITHDRAW_END) return {
24
+ x: -focus * 0.5 + (-SHELF_EXIT_DISTANCE + focus * 0.5) * smooth(progress / SHELF_WITHDRAW_END),
25
+ y: slot.y, z: slot.z, turn: 0,
26
+ };
27
+ return { x: SHELF_READING_POSITION.x,
28
+ y: slot.y + (SHELF_READING_POSITION.y - slot.y) * turn,
29
+ z: slot.z + (SHELF_READING_POSITION.z - slot.z) * turn, turn };
30
+ }
31
+
32
+ export function shelfSlot(index: number) {
33
+ const pageSlot = ((Math.trunc(index) % SHELF_PAGE_SIZE) + SHELF_PAGE_SIZE) % SHELF_PAGE_SIZE;
34
+ const level = Math.floor(pageSlot / SHELF_SLOTS_PER_LEVEL);
35
+ const slot = pageSlot % SHELF_SLOTS_PER_LEVEL;
36
+ return { level, slot, x: 0, y: 0.28 + level * SHELF_LEVEL_SPACING,
37
+ z: (slot - (SHELF_SLOTS_PER_LEVEL - 1) / 2) * SHELF_SLOT_SPACING };
38
+ }
@@ -0,0 +1,22 @@
1
+ import type { InvestigationSnapshot } from './types';
2
+
3
+ /** Keep value signatures: snapshots may mutate in place or contain equal new objects. */
4
+ export class SnapshotUpdateGate {
5
+ private session = '';
6
+ private sources = '';
7
+ private status = '';
8
+ constructor(snapshot: InvestigationSnapshot) { this.read(snapshot); }
9
+ read(snapshot: InvestigationSnapshot) {
10
+ // Records and answer text belong to the reading panels. Answer presence still
11
+ // affects the status/report entry; every other field remains significant.
12
+ const { sources, answer, records: _records, ...status } = snapshot;
13
+ const nextSources = JSON.stringify([snapshot.sessionId, sources]);
14
+ const nextStatus = JSON.stringify({ ...status, hasAnswer: Boolean(answer) });
15
+ const sessionChanged = snapshot.sessionId !== this.session;
16
+ this.session = snapshot.sessionId;
17
+ const sourcesChanged = nextSources !== this.sources;
18
+ const statusChanged = sourcesChanged || nextStatus !== this.status;
19
+ this.sources = nextSources; this.status = nextStatus;
20
+ return { sessionChanged, sourcesChanged, statusChanged };
21
+ }
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { ArchiveSource } from './types';
2
+
3
+ /** A citation's reading position is independent of cumulative search/read receipts. */
4
+ export function referenceSource(source: ArchiveSource, ref: { line_start?: number; line_end?: number }): ArchiveSource {
5
+ return ref.line_start ? { ...source, lineStart: ref.line_start, lineEnd: ref.line_end ?? ref.line_start,
6
+ readingRange: { start: ref.line_start, end: ref.line_end ?? ref.line_start } } : source;
7
+ }
8
+
9
+ export function resolveReadingSource(source: ArchiveSource, latest?: ArchiveSource): ArchiveSource {
10
+ return latest ? { ...latest, ...(source.readingRange ? { readingRange: source.readingRange } : {}) } : source;
11
+ }
12
+
13
+ export function readerStartLine(source: ArchiveSource) {
14
+ return Math.max(1, (source.readingRange?.start || source.readRanges?.[0]?.start || source.lineStart || 1) - 3);
15
+ }
@@ -0,0 +1,164 @@
1
+ /* Title-first selectors remain on the HUD plane; the physical archive stays behind them. */
2
+ .rhine-workbench .rhine-original-stage{--choice-ink:#30382d;--choice-muted:#707461;--choice-rule:#7c81642e;--choice-font:max(17px,calc(14px / var(--stage-scale,1)))}
3
+ .rhine-workbench [data-layout] .rhine-source-choice{display:grid;grid-template-columns:33px minmax(0,1fr) 16px;gap:10px;align-items:start;width:100%;height:auto;min-height:70px;margin:0;padding:10px 10px 10px 2px;border:0;border-bottom:1px solid var(--choice-rule);text-align:left;color:var(--choice-ink);background:transparent;transform:none;transition:background .18s,color .18s}
4
+ .rhine-workbench [data-layout] .rhine-choice-number{font-size:11px;font-variant-numeric:tabular-nums;color:var(--choice-muted);padding-top:4px}
5
+ .rhine-workbench [data-layout] .rhine-choice-copy{min-width:0;display:grid;gap:4px}
6
+ .rhine-workbench [data-layout] .rhine-choice-copy strong{font-size:var(--choice-font);font-weight:500;line-height:1.45;overflow-wrap:anywhere;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
7
+ .rhine-workbench [data-layout] .rhine-choice-copy small{font-size:11px;line-height:1.5;color:var(--choice-muted)}
8
+ .rhine-workbench [data-layout] .rhine-choice-mark{font-size:16px;opacity:0;padding-top:2px;color:#62714e}
9
+ .rhine-workbench [data-layout] .rhine-source-choice:is(:hover,:focus-visible,.selected,.is-selected){background:transparent;color:#283222;transform:none;border-bottom:1px solid #90967d70}
10
+ .rhine-workbench [data-layout] .rhine-source-choice:is(.selected,.is-selected){box-shadow:inset 2px 0 #78835c;padding-left:9px}
11
+ .rhine-workbench [data-layout] .rhine-source-choice:is(:hover,:focus-visible,.selected,.is-selected) .rhine-choice-mark{opacity:1}
12
+ .rhine-workbench [data-layout] .rhine-source-choice:focus-visible{outline:1px solid #7a845e;outline-offset:-1px}
13
+ .rhine-workbench [data-layout] .rhine-source-choice:is([data-state=read],[data-state=cited]) .rhine-choice-number{color:#52683e}
14
+ .rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list){scrollbar-width:thin;scrollbar-color:#82877270 transparent;overscroll-behavior:contain;scroll-padding-block:5px}
15
+ .rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar{width:4px}
16
+ .rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar-track{background:transparent}
17
+ .rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar-thumb{background:#82877270;border-radius:2px}
18
+ .rhine-workbench :is(.file-ticks,.rhine-desk-list,.rhine-rack-index-list,.rhine-result-list)::-webkit-scrollbar-button{display:none}
19
+
20
+ /* The array's former anonymous ticks are now a bounded title directory. */
21
+ .rhine-workbench [data-layout] .archive-navigation{left:var(--edge);top:24%;bottom:235px;width:min(440px,32%);display:flex;flex-direction:column;align-items:stretch;gap:0;transform:none;background:transparent;padding:0 16px 0 0}
22
+ .rhine-workbench [data-layout] .rhine-choice-heading{display:flex;align-items:center;justify-content:space-between;gap:10px;color:var(--choice-muted);font-size:10px;letter-spacing:1px;min-height:28px}
23
+ .rhine-workbench [data-layout] .rhine-choice-heading button{font-size:12px;letter-spacing:0;min-height:32px;white-space:nowrap;color:var(--choice-ink)}
24
+ .rhine-workbench [data-layout] .archive-navigation .column-navigation{position:static;display:flex;flex:none;align-items:center;justify-content:space-between;gap:15px;border-bottom:1px solid #7c816461;padding:10px 0 14px;transform:none}
25
+ .rhine-workbench .archive-navigation .column-navigation>div{flex:1;min-width:0;gap:4px}
26
+ .rhine-workbench .archive-navigation .column-navigation strong{font-size:24px}
27
+ .rhine-workbench .archive-navigation .column-navigation button{height:38px;width:30px;font-size:22px}
28
+ .rhine-workbench [data-layout] .file-ticks{display:flex;flex-direction:column;align-items:stretch;gap:0;min-height:0;max-width:none;flex:1;overflow:auto}
29
+ .rhine-workbench [data-layout] .file-ticks button{flex:none;width:100%;height:auto;min-height:70px}
30
+ .rhine-workbench [data-layout] .file-ticks button:before{display:none}
31
+ .rhine-workbench [data-layout] .rhine-choice-footer{flex:none;display:flex;justify-content:space-between;align-items:center;gap:12px;padding:12px 0;border-top:1px solid var(--choice-rule);font-size:11px;color:var(--choice-muted)}
32
+ .rhine-workbench [data-layout] .rhine-choice-footer button{font-size:12px;min-height:30px;color:var(--choice-ink)}
33
+ .rhine-workbench [data-layout] .rhine-choice-footer button:disabled{opacity:.35}
34
+ .rhine-workbench [data-layout] :is(.archive-counter,.archive-hint){display:none}
35
+
36
+ /* A persistent header and reading action frame the scrollable shelf directory. */
37
+ .rhine-workbench [data-layout] .rhine-desk-panel{left:var(--edge);top:21%;bottom:190px;width:min(460px,35%);max-height:none;overflow:visible;gap:0;background:transparent;padding-right:16px}
38
+ .rhine-workbench [data-layout] .rhine-desk-panel h2{font-size:30px;margin:10px 0 5px}
39
+ .rhine-workbench [data-layout] .rhine-desk-panel>.rhine-rack-intro{font-size:12px;line-height:1.6;margin:0 0 10px;color:var(--choice-muted)}
40
+ .rhine-workbench [data-layout] .rhine-desk-panel>:not(.rhine-shelf-navigation){flex:none}
41
+ .rhine-workbench [data-layout] .rhine-shelf-search{display:flex;align-items:center;gap:9px;border-bottom:1px solid #7c816478;min-height:40px}
42
+ .rhine-workbench [data-layout] .rhine-shelf-search>span{font-size:23px;color:#697153}
43
+ .rhine-workbench [data-layout] .rhine-shelf-filter{background:transparent;border:0;outline:none;box-shadow:none;font:inherit;font-size:13px;min-width:0;flex:1;padding:10px 0;color:var(--choice-ink)}
44
+ .rhine-workbench [data-layout] .rhine-shelf-filter::placeholder{color:#737665}
45
+ .rhine-workbench [data-layout] .rhine-shelf-filter::-webkit-search-cancel-button{display:none}
46
+ .rhine-workbench [data-layout] .rhine-shelf-search:focus-within{border-color:#65734c}
47
+ .rhine-workbench [data-layout] .rhine-shelf-clear{width:30px;min-height:32px;font-size:19px}
48
+ .rhine-workbench [data-layout] .rhine-rack-range{font-size:11px;color:var(--choice-muted);margin:10px 0 5px}
49
+ .rhine-workbench [data-layout] .rhine-shelf-navigation{display:flex;flex:1;min-height:80px;gap:0;margin:0;align-items:stretch;order:1}
50
+ .rhine-workbench [data-layout] .rhine-shelf-navigation>button{display:none}
51
+ .rhine-workbench [data-layout] .rhine-desk-list{display:flex;flex-direction:column;min-height:0;overflow:auto;gap:0;padding-right:5px;border-bottom:1px solid var(--choice-rule)}
52
+ .rhine-workbench [data-layout] .rhine-desk-item{flex:none;display:grid;font-size:inherit;text-align:left;border-bottom-width:1px;min-height:70px}
53
+ .rhine-workbench [data-layout] .rhine-desk-pagination{order:2;display:flex;justify-content:space-between;align-items:center;padding:3px 0;margin:0;min-height:32px;color:var(--choice-muted);font-size:11px}
54
+ .rhine-workbench [data-layout] .rhine-desk-pagination button{font-size:12px;min-height:30px}
55
+ .rhine-workbench [data-layout] .rhine-desk-pagination[hidden]{display:none}
56
+ .rhine-workbench [data-layout] .rhine-shelf-current{order:3;position:relative;border-block:0;border-top:1px solid #7c816461;padding:12px 0 0;margin:0}
57
+ .rhine-workbench [data-layout] .rhine-shelf-number{font-size:11px;display:block;padding-right:100px}
58
+ .rhine-workbench [data-layout] .rhine-shelf-title{font-size:17px;line-height:1.5;margin:10px 0 6px;-webkit-line-clamp:2}
59
+ .rhine-workbench [data-layout] .rhine-shelf-meta{font-size:11px;color:var(--choice-muted)}
60
+ .rhine-workbench [data-layout] .rhine-shelf-excerpt{font-size:12px;line-height:1.6;-webkit-line-clamp:2;margin:6px 0;color:var(--choice-muted)}
61
+ .rhine-workbench [data-layout] .rhine-shelf-open{position:absolute;right:0;top:3px;width:auto;gap:12px;min-height:34px;margin:0;font-size:13px;color:#39482c}
62
+ .rhine-workbench [data-layout] .rhine-rack-directory{order:4;min-height:34px;margin-top:5px;font-size:12px;border-top:1px solid var(--choice-rule)}
63
+ .rhine-workbench [data-layout] .rhine-rack-legend,.rhine-workbench [data-layout] .rhine-shelf-report{display:none}
64
+
65
+ /* Directory, extracts and receipts use the same thin, translucent reading surface. */
66
+ .rhine-workbench [data-layout] :is(.rhine-results,.rhine-rack-index,.rhine-basket,.rhine-log).modal-backdrop{background:linear-gradient(90deg,#eeece57a,#eeece5c9 28%,#eeece5b3 75%,#eeece573);backdrop-filter:blur(7px);padding:0}
67
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal){width:min(1250px,calc(100% - 2 * var(--edge)));max-width:none;height:78%;max-height:880px;min-height:0;padding:24px 28px 18px;background:linear-gradient(115deg,#f1efe668,#f1efe624);border:0;border-block:1px solid #83896e70;box-shadow:none;color:var(--choice-ink)}
68
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) .modal-top{padding-bottom:10px;min-height:40px;font-size:11px;border-bottom:1px solid var(--choice-rule)}
69
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) .modal-top button{font-size:12px;gap:12px;color:#4a553c}
70
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) h2{font-size:30px;font-weight:500;letter-spacing:0;margin:20px 0 18px;gap:18px}
71
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) h2 small{font-size:11px;letter-spacing:1.7px;color:var(--choice-muted)}
72
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal) .search-field{height:48px;gap:16px;border-bottom:1px solid #6f7959}
73
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal) .search-field input{font-size:16px;background:transparent;border:0;box-shadow:none;outline:none}
74
+ .rhine-workbench [data-layout] :is(.rhine-index-modal,.rhine-rack-index-modal) .search-field:focus-within{box-shadow:0 1px #6f7959}
75
+ .rhine-workbench [data-layout] .rhine-index-modal .category-filters{gap:20px;padding:13px 0 9px}
76
+ .rhine-workbench [data-layout] .rhine-index-modal .category-filters button{min-height:32px;font-size:13px}
77
+ .rhine-workbench [data-layout] .rhine-index-modal .rhine-query-actions{font-size:12px;margin-top:10px}
78
+ .rhine-workbench [data-layout] .rhine-index-modal .rhine-search-submit{font-size:13px;min-height:38px}
79
+ .rhine-workbench [data-layout] .rhine-index-modal .result-header{display:flex;justify-content:space-between;color:var(--choice-muted);font-size:10px;padding:10px 0;border-block:1px solid var(--choice-rule)}
80
+ .rhine-workbench [data-layout] .rhine-result-summary{position:static;margin:0;padding:9px 0;font-size:11px;color:var(--choice-muted)}
81
+ .rhine-workbench [data-layout] .rhine-result-item{display:grid;grid-template-columns:minmax(0,1fr) 175px;gap:30px;padding:16px 10px 18px 0;border-bottom:1px solid var(--choice-rule)}
82
+ .rhine-workbench [data-layout] .rhine-result-open strong{font-size:20px;line-height:1.5;font-weight:500;margin-top:6px;overflow-wrap:anywhere}
83
+ .rhine-workbench [data-layout] .rhine-result-open p{font-size:13px;line-height:1.75;color:var(--choice-muted)}
84
+ .rhine-workbench [data-layout] .rhine-result-item:hover{background:#eff0e138}
85
+ .rhine-workbench [data-layout] .rhine-result-meta{font-size:11px;gap:12px;align-items:flex-start}
86
+ .rhine-workbench [data-layout] .rhine-result-meta button{font-size:12px;min-height:26px}
87
+ .rhine-workbench [data-layout] .rhine-rack-index-item{grid-template-columns:60px minmax(0,1fr) 195px;gap:18px;padding:20px 4px;border-bottom:1px solid var(--choice-rule);align-items:baseline}
88
+ .rhine-workbench [data-layout] .rhine-rack-index-item strong{font-size:19px;line-height:1.5;font-weight:500;overflow-wrap:anywhere}
89
+ .rhine-workbench [data-layout] .rhine-rack-index-item span{font-size:11px;line-height:1.6;color:var(--choice-muted)}
90
+ .rhine-workbench [data-layout] .rhine-rack-index-list{margin-top:15px}
91
+ .rhine-workbench [data-layout] .rhine-index-modal .modal-bottom{position:static;margin-top:12px;padding:12px 0 0;font-size:9px;border-top:1px solid var(--choice-rule)}
92
+
93
+ .rhine-workbench [data-layout=compact] .archive-navigation{top:22%;bottom:174px;width:34%;padding-right:10px}
94
+ .rhine-workbench [data-layout=compact] .rhine-desk-panel{top:19%;bottom:160px;width:38%;padding-right:12px}
95
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]){--choice-font:15px}
96
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-choice-copy small{font-size:10px}
97
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-choice-heading>span{font-size:9px;letter-spacing:.5px}
98
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-choice-heading button{font-size:11px}
99
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-shelf-excerpt{display:none}
100
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-desk-panel h2{font-size:24px;margin:6px 0}
101
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-shelf-title{font-size:15px;margin:7px 0 4px;-webkit-line-clamp:1}
102
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) .rhine-source-choice{gap:7px;grid-template-columns:26px minmax(0,1fr) 12px;padding-block:10px;min-height:60px}
103
+ .rhine-workbench :is([data-layout=compact],[data-layout=portrait]) :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal){height:90%;padding:12px 18px;overflow:hidden}
104
+ .rhine-workbench [data-layout=portrait] .archive-navigation{top:48%;bottom:183px;left:var(--edge);width:calc(100% - 2 * var(--edge));padding:0;background:transparent}
105
+ .rhine-workbench [data-layout=portrait] .archive-navigation .column-navigation{padding:3px 0 6px;gap:8px}
106
+ .rhine-workbench [data-layout=portrait] .archive-navigation .column-navigation>div{display:flex;align-items:baseline;justify-content:space-between}
107
+ .rhine-workbench [data-layout=portrait] .archive-navigation .column-navigation strong{font-size:18px}
108
+ .rhine-workbench [data-layout=portrait] .rhine-choice-heading{min-height:24px}
109
+ .rhine-workbench [data-layout=portrait] .rhine-choice-footer{padding:4px 0}
110
+ .rhine-workbench [data-layout=portrait] .rhine-desk-panel{left:var(--edge);top:31%;bottom:175px;width:calc(100% - 2 * var(--edge));padding:0;background:transparent}
111
+ .rhine-workbench [data-layout=portrait] .rhine-desk-panel>.rhine-rack-intro{display:none}
112
+ .rhine-workbench [data-layout=portrait] .rhine-shelf-current{padding-top:8px}
113
+ .rhine-workbench [data-layout=portrait] .rhine-desk-pagination{padding:2px 0;min-height:28px}
114
+ .rhine-workbench [data-layout=portrait] :is(.rhine-index-modal,.rhine-rack-index-modal,.rhine-basket-modal,.rhine-log-modal) h2{font-size:24px;margin:12px 0}
115
+ .rhine-workbench [data-layout=portrait] .rhine-index-modal .category-filters{gap:6px 15px;padding:8px 0}
116
+ .rhine-workbench [data-layout=portrait] .rhine-index-modal .category-filters button{font-size:12px;min-height:26px}
117
+ .rhine-workbench [data-layout=portrait] .rhine-result-item{grid-template-columns:minmax(0,1fr);gap:12px;padding:15px 0}
118
+ .rhine-workbench [data-layout=portrait] .rhine-result-meta{flex-direction:row;flex-wrap:wrap;gap:6px 14px;align-items:center}
119
+ .rhine-workbench [data-layout=portrait] .rhine-result-meta>span{width:100%}
120
+ .rhine-workbench [data-layout=portrait] .rhine-result-open strong{font-size:17px}
121
+ .rhine-workbench [data-layout=portrait] .rhine-rack-index-item{grid-template-columns:38px minmax(0,1fr);gap:8px;padding:14px 0}
122
+ .rhine-workbench [data-layout=portrait] .rhine-rack-index-item strong{font-size:16px}
123
+ .rhine-workbench [data-layout=portrait] .rhine-rack-index-item span{grid-column:2}
124
+ .rhine-workbench [data-layout=portrait] .rhine-result-summary{font-size:10px}
125
+ @media(max-height:600px){
126
+ .rhine-workbench [data-layout=compact] :is(.archive-navigation,.rhine-desk-panel){top:110px;bottom:128px}
127
+ .rhine-workbench [data-layout=compact] .rhine-desk-panel>.rhine-rack-intro{display:none}
128
+ .rhine-workbench [data-layout=compact] .rhine-shelf-meta{display:none}
129
+ .rhine-workbench [data-layout=compact] .rhine-shelf-title{-webkit-line-clamp:1;margin:5px 0}
130
+ .rhine-workbench [data-layout=compact] .rhine-shelf-current{padding-top:6px}
131
+ }
132
+ /* Leave the list more room than the already selected item's summary. */
133
+ .rhine-workbench [data-layout] .rhine-shelf-excerpt{display:none}
134
+ .rhine-workbench [data-layout] .rhine-shelf-current{padding:10px 0 8px}
135
+ .rhine-workbench [data-layout] .rhine-shelf-meta{display:none}
136
+ .rhine-workbench [data-layout] .rhine-shelf-title{margin:8px 0 0}
137
+ .rhine-workbench [data-layout] .rhine-rack-directory{padding:6px 0;margin-top:0}
138
+ .rhine-workbench [data-layout] .rhine-desk-list{gap:0}
139
+ .rhine-workbench [data-layout] .rhine-array-read{display:none}
140
+ .rhine-workbench [data-layout=portrait] .rhine-array-read{display:block}
141
+ .rhine-workbench [data-layout=portrait] .rhine-choice-heading>span{display:none}
142
+ .rhine-workbench [data-layout=portrait] .rhine-investigation-brief{top:175px;bottom:auto;max-height:calc(43% - 195px)}
143
+ .rhine-workbench [data-layout=portrait] .rhine-investigation-brief .rhine-current-source{display:none}
144
+ .rhine-workbench [data-layout=portrait] .archive-navigation{top:43%;bottom:218px}
145
+ .rhine-workbench [data-layout=portrait] .rhine-desk-panel{top:29%;bottom:218px}
146
+ .rhine-workbench [data-layout=portrait] .rhine-desk-panel>.tiny-label{font-size:10px}
147
+ .rhine-workbench [data-layout=portrait] .rhine-shelf-navigation{margin-top:0}
148
+ .rhine-workbench [data-layout=portrait] .rhine-shelf-title{font-size:14px}
149
+
150
+ /* The directory is an on-demand control, with the same unfilled HUD as the brief. */
151
+ .rhine-workbench [data-layout] .archive-navigation[hidden],
152
+ .rhine-workbench [data-layout] .rhine-array-toggle[hidden]{display:none}
153
+ .rhine-workbench [data-layout] .rhine-array-toggle{position:absolute;left:var(--edge);bottom:235px;display:flex;align-items:center;gap:12px;min-height:38px;font-size:13px;color:#414b36;background:none;border:0;border-bottom:1px solid #7c81644a;padding:6px 0}
154
+ .rhine-workbench [data-layout] .rhine-array-toggle>span{font-size:18px}
155
+ .rhine-workbench [data-layout] .rhine-array-toggle small{font-size:10px;color:#747863;font-weight:400}
156
+ .rhine-workbench [data-layout] .rhine-array-toggle:hover{color:#25371f;border-color:#78835c}
157
+ .rhine-workbench [data-layout] .rhine-array-toggle:focus-visible{outline:1px solid #78835c;outline-offset:4px}
158
+ .rhine-workbench [data-layout] .rhine-array-collapse{color:#646e54}
159
+ .rhine-workbench [data-layout] .archive-navigation[inert]{pointer-events:none}
160
+ .rhine-workbench [data-layout=compact] .rhine-array-toggle{bottom:174px;font-size:12px}
161
+ .rhine-workbench [data-layout=portrait] .rhine-array-toggle{bottom:218px;font-size:12px}
162
+ .rhine-workbench [data-layout=portrait][data-array-list=closed] .rhine-investigation-brief{max-height:calc(100% - 455px)}
163
+ .rhine-workbench [data-layout=portrait][data-array-list=closed] .rhine-investigation-brief .rhine-current-source{display:block}
164
+ @media(max-height:600px){.rhine-workbench [data-layout=compact] .rhine-array-toggle{bottom:128px}}
@@ -0,0 +1,163 @@
1
+ // TEMPORARY RHINE PROFILER: observers exist only during an explicitly started capture.
2
+ import { RenderPerformanceCapture } from './temporary-performance.ts';
3
+ import type { DiagnosticChannel } from './temporary-performance.ts';
4
+
5
+ type Entry = Record<string, unknown>;
6
+ const number = (value: unknown) => typeof value === 'number' && Number.isFinite(value) ? value : null;
7
+ const fields = (entry: Entry, keys: string[]) => Object.fromEntries(keys.map(key => [key, number(entry[key])]));
8
+ const relative = (value: unknown, origin: number) => typeof value === 'number' && value > 0 ? value - origin : null;
9
+ const allowed = (value: unknown, list: string[]) => typeof value === 'string' && list.includes(value) ? value : 'other';
10
+
11
+ /** Only authored public asset names; never export arbitrary URLs or document IDs. */
12
+ export function resourceIdentity(value: unknown) {
13
+ if (typeof value !== 'string') return null;
14
+ try {
15
+ const name = new URL(value, 'https://local.invalid').pathname.split('/').pop() || '';
16
+ return ['archive-cassette.glb', 'archive-assembly.glb', 'rhine.js', 'rhine.css', 'client.js', 'evidence-observatory.webp'].includes(name)
17
+ ? name : /^MiSans-(Light|Regular|Medium|Demibold|Semibold|SemiBold|Bold)\.woff2?$/.test(name) ? name : null;
18
+ } catch { return null; }
19
+ }
20
+ function surface(node: unknown) {
21
+ if (typeof Element === 'undefined' || !(node instanceof Element)) return 'unknown';
22
+ for (const [selector, name] of [['.rhine-perf-tools', 'profiler'], ['.rhine-reader', 'reader'],
23
+ ['.rhine-report', 'report'], ['.model-viewer', 'viewer'], ['.rhine-case', 'status'], ['canvas', 'canvas']]) {
24
+ if (node.closest(selector)) return name;
25
+ }
26
+ return 'other-ui';
27
+ }
28
+ export function resourceKind(value: unknown) {
29
+ if (typeof value !== 'string') return 'unknown';
30
+ try {
31
+ const path = new URL(value, 'https://local.invalid').pathname;
32
+ if (/\/rhine\.js$/.test(path)) return 'rhine-bundle';
33
+ if (/\/client\.js$/.test(path)) return 'host-client';
34
+ if (/\.(woff2?|ttf|otf)$/i.test(path)) return 'font';
35
+ if (/\.(glb|gltf)$/i.test(path)) return 'model';
36
+ if (/\.(png|jpg|jpeg|webp|avif|svg)$/i.test(path)) return 'image';
37
+ if (/\.css$/i.test(path)) return 'stylesheet';
38
+ if (/\.(m?js)$/i.test(path)) return 'other-script';
39
+ return 'other';
40
+ } catch { return 'unknown'; }
41
+ }
42
+ export function sanitizePerformanceEntry(type: string, entry: Entry, origin: number): Record<string, unknown> {
43
+ if (type === 'long-animation-frame') {
44
+ const scripts = Array.isArray(entry.scripts) ? entry.scripts : [];
45
+ return { ...fields(entry, ['duration', 'blockingDuration']),
46
+ renderStart: relative(entry.renderStart, origin), styleAndLayoutStart: relative(entry.styleAndLayoutStart, origin),
47
+ firstUIEventTimestamp: relative(entry.firstUIEventTimestamp, origin),
48
+ scriptsTotal: scripts.length, scriptsOmitted: Math.max(0, scripts.length - 16),
49
+ scripts: scripts.slice(0, 16).map((value: Entry) => ({
50
+ source: resourceKind(value.sourceURL), asset: resourceIdentity(value.sourceURL),
51
+ invokerType: allowed(value.invokerType, ['event-listener', 'user-callback', 'resolve-promise', 'reject-promise', 'classic-script', 'module-script']),
52
+ windowAttribution: allowed(value.windowAttribution, ['self', 'same-origin-ancestor', 'same-origin-descendant', 'same-origin', 'cross-origin-ancestor', 'cross-origin-descendant', 'cross-origin', 'other']),
53
+ executionStart: relative(value.executionStart, origin),
54
+ ...fields(value, ['duration', 'pauseDuration', 'forcedStyleAndLayoutDuration', 'sourceCharPosition']),
55
+ })) };
56
+ }
57
+ if (type === 'event') return {
58
+ event: allowed(entry.name, ['click', 'pointerdown', 'pointerup', 'mousedown', 'mouseup', 'keydown', 'keyup', 'touchstart', 'touchend', 'input', 'change']),
59
+ ...fields(entry, ['duration', 'interactionId']),
60
+ inputDelayMs: typeof entry.processingStart === 'number' && typeof entry.startTime === 'number' ? entry.processingStart - entry.startTime : null,
61
+ processingMs: typeof entry.processingStart === 'number' && typeof entry.processingEnd === 'number' ? entry.processingEnd - entry.processingStart : null,
62
+ };
63
+ if (type === 'resource') return {
64
+ resource: resourceKind(entry.name), asset: resourceIdentity(entry.name),
65
+ initiator: allowed(entry.initiatorType, ['fetch', 'xmlhttprequest', 'script', 'css', 'img', 'link', 'other']),
66
+ ...fields(entry, ['duration', 'transferSize', 'encodedBodySize', 'decodedBodySize', 'responseStatus']),
67
+ responseStart: relative(entry.responseStart, origin), responseEnd: relative(entry.responseEnd, origin),
68
+ };
69
+ if (type === 'layout-shift') return { value: number(entry.value), hadRecentInput: entry.hadRecentInput === true,
70
+ surfaces: [...new Set((Array.isArray(entry.sources) ? entry.sources : []).map((source: Entry) => surface(source.node)))] };
71
+ return { duration: number(entry.duration) };
72
+ }
73
+
74
+ export function observePerformance(capture: RenderPerformanceCapture, root: HTMLElement, state: () => unknown = () => null) {
75
+ const observers: { observer: PerformanceObserver; consume: (entries: PerformanceEntry[]) => void }[] = [];
76
+ const events = new AbortController();
77
+ const types: [string, DiagnosticChannel | 'longtask'][] = [
78
+ ['longtask', 'longtask'], ['long-animation-frame', 'longAnimationFrames'],
79
+ ['event', 'interactions'], ['resource', 'resources'], ['layout-shift', 'layoutShifts'],
80
+ ];
81
+ for (const [type, channel] of types) {
82
+ const supported = typeof PerformanceObserver !== 'undefined' && PerformanceObserver.supportedEntryTypes?.includes(type) === true;
83
+ const capability: { supported: boolean; active: boolean; reason: string } = capture.capabilities[type] = { supported, active: false, reason: supported ? '' : 'unsupported' };
84
+ if (type === 'longtask') capture.longTasksSupported = supported;
85
+ if (!supported) continue;
86
+ const consume = (entries: PerformanceEntry[]) => {
87
+ if (!entries.length) return;
88
+ const start = performance.now();
89
+ for (const entry of entries) {
90
+ if (channel === 'longtask') capture.addLongTask(entry.startTime, entry.duration);
91
+ else {
92
+ const detail = sanitizePerformanceEntry(type, entry as unknown as Entry, capture.startedAt);
93
+ if (type === 'event') {
94
+ capture.diagnostic('interactionSummary', { kind: detail.event, durationMs: detail.duration,
95
+ inputDelayMs: detail.inputDelayMs, processingMs: detail.processingMs }, entry.startTime);
96
+ // Hover/movement entries have interactionId=0 and formerly exhausted
97
+ // the raw channel. Keep real interactions and slow anonymous events.
98
+ if (!(Number(detail.interactionId) > 0 || Number(detail.duration) >= 48)) continue;
99
+ }
100
+ capture.diagnostic(channel, detail, entry.startTime);
101
+ }
102
+ }
103
+ capture.diagnostic('overhead', { kind: 'observer', entryType: type, entries: entries.length, durationMs: performance.now() - start }, start);
104
+ };
105
+ let observer: PerformanceObserver | undefined;
106
+ try {
107
+ observer = new PerformanceObserver(list => consume(list.getEntries()));
108
+ // Event Timing has a browser minimum threshold (16ms), and rounds durations.
109
+ observer.observe({ type, buffered: false, ...(type === 'event' ? { durationThreshold: 16 } : {}) } as PerformanceObserverInit);
110
+ observers.push({ observer, consume }); capability.active = true; capability.reason = '';
111
+ } catch {
112
+ observer?.disconnect(); capability.reason = 'observe-failed';
113
+ if (type === 'longtask') capture.longTasksSupported = false;
114
+ }
115
+ }
116
+ const memory = () => {
117
+ const heap = (performance as unknown as { memory?: { usedJSHeapSize: number; totalJSHeapSize: number; jsHeapSizeLimit: number } }).memory;
118
+ return heap ? { usedJSHeapSize: heap.usedJSHeapSize, totalJSHeapSize: heap.totalJSHeapSize, jsHeapSizeLimit: heap.jsHeapSizeLimit } : null;
119
+ };
120
+ capture.capabilities['js-heap'] = { supported: memory() !== null, active: memory() !== null, reason: 'Chromium estimate; may be shared/rounded; no GC attribution' };
121
+ let expected = performance.now() + 1000;
122
+ const heartbeat = (initial = false) => {
123
+ const start = performance.now();
124
+ if (!capture.running) return;
125
+ capture.diagnostic('heartbeat', {
126
+ timerDelayMs: initial ? null : Math.max(0, start - expected),
127
+ hidden: document.hidden, focused: document.hasFocus(), visibility: document.visibilityState,
128
+ viewport: { width: innerWidth, height: innerHeight, dpr: devicePixelRatio,
129
+ visualWidth: visualViewport?.width ?? null, visualHeight: visualViewport?.height ?? null, visualScale: visualViewport?.scale ?? null },
130
+ fonts: document.fonts?.status ?? null, heap: memory(), pendingGpu: capture.pendingGpu,
131
+ renderers: capture.rendererStates(), workbench: state(), loading: capture.loadingState(),
132
+ }, start);
133
+ expected = start + 1000;
134
+ capture.diagnostic('overhead', { kind: 'heartbeat', durationMs: performance.now() - start }, start);
135
+ };
136
+ heartbeat(true);
137
+ const interval = window.setInterval(heartbeat, 1000);
138
+ const lifecycle = (kind: string, detail?: unknown) => capture.mark(kind, detail);
139
+ for (const name of ['focus', 'blur']) window.addEventListener(name, () => lifecycle('window-' + name), { signal: events.signal });
140
+ for (const name of ['pagehide', 'pageshow']) window.addEventListener(name, event => {
141
+ capture.breakTimeline(name); lifecycle(name, { persisted: (event as PageTransitionEvent).persisted });
142
+ }, { signal: events.signal });
143
+ for (const name of ['freeze', 'resume']) document.addEventListener(name, () => capture.breakTimeline(name), { signal: events.signal });
144
+ window.addEventListener('resize', () => lifecycle('viewport-resize', { width: innerWidth, height: innerHeight, dpr: devicePixelRatio }), { signal: events.signal });
145
+ let lastWheel = -Infinity;
146
+ for (const name of ['pointerdown', 'wheel', 'keydown']) root.addEventListener(name, event => {
147
+ if (!capture.running) return;
148
+ if (name === 'wheel') { if (performance.now() - lastWheel < 150) return; lastWheel = performance.now(); }
149
+ const target = event.target instanceof Element ? event.target : null;
150
+ const surface = target?.closest('.rhine-perf-tools') ? 'profiler' : target?.closest('.model-viewer') ? 'viewer' : target?.tagName === 'CANVAS' ? 'scene' : 'ui';
151
+ capture.mark('input', { type: name, surface, trusted: event.isTrusted,
152
+ ...(name === 'keydown' ? { key: allowed((event as KeyboardEvent).key, ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Enter', 'Escape', 'Tab', 'Home', 'End', 'PageUp', 'PageDown', ' ']) } : {}) });
153
+ }, { signal: events.signal, capture: true, passive: true });
154
+ window.addEventListener('error', event => lifecycle('runtime-error', {
155
+ kind: event instanceof ErrorEvent ? 'script' : 'resource',
156
+ ...(event instanceof ErrorEvent ? { source: resourceKind(event.filename), line: event.lineno, column: event.colno } : {}),
157
+ }), { signal: events.signal, capture: true });
158
+ window.addEventListener('unhandledrejection', () => lifecycle('unhandled-rejection'), { signal: events.signal });
159
+ return () => {
160
+ clearInterval(interval); events.abort();
161
+ for (const { observer, consume } of observers) { consume(observer.takeRecords()); observer.disconnect(); }
162
+ };
163
+ }
@@ -0,0 +1,124 @@
1
+ // TEMPORARY RHINE PROFILER — remove this module and its marked workbench hooks after testing.
2
+ import { RenderPerformanceCapture, liveFrameSummary } from './temporary-performance';
3
+ import { observePerformance } from './temporary-performance-observers';
4
+ import './temporary-performance.css';
5
+
6
+ const ms = (n: number | null | undefined) => n == null ? '—' : n.toFixed(2);
7
+ const count = (n: number | null | undefined) => n == null ? '—' : Math.round(n).toLocaleString();
8
+ const states: Record<string, string> = { array: '阵列', rack: '档案架', detail: '抽出 / 阅读', travel: '移动到档案架', viewer: '360° 模型' };
9
+ const stateLabel = (state: string) => state.split(':').map(s => states[s] || ({ search: '检索', activity: '资料动画' }[s]) || s).join(' · ');
10
+
11
+ export function mountTemporaryPerformancePanel(root: HTMLElement, metadata: () => unknown, state: () => unknown = () => null) {
12
+ const capture = new RenderPerformanceCapture();
13
+ capture.longTasksSupported = typeof PerformanceObserver !== 'undefined' && PerformanceObserver.supportedEntryTypes?.includes('longtask') === true;
14
+ const container = document.createElement('div');
15
+ container.className = 'rhine-perf-tools';
16
+ container.innerHTML = `<button type="button" class="rhine-perf-toggle" aria-expanded="false">性能测试 · 临时</button>
17
+ <section class="rhine-perf-panel" aria-label="临时渲染性能测试" hidden>
18
+ <header><strong>性能诊断 / v3.10</strong><button type="button" data-perf="close" aria-label="收起性能面板">收起 ×</button></header>
19
+ <p>保持日常使用即可,收起面板仍采集完整日志。可先录制 120 秒静置,再单独录制操作过程。</p>
20
+ <div class="rhine-perf-controls"><label>时长 <select aria-label="性能测试时长"><option value="30">30 秒</option><option value="60" selected>60 秒</option><option value="120">120 秒</option></select></label><button type="button" data-perf="start">开始测试</button><button type="button" data-perf="stop" disabled>停止</button><button type="button" data-perf="export" disabled>导出 JSON</button></div>
21
+ <div class="rhine-perf-controls"><label><input type="checkbox" data-perf="gpu" checked> GPU 计时</label><label><input type="checkbox" data-perf="detailed" checked> 详细诊断(抽样)</label><button type="button" data-perf="mark" disabled>标记刚才的卡顿</button><span>关闭详细诊断或 GPU 计时可对照采集开销</span></div>
22
+ <div class="rhine-perf-status" role="status">尚未开始</div>
23
+ <div class="rhine-perf-current">请等模型载入后开始。</div>
24
+ <canvas width="680" height="110" class="rhine-perf-chart" aria-label="最近 120 帧的实际回调间隔曲线,横线为 16.7 与 33.3 毫秒"></canvas>
25
+ <table><caption>最近 2 秒(最多 512 帧)· 单位 ms</caption><thead><tr><th>测量项</th><th>平均</th><th>P95</th><th>P99</th><th>最高</th></tr></thead><tbody></tbody></table>
26
+ <div class="rhine-perf-resources"></div><div class="rhine-perf-gpu"></div><div class="rhine-perf-tasks"></div>
27
+ <details><summary>如何读数</summary><p>实际回调间隔越长,停顿越明显;RAF 时间戳差单独保留在日志中。P95 表示 95% 的样本不超过此值。60 帧的单帧预算是 16.7 ms。FPS 是渲染回调频率,不保证屏幕实际呈现帧数。</p><p>CPU 是场景更新和绘制提交耗时;GPU 包含阴影、透射与后处理。两者不能相加,也不代表占用率。表格只统计最近窗口,全程数据和精确累计分位数保留在导出中。日志还记录绘制阶段、场景构成、资源加载生命周期、界面更新、长动画帧、交互延迟和后台档案准备。浏览器不支持的项目会明确注明。</p><p>资源显示数量,无法读取整卡显存或 GPU 占用率。切到后台时断开帧间隔统计,恢复后继续。首次出现的卡顿会保留;可先预热,再另开一次测试。记录只导出到本机;不采集正文或输入内容。JS 堆是浏览器估值,不能据此直接判断泄漏。新测试会覆盖未导出的记录。</p></details>
28
+ </section>`;
29
+ root.append(container); // Outside the scaled stage and viewer's inert siblings.
30
+ const get = <T extends HTMLElement = HTMLElement>(selector: string) => container.querySelector<T>(selector)!;
31
+ const toggle = get<HTMLButtonElement>('.rhine-perf-toggle'), panel = get('.rhine-perf-panel');
32
+ const start = get<HTMLButtonElement>('[data-perf=start]'), stop = get<HTMLButtonElement>('[data-perf=stop]');
33
+ const download = get<HTMLButtonElement>('[data-perf=export]');
34
+ const labels = ['实际回调间隔', 'RAF 调度滞后', '帧外空档', 'CPU 场景帧', 'GPU 帧区间 / 分段合计', '逐帧采集开销'];
35
+ const tbody = get('tbody');
36
+ const cells = labels.map(label => {
37
+ const row = document.createElement('tr'), heading = document.createElement('th');
38
+ heading.textContent = label; row.append(heading);
39
+ const values = Array.from({ length: 4 }, () => document.createElement('td'));
40
+ row.append(...values); tbody.append(row); return values;
41
+ });
42
+ const textNodes = new Map<string, HTMLElement>();
43
+ const setText = (selector: string, text: string) => {
44
+ let node = textNodes.get(selector);
45
+ if (!node) { node = get(selector); textNodes.set(selector, node); }
46
+ if (node.textContent !== text) node.textContent = text;
47
+ };
48
+ const graph = get<HTMLCanvasElement>('canvas'), graphContext = graph.getContext('2d');
49
+ const detailed = get<HTMLInputElement>('[data-perf=detailed]');
50
+ const gpu = get<HTMLInputElement>('[data-perf=gpu]'), marker = get<HTMLButtonElement>('[data-perf=mark]');
51
+ let interval = 0, active = true, disconnect: (() => void) | undefined, collecting = false;
52
+ const events = new AbortController();
53
+ const finishObserver = () => {
54
+ disconnect?.(); disconnect = undefined;
55
+ if (collecting) { capture.endMetadata = metadata(); collecting = false; }
56
+ };
57
+ const paint = () => {
58
+ const paintStart = performance.now();
59
+ capture.poll();
60
+ if (!capture.running) finishObserver();
61
+ toggle.textContent = capture.running ? `● 测试中 ${Math.floor(capture.elapsedMs / 1000)}s` : '性能测试 · 临时';
62
+ if (!panel.hidden) {
63
+ const samples = capture.samples, latest = samples.at(-1);
64
+ const recentEnd = capture.running ? capture.elapsedMs : latest?.at || 0;
65
+ const summary = liveFrameSummary(samples, recentEnd);
66
+ const recentMean = summary.callbackMs.mean;
67
+ const fps = recentMean ? 1000 / recentMean : null;
68
+ start.disabled = capture.running || capture.pendingGpu > 0;
69
+ stop.disabled = !capture.running; download.disabled = !capture.startedISO || capture.running || capture.pendingGpu > 0;
70
+ gpu.disabled = capture.running || capture.pendingGpu > 0; detailed.disabled = gpu.disabled; marker.disabled = !capture.running;
71
+ start.textContent = samples.length ? '开始新测试' : '开始测试';
72
+ setText(".rhine-perf-status", `${capture.running ? !active || document.hidden ? '采样中 · 页面未激活' : '采样中' : samples.length ? '已停止' : '尚未开始'} · ${(capture.elapsedMs / 1000).toFixed(1)}s · ${samples.length} 帧${capture.pendingGpu ? ` · GPU 待回读 ${capture.pendingGpu}` : ''}`);
73
+ setText(".rhine-perf-current", latest ? `${stateLabel(latest.state)} · 最近 2 秒 ${ms(fps)} 回调/秒 · ${latest.width} × ${latest.height}${summary.truncated ? ' · 已达 512 帧窗口上限' : ''} · 加载中 ${capture.loadingState().active.length}` : '请等模型载入后开始。');
74
+ [summary.callbackMs, summary.rafLagMs, summary.outsideFrameMs, summary.cpuMs, summary.gpuMs, summary.probeMs]
75
+ .forEach((d, row) => [d.mean, d.p95, d.p99, d.max].forEach((value, col) => {
76
+ const text = ms(value); if (cells[row][col].textContent !== text) cells[row][col].textContent = text;
77
+ }));
78
+ setText(".rhine-perf-resources", latest ? `最近一帧:${count(latest.triangles)} 三角形 / ${latest.calls} 次绘制(含多遍)\n资源:${latest.geometries} 几何 / ${latest.textures} 纹理 / ${latest.programs} 着色程序` : '');
79
+ const adapter = capture.adapters.find(a => a.name === latest?.renderer);
80
+ setText(".rhine-perf-gpu", adapter ? `${adapter.software ? '软件渲染器 · 不能代表硬件表现\n' : ''}${adapter.renderer}\n${!capture.options.gpuTiming ? '本次已关闭 GPU 计时,仍记录 CPU 和浏览器诊断。' : adapter.gpuTimerSupported ? `窗口内 GPU 有效样本 ${summary.gpuMs.count}/${summary.frames};全程统计见导出` : '浏览器不支持 GPU 计时;GPU 耗时不作推算。'}` : '');
81
+ setText(".rhine-perf-tasks", `窗口内实际回调间隔 >50ms:${summary.callbackOver50ms} 次;RAF >50ms:${summary.over50ms} 次\n${capture.longTasksSupported ? `页面长任务:${capture.longTasks.length} 次,最长 ${ms(capture.longTaskMaxMs)} ms` : '浏览器不支持页面长任务观测'}\n长动画帧:${capture.capabilities['long-animation-frame']?.active ? capture.diagnostics.longAnimationFrames.length + ' 次' : '不可用'};交互记录:${capture.diagnostics.interactions.length};心跳:${capture.diagnostics.heartbeat.length}`);
82
+ if (graphContext) {
83
+ const ctx = graphContext, tail = samples.slice(-120);
84
+ const ceiling = Math.max(50, ...tail.map(s => s.callbackIntervalMs || 0));
85
+ ctx.clearRect(0, 0, graph.width, graph.height);
86
+ ctx.font = '18px sans-serif';
87
+ for (const line of [16.67, 33.34]) { const y = 108 - line / ceiling * 98; ctx.strokeStyle = '#bbc0b3'; ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(680, y); ctx.stroke(); ctx.fillStyle = '#676e5f'; if (ceiling < 90) ctx.fillText(`${line.toFixed(1)} ms`, 6, y - 2); }
88
+ if (ceiling >= 90) ctx.fillText('16.7 / 33.3 ms', 6, 100);
89
+ ctx.fillText(`${Math.ceil(ceiling)} ms`, 565, 20);
90
+ ctx.strokeStyle = '#96734d'; ctx.lineWidth = 2; ctx.beginPath();
91
+ let move = true;
92
+ tail.forEach((s, i) => { if (s.callbackIntervalMs === null) { move = true; return; } const x = i / 119 * 680, y = 108 - s.callbackIntervalMs / ceiling * 98; if (move) ctx.moveTo(x, y); else ctx.lineTo(x, y); move = false; }); ctx.stroke();
93
+ }
94
+ }
95
+ capture.diagnostic('overhead', { kind: 'panel', visible: !panel.hidden, durationMs: performance.now() - paintStart }, paintStart);
96
+ if (!capture.running && !capture.pendingGpu) { clearInterval(interval); interval = 0; }
97
+ };
98
+ const wake = () => { if (!interval) interval = window.setInterval(paint, 1000); paint(); };
99
+ const show = (visible: boolean) => { capture.mark('profiler-panel', { visible }); panel.hidden = !visible; toggle.setAttribute('aria-expanded', String(visible)); wake(); };
100
+ toggle.addEventListener('click', () => show(panel.hidden));
101
+ get('[data-perf=close]').addEventListener('click', () => { show(false); toggle.focus(); });
102
+ start.addEventListener('click', () => {
103
+ finishObserver(); capture.start(metadata(), Number(get<HTMLSelectElement>('select').value), { gpuTiming: gpu.checked, detailed: detailed.checked }); collecting = true;
104
+ disconnect = observePerformance(capture, root, () => ({ active, state: state() }));
105
+ capture.mark('profiler-panel', { visible: !panel.hidden });
106
+ wake();
107
+ });
108
+ marker.addEventListener('click', () => capture.mark('user-hitch-marker'));
109
+ stop.addEventListener('click', () => { capture.stop(); paint(); });
110
+ download.addEventListener('click', () => {
111
+ if (capture.running || capture.pendingGpu) return;
112
+ const blob = new Blob([JSON.stringify(capture.report(), null, 2)], { type: 'application/json' });
113
+ const url = URL.createObjectURL(blob), link = document.createElement('a');
114
+ link.href = url; link.download = `rhine-performance-${capture.startedISO.replace(/[:.]/g, '-')}.json`; link.click();
115
+ window.setTimeout(() => URL.revokeObjectURL(url), 1000);
116
+ });
117
+ // Local keyboard/wheel interaction must not rotate or navigate the model behind the panel.
118
+ for (const name of ['keydown', 'wheel', 'pointerdown']) container.addEventListener(name, e => e.stopPropagation());
119
+ document.addEventListener('visibilitychange', () => capture.breakTimeline(document.hidden ? 'hidden' : 'visible'), { signal: events.signal });
120
+ return { capture,
121
+ setActive(value: boolean) { if (active !== value) capture.breakTimeline(value ? 'workbench-active' : 'workbench-inactive'); active = value; },
122
+ dispose() { capture.stop('dispose'); finishObserver(); clearInterval(interval); events.abort(); capture.dispose(); container.remove(); },
123
+ };
124
+ }
@@ -0,0 +1,19 @@
1
+ /* TEMPORARY RHINE PROFILER: no blur/filter, animations, or full-screen compositing layer. */
2
+ .rhine-workbench .rhine-perf-tools{position:absolute;right:12px;bottom:10px;z-index:300;font:12px/1.5 system-ui,sans-serif;color:#262b24;text-align:left}
3
+ .rhine-workbench .rhine-perf-tools button,.rhine-workbench .rhine-perf-tools select{font:inherit;color:inherit;background:#f1f1e9;border:1px solid #b8bdad;border-radius:3px;padding:5px 8px;cursor:pointer;letter-spacing:0}
4
+ .rhine-workbench .rhine-perf-tools button:disabled{opacity:.45;cursor:default}
5
+ .rhine-workbench .rhine-perf-tools button:focus-visible,.rhine-workbench .rhine-perf-tools select:focus-visible{outline:2px solid #647749;outline-offset:2px}
6
+ .rhine-workbench .rhine-perf-toggle{float:right}
7
+ .rhine-workbench .rhine-perf-panel{position:absolute;bottom:38px;right:0;width:min(380px,calc(100vw - 24px));max-height:calc(100dvh - 76px);overflow:auto;box-sizing:border-box;background:#f4f4ed;border:1px solid #abb29f;padding:14px;border-radius:5px;box-shadow:0 3px 16px #0002}
8
+ .rhine-workbench .rhine-perf-panel header,.rhine-workbench .rhine-perf-controls{display:flex;align-items:center;justify-content:space-between;gap:6px;flex-wrap:wrap}
9
+ .rhine-workbench .rhine-perf-panel p{margin:8px 0;color:#656c60;font-size:11px}
10
+ .rhine-workbench .rhine-perf-status{margin-top:10px;color:#536440}
11
+ .rhine-workbench .rhine-perf-current{font-variant-numeric:tabular-nums;margin-top:6px}
12
+ .rhine-workbench .rhine-perf-chart{width:100%;height:55px;margin-top:10px}
13
+ .rhine-workbench .rhine-perf-panel table{width:100%;border-collapse:collapse;font-variant-numeric:tabular-nums}
14
+ .rhine-workbench .rhine-perf-panel caption{text-align:left;font-size:10px;color:#656c60}
15
+ .rhine-workbench .rhine-perf-panel th,.rhine-workbench .rhine-perf-panel td{padding:4px;text-align:right;border-bottom:1px solid #d8dccf}
16
+ .rhine-workbench .rhine-perf-panel th:first-child{text-align:left;font-weight:400}
17
+ .rhine-workbench .rhine-perf-resources,.rhine-workbench .rhine-perf-gpu,.rhine-workbench .rhine-perf-tasks{font-size:11px;margin-top:8px;white-space:pre-line;overflow-wrap:anywhere}
18
+ .rhine-workbench .rhine-perf-panel details{border-top:1px solid #d8dccf;margin-top:10px;padding-top:8px;font-size:11px}
19
+ .rhine-workbench .rhine-perf-panel summary{cursor:pointer}