matterviz 0.4.1 → 0.4.3

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 (499) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +48 -60
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/api/optimade.js +10 -7
  6. package/dist/app.css +68 -0
  7. package/dist/brillouin/BrillouinZone.svelte +90 -91
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
  9. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
  12. package/dist/brillouin/BrillouinZoneScene.svelte +46 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  14. package/dist/brillouin/compute.js +10 -16
  15. package/dist/brillouin/geometry.js +2 -2
  16. package/dist/chempot-diagram/ChemPotDiagram.svelte +48 -50
  17. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +61 -169
  18. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +559 -755
  20. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
  21. package/dist/chempot-diagram/async-compute.svelte.js +13 -14
  22. package/dist/chempot-diagram/color.d.ts +21 -1
  23. package/dist/chempot-diagram/color.js +70 -1
  24. package/dist/chempot-diagram/compute.d.ts +7 -0
  25. package/dist/chempot-diagram/compute.js +85 -26
  26. package/dist/chempot-diagram/controls-state.svelte.js +1 -1
  27. package/dist/chempot-diagram/export.js +1 -6
  28. package/dist/chempot-diagram/temperature.js +9 -10
  29. package/dist/colors/index.d.ts +0 -4
  30. package/dist/colors/index.js +19 -20
  31. package/dist/composition/BarChart.svelte +30 -26
  32. package/dist/composition/BubbleChart.svelte +17 -17
  33. package/dist/composition/Composition.svelte +10 -19
  34. package/dist/composition/Formula.svelte +20 -27
  35. package/dist/composition/FormulaFilter.svelte +115 -184
  36. package/dist/composition/PieChart.svelte +55 -41
  37. package/dist/composition/format.js +7 -9
  38. package/dist/composition/index.d.ts +2 -0
  39. package/dist/composition/index.js +14 -0
  40. package/dist/composition/parse.d.ts +2 -2
  41. package/dist/composition/parse.js +61 -115
  42. package/dist/constants.d.ts +0 -1
  43. package/dist/constants.js +17 -26
  44. package/dist/convex-hull/ConvexHull.svelte +12 -15
  45. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  46. package/dist/convex-hull/ConvexHull2D.svelte +93 -94
  47. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  48. package/dist/convex-hull/ConvexHull3D.svelte +127 -186
  49. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  50. package/dist/convex-hull/ConvexHull4D.svelte +131 -180
  51. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  52. package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
  53. package/dist/convex-hull/ConvexHullControls.svelte +46 -48
  54. package/dist/convex-hull/ConvexHullStats.svelte +193 -195
  55. package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
  56. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  57. package/dist/convex-hull/StructurePopup.svelte +18 -22
  58. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  59. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  60. package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
  61. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  62. package/dist/convex-hull/helpers.d.ts +8 -10
  63. package/dist/convex-hull/helpers.js +37 -43
  64. package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
  65. package/dist/convex-hull/thermodynamics.js +37 -44
  66. package/dist/coordination/CoordinationBarPlot.svelte +49 -53
  67. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  68. package/dist/coordination/calc-coordination.d.ts +1 -1
  69. package/dist/coordination/calc-coordination.js +28 -25
  70. package/dist/element/BohrAtom.svelte +5 -9
  71. package/dist/element/ElementHeading.svelte +4 -1
  72. package/dist/element/ElementPhoto.svelte +11 -9
  73. package/dist/element/ElementStats.svelte +12 -10
  74. package/dist/element/ElementTile.svelte +44 -67
  75. package/dist/element/Nucleus.svelte +4 -2
  76. package/dist/element/data.d.ts +3 -2
  77. package/dist/element/data.js +5 -1
  78. package/dist/element/helpers.d.ts +4 -0
  79. package/dist/element/helpers.js +18 -0
  80. package/dist/element/index.d.ts +2 -5
  81. package/dist/element/index.js +4 -19
  82. package/dist/feedback/DragOverlay.svelte +5 -5
  83. package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
  84. package/dist/feedback/StatusMessage.svelte +7 -4
  85. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  86. package/dist/fermi-surface/FermiSurface.svelte +35 -44
  87. package/dist/fermi-surface/FermiSurface.svelte.d.ts +7 -13
  88. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  89. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  90. package/dist/fermi-surface/FermiSurfaceScene.svelte +64 -136
  91. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  92. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  93. package/dist/fermi-surface/compute.js +25 -58
  94. package/dist/fermi-surface/parse.js +54 -88
  95. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  96. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  97. package/dist/file-viewer/PlotPanel.svelte +350 -0
  98. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  99. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  100. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  101. package/dist/file-viewer/detect.d.ts +11 -0
  102. package/dist/file-viewer/detect.js +381 -0
  103. package/dist/file-viewer/eligibility.d.ts +4 -0
  104. package/dist/file-viewer/eligibility.js +53 -0
  105. package/dist/file-viewer/host-protocol.d.ts +50 -0
  106. package/dist/file-viewer/host-transfer.d.ts +25 -0
  107. package/dist/file-viewer/host-transfer.js +42 -0
  108. package/dist/file-viewer/index.d.ts +5 -0
  109. package/dist/file-viewer/index.js +5 -0
  110. package/dist/file-viewer/main.d.ts +44 -0
  111. package/dist/file-viewer/main.js +535 -0
  112. package/dist/file-viewer/parse.d.ts +9 -0
  113. package/dist/file-viewer/parse.js +170 -0
  114. package/dist/file-viewer/plot-utils.d.ts +24 -0
  115. package/dist/file-viewer/plot-utils.js +191 -0
  116. package/dist/file-viewer/types.d.ts +4 -0
  117. package/dist/file-viewer/types.js +5 -0
  118. package/dist/heatmap-matrix/HeatmapMatrix.svelte +153 -219
  119. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
  120. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
  121. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  122. package/dist/heatmap-matrix/index.d.ts +5 -0
  123. package/dist/icons.d.ts +4 -0
  124. package/dist/icons.js +4 -0
  125. package/dist/index.d.ts +2 -2
  126. package/dist/index.js +1 -1
  127. package/dist/io/ExportPane.svelte +11 -7
  128. package/dist/io/decompress.d.ts +4 -1
  129. package/dist/io/decompress.js +24 -36
  130. package/dist/io/export.d.ts +11 -4
  131. package/dist/io/export.js +75 -19
  132. package/dist/io/fetch.d.ts +0 -3
  133. package/dist/io/fetch.js +0 -11
  134. package/dist/io/file-drop.js +60 -35
  135. package/dist/io/index.d.ts +0 -1
  136. package/dist/io/index.js +2 -9
  137. package/dist/io/is-binary.d.ts +9 -0
  138. package/dist/io/is-binary.js +45 -1
  139. package/dist/io/url-drop.d.ts +2 -0
  140. package/dist/io/url-drop.js +40 -40
  141. package/dist/isosurface/Isosurface.svelte +700 -215
  142. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  143. package/dist/isosurface/IsosurfaceControls.svelte +590 -116
  144. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  145. package/dist/isosurface/VolumeSlice.svelte +224 -0
  146. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  147. package/dist/isosurface/coloring.d.ts +20 -0
  148. package/dist/isosurface/coloring.js +118 -0
  149. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  150. package/dist/isosurface/geometry.worker.d.ts +1 -0
  151. package/dist/isosurface/geometry.worker.js +59 -0
  152. package/dist/isosurface/grid.d.ts +10 -0
  153. package/dist/isosurface/grid.js +31 -0
  154. package/dist/isosurface/index.d.ts +4 -0
  155. package/dist/isosurface/index.js +4 -0
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +6 -6
  159. package/dist/isosurface/profile.d.ts +12 -0
  160. package/dist/isosurface/profile.js +19 -0
  161. package/dist/isosurface/sampling.d.ts +34 -0
  162. package/dist/isosurface/sampling.js +408 -0
  163. package/dist/isosurface/slice-rendering.d.ts +13 -0
  164. package/dist/isosurface/slice-rendering.js +79 -0
  165. package/dist/isosurface/slice.d.ts +22 -2
  166. package/dist/isosurface/slice.js +200 -116
  167. package/dist/isosurface/types.d.ts +24 -1
  168. package/dist/isosurface/types.js +120 -8
  169. package/dist/json-path.d.ts +4 -0
  170. package/dist/json-path.js +89 -0
  171. package/dist/labels.d.ts +6 -2
  172. package/dist/labels.js +44 -14
  173. package/dist/layout/FullscreenButton.svelte +2 -1
  174. package/dist/layout/FullscreenToggle.svelte +10 -4
  175. package/dist/layout/InfoCard.svelte +8 -21
  176. package/dist/layout/InfoTag.svelte +1 -3
  177. package/dist/layout/NumberRangeInput.svelte +46 -0
  178. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  179. package/dist/layout/PropertyFilter.svelte +2 -6
  180. package/dist/layout/SettingsSection.svelte +57 -62
  181. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  182. package/dist/layout/SubpageGrid.svelte +3 -1
  183. package/dist/layout/ViewerChrome.svelte +16 -11
  184. package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
  185. package/dist/layout/fullscreen.js +3 -5
  186. package/dist/layout/fullscreen.svelte.js +1 -2
  187. package/dist/layout/index.d.ts +1 -0
  188. package/dist/layout/index.js +1 -0
  189. package/dist/layout/json-tree/JsonNode.svelte +22 -59
  190. package/dist/layout/json-tree/JsonTree.svelte +101 -172
  191. package/dist/layout/json-tree/JsonValue.svelte +9 -32
  192. package/dist/layout/json-tree/utils.d.ts +0 -3
  193. package/dist/layout/json-tree/utils.js +50 -188
  194. package/dist/marching-cubes.d.ts +8 -1
  195. package/dist/marching-cubes.js +115 -48
  196. package/dist/math.d.ts +5 -2
  197. package/dist/math.js +48 -38
  198. package/dist/overlays/ContextMenu.svelte +19 -34
  199. package/dist/overlays/CopyButton.svelte +11 -5
  200. package/dist/overlays/DragControlTab.svelte +6 -1
  201. package/dist/overlays/DraggablePane.svelte +110 -74
  202. package/dist/overlays/DraggablePane.svelte.d.ts +2 -1
  203. package/dist/overlays/GlassChip.svelte +32 -0
  204. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  205. package/dist/overlays/InfoPaneCards.svelte +11 -23
  206. package/dist/overlays/index.d.ts +7 -0
  207. package/dist/overlays/index.js +26 -0
  208. package/dist/overlays/portal.d.ts +2 -0
  209. package/dist/overlays/portal.js +14 -0
  210. package/dist/periodic-table/PeriodicTable.svelte +113 -145
  211. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  212. package/dist/periodic-table/PeriodicTableControls.svelte +37 -115
  213. package/dist/periodic-table/TableInset.svelte +9 -4
  214. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +68 -105
  215. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  216. package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
  217. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  218. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  219. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
  220. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  221. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  222. package/dist/phase-diagram/build-diagram.js +4 -9
  223. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  224. package/dist/phase-diagram/parse.js +15 -15
  225. package/dist/phase-diagram/svg-to-diagram.js +28 -27
  226. package/dist/phase-diagram/utils.js +2 -3
  227. package/dist/plot/bar/BarPlot.svelte +461 -426
  228. package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
  229. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  230. package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
  231. package/dist/plot/bar/SpacegroupBarPlot.svelte +46 -77
  232. package/dist/plot/bar/data.d.ts +5 -4
  233. package/dist/plot/bar/data.js +2 -1
  234. package/dist/plot/box/BoxPlot.svelte +322 -226
  235. package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
  236. package/dist/plot/box/Violin.svelte.d.ts +1 -0
  237. package/dist/plot/box/box-plot.js +1 -1
  238. package/dist/plot/core/auto-place.d.ts +1 -1
  239. package/dist/plot/core/auto-place.js +2 -2
  240. package/dist/plot/core/axis-utils.d.ts +12 -7
  241. package/dist/plot/core/axis-utils.js +48 -48
  242. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  243. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  244. package/dist/plot/core/components/ColorBar.svelte +101 -106
  245. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  246. package/dist/plot/core/components/FillArea.svelte +10 -17
  247. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  248. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  249. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  250. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  251. package/dist/plot/core/components/Line.svelte +18 -9
  252. package/dist/plot/core/components/Line.svelte.d.ts +2 -0
  253. package/dist/plot/core/components/PlotAxis.svelte +40 -17
  254. package/dist/plot/core/components/PlotControls.svelte +65 -41
  255. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  256. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  257. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  258. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  259. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  260. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  261. package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
  262. package/dist/plot/core/components/ReferencePlane.svelte +7 -8
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +27 -474
  271. package/dist/plot/core/data-transform.d.ts +1 -2
  272. package/dist/plot/core/data-transform.js +9 -15
  273. package/dist/plot/core/fill-utils.d.ts +3 -1
  274. package/dist/plot/core/fill-utils.js +28 -6
  275. package/dist/plot/core/index.d.ts +1 -0
  276. package/dist/plot/core/index.js +1 -0
  277. package/dist/plot/core/interactions.js +0 -2
  278. package/dist/plot/core/layout.d.ts +15 -6
  279. package/dist/plot/core/layout.js +57 -38
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  283. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  284. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -21
  286. package/dist/plot/core/reference-line.js +1 -1
  287. package/dist/plot/core/scales.d.ts +23 -0
  288. package/dist/plot/core/scales.js +39 -13
  289. package/dist/plot/core/svg.js +0 -1
  290. package/dist/plot/core/types/fills.d.ts +101 -0
  291. package/dist/plot/core/types/fills.js +15 -0
  292. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  293. package/dist/plot/core/types/plot-3d.js +4 -0
  294. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  295. package/dist/plot/core/types/reference-lines.js +9 -0
  296. package/dist/plot/core/types.d.ts +29 -324
  297. package/dist/plot/core/types.js +23 -20
  298. package/dist/plot/core/utils/hierarchy-chart.d.ts +52 -0
  299. package/dist/plot/core/utils/hierarchy-chart.js +191 -0
  300. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  301. package/dist/plot/core/utils/hierarchy-labels.js +100 -0
  302. package/dist/plot/core/utils/label-placement.js +2 -2
  303. package/dist/plot/histogram/Histogram.svelte +314 -340
  304. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  305. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  306. package/dist/plot/histogram/histogram.d.ts +31 -0
  307. package/dist/plot/histogram/histogram.js +70 -0
  308. package/dist/plot/histogram/index.d.ts +1 -0
  309. package/dist/plot/histogram/index.js +1 -0
  310. package/dist/plot/index.d.ts +1 -0
  311. package/dist/plot/index.js +1 -0
  312. package/dist/plot/sankey/Sankey.svelte +78 -69
  313. package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
  314. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  315. package/dist/plot/sankey/sankey.js +13 -9
  316. package/dist/plot/scatter/BinnedScatterPlot.svelte +311 -141
  317. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
  318. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  319. package/dist/plot/scatter/ScatterPlot.svelte +517 -410
  320. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
  321. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  322. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  323. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  324. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  325. package/dist/plot/scatter/adaptive-density.js +36 -20
  326. package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
  327. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  328. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  329. package/dist/plot/scatter/scatter-data.js +8 -7
  330. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
  331. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
  332. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
  333. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
  334. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
  335. package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
  336. package/dist/plot/sunburst/Sunburst.svelte +269 -347
  337. package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
  338. package/dist/plot/sunburst/index.d.ts +1 -1
  339. package/dist/plot/sunburst/index.js +2 -1
  340. package/dist/plot/sunburst/render.d.ts +2 -2
  341. package/dist/plot/sunburst/render.js +47 -22
  342. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  343. package/dist/plot/sunburst/sunburst.js +10 -3
  344. package/dist/plot/treemap/Treemap.svelte +1022 -0
  345. package/dist/plot/treemap/Treemap.svelte.d.ts +90 -0
  346. package/dist/plot/treemap/index.d.ts +4 -0
  347. package/dist/plot/treemap/index.js +4 -0
  348. package/dist/plot/treemap/labels.d.ts +38 -0
  349. package/dist/plot/treemap/labels.js +80 -0
  350. package/dist/plot/treemap/treemap.d.ts +28 -0
  351. package/dist/plot/treemap/treemap.js +102 -0
  352. package/dist/rdf/RdfPlot.svelte +37 -59
  353. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  354. package/dist/rdf/calc-rdf.js +63 -52
  355. package/dist/rdf/index.d.ts +0 -1
  356. package/dist/sanitize.js +85 -27
  357. package/dist/scene/SceneCamera.svelte +1 -1
  358. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  359. package/dist/scene/bind-renderer.svelte.js +14 -6
  360. package/dist/scene/index.d.ts +1 -2
  361. package/dist/scene/index.js +1 -1
  362. package/dist/scene/{props.js → props.svelte.js} +15 -1
  363. package/dist/settings.d.ts +19 -9
  364. package/dist/settings.js +229 -525
  365. package/dist/spectral/Bands.svelte +207 -221
  366. package/dist/spectral/BandsAndDos.svelte +11 -9
  367. package/dist/spectral/BrillouinBandsDos.svelte +19 -42
  368. package/dist/spectral/Dos.svelte +92 -93
  369. package/dist/spectral/helpers.d.ts +3 -3
  370. package/dist/spectral/helpers.js +65 -140
  371. package/dist/state.svelte.js +5 -18
  372. package/dist/structure/Arrow.svelte +5 -18
  373. package/dist/structure/ArrowInstances.svelte +160 -0
  374. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  375. package/dist/structure/AtomLegend.svelte +120 -85
  376. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  377. package/dist/structure/Bond.svelte +31 -23
  378. package/dist/structure/CanvasTooltip.svelte +15 -3
  379. package/dist/structure/CellSelect.svelte +45 -35
  380. package/dist/structure/Cylinder.svelte +24 -40
  381. package/dist/structure/InstancedAtoms.svelte +114 -0
  382. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  383. package/dist/structure/Lattice.svelte +85 -149
  384. package/dist/structure/SiteLabels.svelte +122 -0
  385. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  386. package/dist/structure/Structure.svelte +636 -504
  387. package/dist/structure/Structure.svelte.d.ts +4 -2
  388. package/dist/structure/StructureCarousel.svelte +637 -0
  389. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  390. package/dist/structure/StructureControls.svelte +165 -425
  391. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  392. package/dist/structure/StructureExportPane.svelte +17 -21
  393. package/dist/structure/StructureInfoPane.svelte +128 -133
  394. package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
  395. package/dist/structure/StructureScene.svelte +535 -578
  396. package/dist/structure/StructureScene.svelte.d.ts +6 -2
  397. package/dist/structure/StructureViewport.svelte +451 -0
  398. package/dist/structure/StructureViewport.svelte.d.ts +65 -0
  399. package/dist/structure/atom-label-placement.d.ts +16 -12
  400. package/dist/structure/atom-label-placement.js +70 -37
  401. package/dist/structure/atom-properties.d.ts +2 -0
  402. package/dist/structure/atom-properties.js +105 -38
  403. package/dist/structure/bond-order-perception.js +3 -8
  404. package/dist/structure/bonding.d.ts +9 -3
  405. package/dist/structure/bonding.js +95 -46
  406. package/dist/structure/edit-history.d.ts +6 -0
  407. package/dist/structure/edit-history.js +27 -0
  408. package/dist/structure/export.d.ts +1 -0
  409. package/dist/structure/export.js +89 -105
  410. package/dist/structure/format-detect.d.ts +1 -0
  411. package/dist/structure/format-detect.js +20 -0
  412. package/dist/structure/geometry.d.ts +9 -0
  413. package/dist/structure/geometry.js +27 -0
  414. package/dist/structure/index.d.ts +16 -1
  415. package/dist/structure/index.js +15 -10
  416. package/dist/structure/measure.d.ts +1 -1
  417. package/dist/structure/measure.js +6 -12
  418. package/dist/structure/parse.d.ts +2 -1
  419. package/dist/structure/parse.js +272 -313
  420. package/dist/structure/partial-occupancy.js +10 -5
  421. package/dist/structure/pbc.js +85 -107
  422. package/dist/structure/polyhedra.d.ts +0 -1
  423. package/dist/structure/polyhedra.js +7 -21
  424. package/dist/structure/prop-groups.d.ts +85 -0
  425. package/dist/structure/prop-groups.js +18 -0
  426. package/dist/structure/supercell.d.ts +1 -2
  427. package/dist/structure/supercell.js +7 -13
  428. package/dist/symmetry/SymmetryElements.svelte +11 -15
  429. package/dist/symmetry/SymmetryStats.svelte +55 -71
  430. package/dist/symmetry/WyckoffTable.svelte +10 -28
  431. package/dist/symmetry/cell-transform.js +6 -14
  432. package/dist/symmetry/index.d.ts +1 -0
  433. package/dist/symmetry/index.js +13 -10
  434. package/dist/symmetry/spacegroups.js +3 -2
  435. package/dist/table/HeatmapTable.svelte +1142 -447
  436. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  437. package/dist/table/ToggleMenu.svelte +168 -53
  438. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  439. package/dist/table/index.d.ts +21 -6
  440. package/dist/table/index.js +34 -21
  441. package/dist/theme/ThemeControl.svelte +16 -8
  442. package/dist/theme/embedded.d.ts +18 -0
  443. package/dist/theme/embedded.js +206 -0
  444. package/dist/theme/index.d.ts +5 -5
  445. package/dist/theme/index.js +5 -12
  446. package/dist/theme/themes.mjs +44 -66
  447. package/dist/tooltip/KCoords.svelte +4 -1
  448. package/dist/tooltip/TooltipContent.svelte +3 -6
  449. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  450. package/dist/tooltip/index.d.ts +8 -1
  451. package/dist/trajectory/Trajectory.svelte +613 -386
  452. package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
  453. package/dist/trajectory/TrajectoryError.svelte +12 -74
  454. package/dist/trajectory/TrajectoryExportPane.svelte +14 -32
  455. package/dist/trajectory/TrajectoryInfoPane.svelte +127 -164
  456. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  457. package/dist/trajectory/extract.js +61 -49
  458. package/dist/trajectory/format-detect.d.ts +2 -0
  459. package/dist/trajectory/format-detect.js +17 -13
  460. package/dist/trajectory/frame-reader.d.ts +2 -0
  461. package/dist/trajectory/frame-reader.js +35 -32
  462. package/dist/trajectory/helpers.d.ts +2 -0
  463. package/dist/trajectory/helpers.js +34 -56
  464. package/dist/trajectory/index.d.ts +1 -0
  465. package/dist/trajectory/index.js +5 -8
  466. package/dist/trajectory/parse/ase.js +1 -1
  467. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  468. package/dist/trajectory/parse/h5-utils.js +109 -0
  469. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  470. package/dist/trajectory/parse/hdf5.js +116 -106
  471. package/dist/trajectory/parse/index.d.ts +6 -6
  472. package/dist/trajectory/parse/index.js +25 -33
  473. package/dist/trajectory/parse/lammps.js +8 -12
  474. package/dist/trajectory/parse/vasp.js +10 -7
  475. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  476. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  477. package/dist/trajectory/parse/vaspout-h5.d.ts +4 -0
  478. package/dist/trajectory/parse/vaspout-h5.js +281 -0
  479. package/dist/trajectory/parse/xyz.js +59 -9
  480. package/dist/trajectory/plotting.d.ts +8 -9
  481. package/dist/trajectory/plotting.js +113 -64
  482. package/dist/utils.d.ts +3 -0
  483. package/dist/utils.js +7 -0
  484. package/dist/xrd/XrdPlot.svelte +79 -118
  485. package/dist/xrd/broadening.js +2 -1
  486. package/dist/xrd/calc-xrd.js +24 -62
  487. package/dist/xrd/parse.d.ts +0 -1
  488. package/dist/xrd/parse.js +69 -95
  489. package/package.json +94 -34
  490. package/readme.md +6 -5
  491. package/dist/api/mp.d.ts +0 -6
  492. package/dist/api/mp.js +0 -22
  493. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  494. package/dist/scene/types.d.ts +0 -26
  495. package/dist/tooltip/types.d.ts +0 -8
  496. package/dist/trajectory/constants.d.ts +0 -6
  497. package/dist/trajectory/constants.js +0 -11
  498. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  499. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -1,10 +1,16 @@
1
1
  <script lang="ts">
2
- // Threlte component that renders isosurface meshes from volumetric data using marching cubes.
3
- // Supports multiple layers at different isovalues with independent colors,
4
- // plus positive/negative lobes and two-pass transparency.
5
- import { marching_cubes } from '../marching-cubes'
2
+ // Threlte component that renders isosurface meshes from one or more volumetric
3
+ // datasets using marching cubes. Each layer picks a geometry-source volume and
4
+ // optionally a different color-source volume whose scalar field is sampled at
5
+ // surface vertices and mapped through a colormap (e.g. density colored by ESP).
6
+ // Geometry is cached per layer so colormap/color-range changes never rerun
7
+ // marching cubes, and sampled scalars are cached so they only remap through the
8
+ // colormap LUT.
9
+ import { marching_cubes_buffers } from '../marching-cubes'
6
10
  import type { Matrix3x3, Vec3 } from '../math'
7
11
  import { T } from '@threlte/core'
12
+ import { untrack } from 'svelte'
13
+ import { SvelteSet } from 'svelte/reactivity'
8
14
  import {
9
15
  BackSide,
10
16
  BufferAttribute,
@@ -13,58 +19,173 @@
13
19
  FrontSide,
14
20
  Uint32BufferAttribute,
15
21
  } from 'three'
16
- import type { IsosurfaceLayer, IsosurfaceSettings, VolumetricData } from './types'
17
22
  import {
18
- DEFAULT_ISOSURFACE_SETTINGS,
19
- downsample_grid,
20
- pad_periodic_grid,
21
- } from './types'
23
+ compute_scalar_range,
24
+ DEFAULT_ISO_COLORMAP,
25
+ is_signed_range,
26
+ scalars_to_vertex_colors,
27
+ } from './coloring'
28
+ import type {
29
+ GeometryWorkerRequest,
30
+ GeometryWorkerResponse,
31
+ TransferableVolume,
32
+ } from './geometry-worker-types'
33
+ import { flatten_grid, grid_point_count, inflate_grid } from './grid'
34
+ import { profile_stage, record_profile, type IsosurfaceProfiler } from './profile'
35
+ import type { DisplayRange } from './sampling'
36
+ import {
37
+ prepare_geometry_grid,
38
+ resolve_volume_display_range,
39
+ sample_volume_at_positions,
40
+ } from './sampling'
41
+ import type { IsosurfaceLayer, IsosurfaceSettings, VolumetricData } from './types'
42
+ import { DEFAULT_ISOSURFACE_SETTINGS, MAX_GRID_POINTS } from './types'
22
43
 
23
- let { volume, settings = DEFAULT_ISOSURFACE_SETTINGS }: {
24
- volume: VolumetricData
44
+ let {
45
+ volumes = [],
46
+ volume = undefined,
47
+ settings = DEFAULT_ISOSURFACE_SETTINGS,
48
+ active_volume_idx = 0,
49
+ tiling = [1, 1, 1],
50
+ profiler = undefined,
51
+ }: {
52
+ volumes?: VolumetricData[]
53
+ // Deprecated single-volume alias (kept for backwards compatibility)
54
+ volume?: VolumetricData
25
55
  settings?: IsosurfaceSettings
56
+ // Volume that implicit single-isovalue settings apply to when no explicit
57
+ // layers are set (also the fallback for layers without volume_idx)
58
+ active_volume_idx?: number
59
+ // Supercell tiling applied to geometry volumes (color sampling always uses
60
+ // the original volume with periodic wrapping for full fidelity)
61
+ tiling?: Vec3
62
+ // Internal benchmark hook. Deliberately not re-exported from the package API.
63
+ profiler?: IsosurfaceProfiler
26
64
  } = $props()
27
65
 
28
- // Resolve layers: use explicit layers array if provided, else build from single-isovalue settings
29
- let resolved_layers = $derived.by((): IsosurfaceLayer[] => {
30
- if (settings.layers?.length) return settings.layers
31
- return [{
32
- isovalue: settings.isovalue,
33
- color: settings.positive_color,
34
- opacity: settings.opacity,
35
- visible: true,
36
- show_negative: settings.show_negative,
37
- negative_color: settings.negative_color,
38
- }]
66
+ let all_volumes = $derived(volumes.length ? volumes : volume ? [volume] : [])
67
+ let reference_origin = $derived<Vec3>(all_volumes[0]?.origin ?? [0, 0, 0])
68
+ let reference_origin_key = $derived(reference_origin.join(`,`))
69
+
70
+ type ResolvedLayer = IsosurfaceLayer & { volume_idx: number }
71
+
72
+ // Resolve layers: explicit layers array if provided (an empty array means zero
73
+ // surfaces), else one implicit layer from single-isovalue settings bound to
74
+ // the active volume. Layers with an explicitly out-of-range volume_idx are
75
+ // skipped rather than clamped — silently rendering a different volume's data
76
+ // would be scientifically wrong.
77
+ let resolved_layers = $derived.by((): ResolvedLayer[] => {
78
+ const n_vols = all_volumes.length
79
+ if (n_vols === 0) return []
80
+ const clamp_idx = (idx: number) => Math.min(Math.max(idx, 0), n_vols - 1)
81
+ if (settings.layers) {
82
+ return settings.layers
83
+ .filter(
84
+ (layer) =>
85
+ layer.volume_idx === undefined ||
86
+ (layer.volume_idx >= 0 && layer.volume_idx < n_vols),
87
+ )
88
+ .map((layer) => ({
89
+ ...layer,
90
+ volume_idx: layer.volume_idx ?? clamp_idx(active_volume_idx),
91
+ }))
92
+ }
93
+ return [
94
+ {
95
+ isovalue: settings.isovalue,
96
+ color: settings.positive_color,
97
+ opacity: settings.opacity,
98
+ visible: true,
99
+ show_negative: settings.show_negative,
100
+ negative_color: settings.negative_color,
101
+ volume_idx: clamp_idx(active_volume_idx),
102
+ },
103
+ ]
39
104
  })
40
105
 
41
- // Build indexed BufferGeometry from marching cubes output.
42
- // Uses Three.js index buffer to avoid tripling vertex data, and
43
- // computeVertexNormals() for fast GPU-friendly normals.
44
- function build_geometry(
45
- vertices: Vec3[],
46
- faces: number[][],
47
- ): BufferGeometry | null {
48
- if (vertices.length === 0 || faces.length === 0) return null
49
-
50
- // Flatten vertices: Vec3[] → Float32Array
51
- const positions = new Float32Array(vertices.length * 3)
52
- for (let idx = 0; idx < vertices.length; idx++) {
53
- const vert = vertices[idx]
54
- positions[idx * 3] = vert[0]
55
- positions[idx * 3 + 1] = vert[1]
56
- positions[idx * 3 + 2] = vert[2]
106
+ // Stable identity tokens for volume objects so cache keys detect replacement
107
+ let vol_id_counter = 0
108
+ const vol_ids = new WeakMap<VolumetricData, number>()
109
+ const vol_id = (vol: VolumetricData): number => {
110
+ let id = vol_ids.get(vol)
111
+ if (id === undefined) {
112
+ id = ++vol_id_counter
113
+ vol_ids.set(vol, id)
114
+ }
115
+ return id
116
+ }
117
+
118
+ // Finite extraction range for this volume. Periodic volumes always use one
119
+ // so integer tiling and fractional VESTA-style bounds share exact endpoint
120
+ // semantics; finite volumes only use an explicitly requested crop (the
121
+ // resolver clamps negative halo and only applies it to periodic volumes).
122
+ const effective_range = (vol: VolumetricData): DisplayRange | null =>
123
+ resolve_volume_display_range(vol, {
124
+ display_range: settings.display_range,
125
+ tiling,
126
+ halo: settings.halo,
127
+ })
128
+
129
+ // === Geometry-volume preparation (range extraction or finite-grid downsampling) ===
130
+ // The range fully determines the prepared grid for a given volume: it encodes
131
+ // tiling and halo for periodic volumes and is null/explicit-crop for finite
132
+ // ones (which ignore tiling and halo entirely).
133
+ type PreparedGrid = {
134
+ key: string
135
+ grid: number[][][]
136
+ lattice: Matrix3x3
137
+ // Cartesian shift applied to marching-cubes vertices: range offset plus this
138
+ // volume's origin delta relative to the scene reference (first volume's origin)
139
+ vertex_shift: Vec3
140
+ }
141
+ // Keyed by volume object so removed/replaced volumes release their prepared
142
+ // grids to GC automatically and index shifts never serve stale grids
143
+ const prepared_cache = new WeakMap<VolumetricData, PreparedGrid>()
144
+
145
+ const range_key = (vol: VolumetricData): string =>
146
+ effective_range(vol)?.flat().join(`,`) ?? ``
147
+
148
+ const prepared_key = (vol: VolumetricData): string =>
149
+ `${range_key(vol)}|${reference_origin_key}`
150
+ const vertex_shift = (origin: Vec3): Vec3 =>
151
+ origin.map((value, axis) => value - reference_origin[axis]) as Vec3
152
+
153
+ const current_prepared = (vol: VolumetricData): PreparedGrid | undefined => {
154
+ const cached = prepared_cache.get(vol)
155
+ return cached?.key === prepared_key(vol) ? cached : undefined
156
+ }
157
+
158
+ function prepare_geometry_volume(vol: VolumetricData): PreparedGrid {
159
+ const cached = current_prepared(vol)
160
+ if (cached) {
161
+ record_profile(profiler, `prepare_geometry`, 0, { cache_hit: true })
162
+ return cached
57
163
  }
164
+ const profile_start = profiler ? performance.now() : 0
58
165
 
59
- // Flatten face indices: number[][] → Uint32Array
60
- const indices = new Uint32Array(faces.length * 3)
61
- for (let idx = 0; idx < faces.length; idx++) {
62
- const face = faces[idx]
63
- indices[idx * 3] = face[0]
64
- indices[idx * 3 + 1] = face[1]
65
- indices[idx * 3 + 2] = face[2]
166
+ const range = effective_range(vol)
167
+ const { grid, lattice, origin } = prepare_geometry_grid(vol, range)
168
+
169
+ const prepared: PreparedGrid = {
170
+ key: prepared_key(vol),
171
+ grid,
172
+ lattice,
173
+ vertex_shift: vertex_shift(origin),
66
174
  }
175
+ prepared_cache.set(vol, prepared)
176
+ record_profile(profiler, `prepare_geometry`, performance.now() - profile_start, {
177
+ output_points: grid_point_count(grid),
178
+ extracted_range: range !== null,
179
+ })
180
+ return prepared
181
+ }
67
182
 
183
+ // Build indexed BufferGeometry from marching cubes output
184
+ function build_geometry(
185
+ positions: Float32Array,
186
+ indices: Uint32Array,
187
+ ): BufferGeometry | null {
188
+ if (positions.length === 0 || indices.length === 0) return null
68
189
  const geometry = new BufferGeometry()
69
190
  geometry.setAttribute(`position`, new BufferAttribute(positions, 3))
70
191
  geometry.setIndex(new Uint32BufferAttribute(indices, 1))
@@ -72,214 +193,578 @@
72
193
  geometry.computeBoundingSphere()
73
194
  return geometry
74
195
  }
196
+ const geometry_buffer_bytes = (geometry: BufferGeometry): number =>
197
+ [`position`, `normal`, `color`].reduce(
198
+ (total, name) => total + (geometry.getAttribute(name)?.array.byteLength ?? 0),
199
+ geometry.getIndex()?.array.byteLength ?? 0,
200
+ )
75
201
 
76
- // Downsample large grids once when volume changes to keep marching cubes interactive
77
- let ds_result = $derived.by(() => {
78
- if (!volume) return undefined
79
- return downsample_grid(volume.grid, volume.grid_dims)
80
- })
81
-
82
- // Run marching cubes at the given isovalue with pre-prepared grid/lattice/shift.
83
- function extract_surface(
84
- isovalue: number,
85
- mc_grid: number[][][],
86
- mc_lattice: Matrix3x3,
87
- origin_shift: Vec3 | null,
88
- ): BufferGeometry | null {
202
+ function extract_surface(isovalue: number, prepared: PreparedGrid): BufferGeometry | null {
89
203
  if (isovalue === 0) return null
90
-
91
- const result = marching_cubes(mc_grid, isovalue, mc_lattice, {
92
- periodic: false,
93
- interpolate: true,
94
- centered: false,
95
- normals: false,
96
- })
97
-
98
- if (origin_shift) {
99
- for (const vert of result.vertices) {
100
- vert[0] += origin_shift[0]
101
- vert[1] += origin_shift[1]
102
- vert[2] += origin_shift[2]
103
- }
104
- }
105
-
106
- return build_geometry(result.vertices, result.faces)
204
+ const result = profile_stage(profiler, `marching_cubes`, () =>
205
+ marching_cubes_buffers(prepared.grid, isovalue, prepared.lattice, {
206
+ periodic: false,
207
+ interpolate: true,
208
+ centered: false,
209
+ normals: false,
210
+ position_offset: prepared.vertex_shift,
211
+ }),
212
+ )
213
+ return profile_stage(
214
+ profiler,
215
+ `build_geometry`,
216
+ () => build_geometry(result.positions, result.indices),
217
+ (geometry) => ({ buffer_bytes: geometry ? geometry_buffer_bytes(geometry) : 0 }),
218
+ )
107
219
  }
108
220
 
109
- // === Multi-layer geometry management ===
110
- // Each layer produces up to 2 geometries (positive + optional negative lobe).
111
- // Keyed by "layer_idx:sign" for cache invalidation.
112
- type GeoEntry = {
221
+ // === Mesh entry management ===
222
+ // Each visible layer produces up to 2 entries (positive + optional negative lobe).
223
+ type MeshEntry = {
224
+ key: string // unique per (layer, sign) — template/each + colored_keys key
225
+ geo_key: string // geometry signature — cache/reuse key
226
+ layer_idx: number
227
+ sign: 1 | -1
113
228
  geometry: BufferGeometry
114
- color: string
115
- opacity: number
116
229
  render_order: number
230
+ // Cached per-vertex scalars from the color-source volume, so colormap and
231
+ // color-range changes only remap through the LUT without resampling
232
+ scalars: Float32Array | null
233
+ scalars_volume_id: number | null
117
234
  }
118
- let active_geometries = $state<GeoEntry[]>([])
235
+ // $state.raw: entries hold Three.js objects that must not be proxied; the array
236
+ // is replaced wholesale on rebuild so the template stays reactive.
237
+ let active_entries = $state.raw<MeshEntry[]>([])
238
+ // keys of entries that currently carry a vertex-color attribute (drives
239
+ // vertexColors on materials reactively without proxying entries)
240
+ const colored_keys = new SvelteSet<string>()
119
241
  let raf_id = 0
120
242
  let debounce_id = 0
243
+ let rebuild_generation = 0
244
+ let geometry_worker: Worker | undefined
245
+ let reject_worker_job: ((error: Error) => void) | undefined
246
+
247
+ function cancel_geometry_worker(): void {
248
+ reject_worker_job?.(new Error(`Isosurface geometry job superseded`))
249
+ reject_worker_job = undefined
250
+ geometry_worker?.terminate()
251
+ geometry_worker = undefined
252
+ }
121
253
 
122
- // Dispose all current geometries
123
254
  function dispose_all() {
124
- for (const entry of active_geometries) entry.geometry.dispose()
125
- active_geometries = []
255
+ for (const entry of active_entries) entry.geometry.dispose()
256
+ active_entries = []
257
+ colored_keys.clear()
126
258
  }
127
259
 
128
- // Dispose on unmount
129
- $effect(() => () => dispose_all())
130
-
131
- function rebuild_geometries(layers: IsosurfaceLayer[]) {
132
- if (!volume || !ds_result) return
133
- const old = active_geometries
134
- const entries: GeoEntry[] = []
135
-
136
- // Prepare grid/lattice/shift once for all layers.
137
- // When halo > 0 for periodic volumes, the downsampled grid is padded with
138
- // halo cells from the opposite face so isosurfaces extend beyond the unit
139
- // cell and close into complete enclosed shapes around boundary atoms.
140
- let mc_grid = ds_result.grid
141
- let mc_lattice: Matrix3x3 = volume.lattice
142
- let origin_shift: Vec3 | null = null
143
-
144
- if (settings.halo > 0 && volume.periodic) {
145
- const padded = pad_periodic_grid(ds_result.grid, ds_result.dims, settings.halo)
146
- mc_grid = padded.grid
147
- // marching_cubes maps [0,1] fractional -> Cartesian via lattice.
148
- // The padded grid covers a wider fractional range, so scale the lattice
149
- // to match. Then shift all vertices by the fractional offset.
150
- const [la, lb, lc] = volume.lattice
151
- const sx = padded.dims[0] / ds_result.dims[0]
152
- const sy = padded.dims[1] / ds_result.dims[1]
153
- const sz = padded.dims[2] / ds_result.dims[2]
154
- mc_lattice = [
155
- [la[0] * sx, la[1] * sx, la[2] * sx],
156
- [lb[0] * sy, lb[1] * sy, lb[2] * sy],
157
- [lc[0] * sz, lc[1] * sz, lc[2] * sz],
158
- ]
159
- const [ox, oy, oz] = padded.offset
160
- origin_shift = [
161
- ox * la[0] + oy * lb[0] + oz * lc[0],
162
- ox * la[1] + oy * lb[1] + oz * lc[1],
163
- ox * la[2] + oy * lb[2] + oz * lc[2],
164
- ]
165
- }
260
+ // Dispose geometries and cancel pending rebuilds on unmount
261
+ $effect(() => () => {
262
+ rebuild_generation++
263
+ clearTimeout(debounce_id)
264
+ cancelAnimationFrame(raf_id)
265
+ cancel_geometry_worker()
266
+ dispose_all()
267
+ })
268
+
269
+ // range_key covers halo + tiling (encoded in the range for periodic volumes;
270
+ // irrelevant for finite ones), so the geometry identity needs no other inputs
271
+ const geometry_key = (layer: ResolvedLayer, sign: 1 | -1): string => {
272
+ const vol = all_volumes[layer.volume_idx]
273
+ return JSON.stringify([
274
+ layer.volume_idx,
275
+ vol ? vol_id(vol) : 0,
276
+ sign * layer.isovalue,
277
+ vol ? range_key(vol) : ``,
278
+ reference_origin_key,
279
+ ])
280
+ }
281
+
282
+ type PendingSurface = {
283
+ key: string
284
+ geo_key: string
285
+ layer_idx: number
286
+ sign: 1 | -1
287
+ isovalue: number
288
+ volume: VolumetricData
289
+ }
290
+
291
+ type EntryPlan = MeshEntry | PendingSurface
166
292
 
167
- const surface_at = (isovalue: number) =>
168
- extract_surface(isovalue, mc_grid, mc_lattice, origin_shift)
293
+ const estimated_prepared_points = (vol: VolumetricData): number => {
294
+ const range = effective_range(vol)
295
+ if (!range) return Math.min(grid_point_count(vol.grid), MAX_GRID_POINTS)
296
+ const estimate = range.reduce((total, [lower, upper], axis) => {
297
+ const intervals = vol.periodic
298
+ ? vol.grid_dims[axis]
299
+ : Math.max(vol.grid_dims[axis] - 1, 1)
300
+ return total * Math.max(2, Math.round((upper - lower) * intervals) + 1)
301
+ }, 1)
302
+ return Math.min(estimate, MAX_GRID_POINTS)
303
+ }
169
304
 
170
- // Render lower-isovalue (outer) shells earlier so per-layer back/front passes
171
- // interleave back-to-front across shells and reduce transparency artefacts.
172
- const layer_render_rank = new Map<number, number>(
173
- layers
174
- .map((layer, layer_idx) => ({ layer_idx, isovalue: layer.isovalue }))
175
- .sort((layer_a, layer_b) => layer_a.isovalue - layer_b.isovalue)
176
- .map(({ layer_idx }, rank) => [layer_idx, rank]),
305
+ const use_geometry_worker = (pending: PendingSurface[]): boolean =>
306
+ typeof window !== `undefined` &&
307
+ typeof Worker !== `undefined` &&
308
+ pending.some(
309
+ (surface) =>
310
+ !current_prepared(surface.volume) &&
311
+ estimated_prepared_points(surface.volume) >= 200_000,
177
312
  )
178
313
 
314
+ function request_worker_geometries(
315
+ pending: PendingSurface[],
316
+ ): Promise<GeometryWorkerResponse> {
317
+ const grouped = new Map<VolumetricData, PendingSurface[]>()
318
+ for (const surface of pending) {
319
+ const surfaces = grouped.get(surface.volume) ?? []
320
+ surfaces.push(surface)
321
+ grouped.set(surface.volume, surfaces)
322
+ }
323
+ const transfer: Transferable[] = []
324
+ const request: GeometryWorkerRequest = {
325
+ volumes: [...grouped].map(([vol, surfaces]) => {
326
+ const serialize_start = profiler ? performance.now() : 0
327
+ const transferred_volume: TransferableVolume = {
328
+ grid_values: flatten_grid(vol.grid).values,
329
+ grid_dims: [...vol.grid_dims],
330
+ lattice: vol.lattice.map((row) => [...row]) as Matrix3x3,
331
+ origin: [...vol.origin],
332
+ periodic: vol.periodic,
333
+ }
334
+ transfer.push(transferred_volume.grid_values.buffer)
335
+ record_profile(profiler, `prepare_geometry`, performance.now() - serialize_start, {
336
+ worker: true,
337
+ serialize: true,
338
+ source_points: transferred_volume.grid_values.length,
339
+ })
340
+ return {
341
+ token: vol_id(vol),
342
+ volume: transferred_volume,
343
+ range: effective_range(vol),
344
+ reference_origin: [...reference_origin],
345
+ surfaces: surfaces.map((surface) => ({
346
+ token: surface.key,
347
+ isovalue: surface.isovalue,
348
+ })),
349
+ }
350
+ }),
351
+ }
352
+
353
+ return new Promise((resolve, reject) => {
354
+ const worker = new Worker(new URL(`geometry.worker`, import.meta.url), {
355
+ type: `module`,
356
+ })
357
+ geometry_worker = worker
358
+ reject_worker_job = reject
359
+ const cleanup = (): void => {
360
+ worker.terminate()
361
+ if (geometry_worker === worker) geometry_worker = undefined
362
+ reject_worker_job = undefined
363
+ }
364
+ worker.addEventListener(`message`, ({ data }: MessageEvent<GeometryWorkerResponse>) => {
365
+ cleanup()
366
+ resolve(data)
367
+ })
368
+ worker.addEventListener(`error`, (event) => {
369
+ cleanup()
370
+ reject(new Error(event.message || `Isosurface geometry worker failed`))
371
+ })
372
+ try {
373
+ worker.postMessage(request, transfer)
374
+ } catch (error) {
375
+ cleanup()
376
+ reject(error instanceof Error ? error : new Error(String(error)))
377
+ }
378
+ })
379
+ }
380
+
381
+ async function rebuild_geometries(
382
+ layers: ResolvedLayer[],
383
+ generation: number,
384
+ ): Promise<void> {
385
+ const profile_start = profiler ? performance.now() : 0
386
+ let cache_hits = 0
387
+ // Reuse geometries whose signature is unchanged (take-once so duplicate
388
+ // layers at the same isovalue never share a geometry instance)
389
+ const reusable = new Map(active_entries.map((entry) => [entry.geo_key, entry]))
390
+ const plans: EntryPlan[] = []
391
+ const pending: PendingSurface[] = []
392
+
179
393
  for (let layer_idx = 0; layer_idx < layers.length; layer_idx++) {
180
394
  const layer = layers[layer_idx]
181
- if (!layer.visible || layer.isovalue <= 0) continue
182
-
183
- // Each layer gets 4 slots (positive back/front + negative back/front).
184
- const base_order = (layer_render_rank.get(layer_idx) ?? layer_idx) * 4
185
-
186
- const pos_geo = surface_at(layer.isovalue)
187
- if (pos_geo) {
188
- entries.push({
189
- geometry: pos_geo,
190
- color: layer.color,
191
- opacity: layer.opacity,
192
- render_order: base_order,
395
+ const vol = all_volumes[layer.volume_idx]
396
+ if (!vol || !layer.visible || layer.isovalue <= 0) continue
397
+
398
+ const signs: (1 | -1)[] = layer.show_negative ? [1, -1] : [1]
399
+ for (const sign of signs) {
400
+ const key = `${layer_idx}:${sign}`
401
+ const geo_key = geometry_key(layer, sign)
402
+ const reused = reusable.get(geo_key)
403
+ if (reused) {
404
+ cache_hits++
405
+ reusable.delete(geo_key)
406
+ // Keep only the cached geometry + scalars; key/layer_idx/sign are
407
+ // rebound to the CURRENT loop values (they override the spread)
408
+ plans.push({ ...reused, key, layer_idx, sign })
409
+ continue
410
+ }
411
+ const surface: PendingSurface = {
412
+ key,
413
+ geo_key,
414
+ layer_idx,
415
+ sign,
416
+ isovalue: sign * layer.isovalue,
417
+ volume: vol,
418
+ }
419
+ pending.push(surface)
420
+ plans.push(surface)
421
+ }
422
+ }
423
+
424
+ const geometries = new Map<string, BufferGeometry>()
425
+ let build_synchronously = !use_geometry_worker(pending)
426
+ if (!build_synchronously) {
427
+ try {
428
+ const response = await request_worker_geometries(pending)
429
+ if (generation !== rebuild_generation || `error` in response) {
430
+ if (`error` in response && generation === rebuild_generation) {
431
+ throw new Error(response.error)
432
+ }
433
+ return
434
+ }
435
+ const volume_by_token = new Map(
436
+ pending.map(({ volume: source }) => [vol_id(source), source]),
437
+ )
438
+ for (const volume_result of response.volumes) {
439
+ const source_volume = volume_by_token.get(volume_result.token)
440
+ if (source_volume) {
441
+ const grid = profile_stage(
442
+ profiler,
443
+ `prepare_geometry`,
444
+ () => inflate_grid(volume_result.prepared_values, volume_result.grid_dims),
445
+ {
446
+ worker: true,
447
+ inflate: true,
448
+ },
449
+ )
450
+ prepared_cache.set(source_volume, {
451
+ key: prepared_key(source_volume),
452
+ grid,
453
+ lattice: volume_result.lattice,
454
+ vertex_shift: vertex_shift(volume_result.origin),
455
+ })
456
+ }
457
+ record_profile(profiler, `prepare_geometry`, volume_result.prepare_geometry_ms, {
458
+ worker: true,
459
+ })
460
+ for (const surface_result of volume_result.surfaces) {
461
+ record_profile(profiler, `marching_cubes`, surface_result.marching_cubes_ms, {
462
+ worker: true,
463
+ vertices: surface_result.positions.length / 3,
464
+ })
465
+ const geometry = profile_stage(
466
+ profiler,
467
+ `build_geometry`,
468
+ () => build_geometry(surface_result.positions, surface_result.indices),
469
+ (built_geometry) => ({
470
+ worker: true,
471
+ buffer_bytes: built_geometry ? geometry_buffer_bytes(built_geometry) : 0,
472
+ }),
473
+ )
474
+ if (geometry) geometries.set(surface_result.token, geometry)
475
+ }
476
+ }
477
+ } catch (error) {
478
+ if (generation !== rebuild_generation) return
479
+ const message = error instanceof Error ? error.message : String(error)
480
+ record_profile(profiler, `prepare_geometry`, 0, {
481
+ worker_fallback: true,
482
+ error: message,
193
483
  })
484
+ console.warn(`Isosurface geometry worker failed; using synchronous fallback`, error)
485
+ build_synchronously = true
486
+ }
487
+ }
488
+ if (build_synchronously) {
489
+ for (const surface of pending) {
490
+ const geometry = extract_surface(
491
+ surface.isovalue,
492
+ prepare_geometry_volume(surface.volume),
493
+ )
494
+ if (geometry) geometries.set(surface.key, geometry)
194
495
  }
496
+ }
195
497
 
196
- if (layer.show_negative) {
197
- const neg_geo = surface_at(-layer.isovalue)
198
- if (neg_geo) {
498
+ if (generation !== rebuild_generation) {
499
+ for (const geometry of geometries.values()) geometry.dispose()
500
+ return
501
+ }
502
+ const entries: MeshEntry[] = []
503
+ for (const plan of plans) {
504
+ if (`geometry` in plan) entries.push(plan)
505
+ else {
506
+ const geometry = geometries.get(plan.key)
507
+ if (geometry) {
508
+ const { key, geo_key, layer_idx, sign } = plan
199
509
  entries.push({
200
- geometry: neg_geo,
201
- color: layer.negative_color,
202
- opacity: layer.opacity,
203
- render_order: base_order + 2,
510
+ key,
511
+ geo_key,
512
+ layer_idx,
513
+ sign,
514
+ geometry,
515
+ render_order: 0,
516
+ scalars: null,
517
+ scalars_volume_id: null,
204
518
  })
205
519
  }
206
520
  }
207
521
  }
208
522
 
209
- active_geometries = entries
210
- for (const entry of old) entry.geometry.dispose()
523
+ // Render outer shells first so per-entry back/front passes interleave
524
+ // roughly back-to-front. Isovalues from different volumes aren't directly
525
+ // comparable, so rank by isovalue as a fraction of each volume's abs_max.
526
+ const shell_fraction = (entry: MeshEntry): number => {
527
+ const layer = layers[entry.layer_idx]
528
+ const abs_max = all_volumes[layer.volume_idx]?.data_range.abs_max ?? 1
529
+ return layer.isovalue / Math.max(abs_max, 1e-30)
530
+ }
531
+ entries
532
+ .sort((entry_a, entry_b) => shell_fraction(entry_a) - shell_fraction(entry_b))
533
+ .forEach((entry, rank) => (entry.render_order = rank * 2))
534
+
535
+ // Dispose old geometries that were not reused, then swap in the new list
536
+ const kept = new Set(entries.map((entry) => entry.geometry))
537
+ for (const entry of active_entries) {
538
+ if (!kept.has(entry.geometry)) entry.geometry.dispose()
539
+ }
540
+ // Sync vertex-color flags: reused geometries keep their color attribute;
541
+ // new entries get colored by the color effect after this rebuild
542
+ colored_keys.clear()
543
+ for (const entry of entries) {
544
+ if (entry.geometry.getAttribute(`color`)) colored_keys.add(entry.key)
545
+ }
546
+ active_entries = entries
547
+ let [vertices, triangles, buffer_bytes] = [0, 0, 0]
548
+ for (const entry of entries) {
549
+ vertices += entry.geometry.getAttribute(`position`).count
550
+ triangles += (entry.geometry.getIndex()?.count ?? 0) / 3
551
+ buffer_bytes += geometry_buffer_bytes(entry.geometry)
552
+ }
553
+ record_profile(profiler, `rebuild_total`, performance.now() - profile_start, {
554
+ entries: entries.length,
555
+ cache_hits,
556
+ vertices,
557
+ triangles,
558
+ buffer_bytes,
559
+ })
211
560
  }
212
561
 
213
- // Rebuild all layer geometries when layers or volume change.
214
- // Debounces rapid changes (e.g. slider drags) to avoid repeated expensive marching cubes.
562
+ // Geometry-relevant inputs as a string: $derived strings only notify
563
+ // dependents when the value changes, so the rebuild effect below skips
564
+ // color-only updates (colors, opacity, colormap, range are excluded here)
565
+ // without any manual last-signature bookkeeping.
566
+ let geo_sig = $derived(
567
+ resolved_layers
568
+ .map((layer) => {
569
+ const vol = all_volumes[layer.volume_idx]
570
+ if (!vol || !layer.visible || layer.isovalue <= 0) return `off`
571
+ return `${geometry_key(layer, 1)}.${layer.show_negative}`
572
+ })
573
+ .join(`|`),
574
+ )
575
+
576
+ // Rebuild layer geometries when the geometry signature changes (debounced for
577
+ // slider drags). The rAF callback reads resolved_layers fresh, so a rebuild
578
+ // scheduled by an earlier signature still sees the latest layer props.
215
579
  $effect(() => {
216
- const layers = resolved_layers
217
- void settings.halo
218
- if (!ds_result) {
219
- dispose_all()
580
+ void geo_sig
581
+ const generation = ++rebuild_generation
582
+ clearTimeout(debounce_id)
583
+ cancelAnimationFrame(raf_id)
584
+ cancel_geometry_worker()
585
+ if (untrack(() => all_volumes.length) === 0) {
586
+ untrack(() => dispose_all())
220
587
  return
221
588
  }
222
589
  debounce_id = window.setTimeout(() => {
223
- raf_id = requestAnimationFrame(() => rebuild_geometries(layers))
590
+ // No reactive context inside rAF: reads the freshest layers untracked
591
+ raf_id = requestAnimationFrame(() => {
592
+ void rebuild_geometries(resolved_layers, generation)
593
+ })
224
594
  }, 50)
225
- return () => {
226
- clearTimeout(debounce_id)
227
- cancelAnimationFrame(raf_id)
595
+ })
596
+
597
+ // === Cross-volume vertex coloring ===
598
+
599
+ // Sample the color volume at this entry's vertices. Vertices are in the scene
600
+ // frame (first volume's origin at the grid corner), so shift back to absolute
601
+ // Cartesian coordinates before sampling. The bulk kernel caches each volume's
602
+ // inverse lattice and avoids per-vertex closures or temporary position arrays.
603
+ function sample_entry_scalars(entry: MeshEntry, color_vol: VolumetricData): Float32Array {
604
+ return profile_stage(profiler, `sample_scalars`, () => {
605
+ const positions = entry.geometry.getAttribute(`position`).array as Float32Array
606
+ return sample_volume_at_positions(color_vol, positions, {
607
+ out_of_bounds: color_vol.periodic ? `clamp` : `fallback`,
608
+ position_offset: reference_origin,
609
+ out: entry.scalars ?? undefined,
610
+ })
611
+ })
612
+ }
613
+
614
+ // The layer's scalar-color-source volume, if any
615
+ const color_vol_of = (layer: IsosurfaceLayer | undefined): VolumetricData | undefined =>
616
+ layer?.color_volume_idx != null ? all_volumes[layer.color_volume_idx] : undefined
617
+
618
+ function apply_vertex_colors(entries: MeshEntry[], layers: ResolvedLayer[]) {
619
+ const profile_start = profiler ? performance.now() : 0
620
+ const auto_scalars = new Map<number, Float32Array[]>()
621
+ // First pass: (re)sample scalars for entries with a color source
622
+ for (const entry of entries) {
623
+ const layer = layers[entry.layer_idx]
624
+ const color_vol = color_vol_of(layer)
625
+ if (!layer || !color_vol) {
626
+ if (entry.geometry.getAttribute(`color`)) entry.geometry.deleteAttribute(`color`)
627
+ entry.scalars = null
628
+ entry.scalars_volume_id = null
629
+ colored_keys.delete(entry.key)
630
+ continue
631
+ }
632
+ const color_vol_id = vol_id(color_vol)
633
+ if (!entry.scalars || entry.scalars_volume_id !== color_vol_id) {
634
+ entry.scalars = sample_entry_scalars(entry, color_vol)
635
+ entry.scalars_volume_id = color_vol_id
636
+ } else {
637
+ record_profile(profiler, `sample_scalars`, 0, {
638
+ cache_hit: true,
639
+ vertices: entry.scalars.length,
640
+ })
641
+ }
642
+ if (!layer.color_range) {
643
+ const scalars = auto_scalars.get(entry.layer_idx) ?? []
644
+ scalars.push(entry.scalars)
645
+ auto_scalars.set(entry.layer_idx, scalars)
646
+ }
647
+ }
648
+
649
+ // Default color range per layer: fit to the scalars actually present on that
650
+ // layer's surfaces (both lobes) — whole-volume ranges are dominated by
651
+ // extremes near nuclei that the surface never touches. Signed source fields
652
+ // keep a symmetric range so diverging colormaps stay centered on zero.
653
+ const auto_ranges = new Map<number, [number, number]>()
654
+ for (const [layer_idx, layer_scalars] of auto_scalars) {
655
+ const layer = layers[layer_idx]
656
+ const color_vol = color_vol_of(layer)
657
+ auto_ranges.set(
658
+ layer_idx,
659
+ profile_stage(profiler, `scalar_range`, () =>
660
+ compute_scalar_range(layer_scalars, {
661
+ symmetric: color_vol ? is_signed_range(color_vol.data_range) : false,
662
+ }),
663
+ ),
664
+ )
665
+ }
666
+
667
+ // Second pass: map scalars through the colormap LUT, reusing the existing
668
+ // color attribute's array where possible to avoid GPU buffer churn
669
+ for (const entry of entries) {
670
+ const layer = layers[entry.layer_idx]
671
+ if (!layer || !entry.scalars) continue
672
+ const color_range = layer.color_range ?? auto_ranges.get(entry.layer_idx) ?? [0, 1]
673
+ const existing = entry.geometry.getAttribute(`color`) as BufferAttribute | undefined
674
+ const colors = profile_stage(profiler, `apply_colormap`, () =>
675
+ scalars_to_vertex_colors(
676
+ entry.scalars as Float32Array,
677
+ {
678
+ colormap: layer.colormap ?? DEFAULT_ISO_COLORMAP,
679
+ color_range,
680
+ fallback_color: entry.sign > 0 ? layer.color : layer.negative_color,
681
+ },
682
+ existing?.array instanceof Float32Array ? existing.array : undefined,
683
+ ),
684
+ )
685
+ if (existing && existing.array === colors) existing.needsUpdate = true
686
+ else entry.geometry.setAttribute(`color`, new BufferAttribute(colors, 3))
687
+ colored_keys.add(entry.key)
228
688
  }
689
+ record_profile(profiler, `recolor_total`, performance.now() - profile_start, {
690
+ entries: entries.length,
691
+ })
692
+ }
693
+
694
+ // Color-relevant layer props as a string, including the color-source volume
695
+ // identity (so replacing a color volume in place resamples) but excluding
696
+ // geometry fields like isovalue and opacity, so e.g. isovalue slider drags
697
+ // don't remap colors on every tick.
698
+ let color_sig = $derived(
699
+ resolved_layers
700
+ .map((layer) => {
701
+ const color_vol = color_vol_of(layer)
702
+ return [
703
+ layer.color_volume_idx ?? ``,
704
+ color_vol ? vol_id(color_vol) : ``,
705
+ layer.colormap ?? ``,
706
+ layer.color_range?.join(`,`) ?? ``,
707
+ layer.color,
708
+ layer.negative_color,
709
+ ].join(`.`)
710
+ })
711
+ .join(`|`),
712
+ )
713
+
714
+ // Apply/refresh vertex colors when entries are rebuilt or color props change
715
+ $effect(() => {
716
+ void color_sig
717
+ const entries = active_entries
718
+ untrack(() => apply_vertex_colors(entries, resolved_layers))
229
719
  })
230
720
  </script>
231
721
 
232
- <!-- Render each geometry entry with appropriate material -->
233
- {#each active_geometries as entry (entry)}
234
- {#if settings.wireframe}
235
- <T.Mesh geometry={entry.geometry} frustumCulled={false}>
236
- <T.MeshBasicMaterial
237
- color={entry.color}
238
- wireframe
239
- transparent={entry.opacity < 1}
240
- opacity={entry.opacity}
241
- />
242
- </T.Mesh>
243
- {:else if entry.opacity < 1}
244
- <!-- Two-pass rendering for correct transparency -->
245
- <T.Mesh
246
- geometry={entry.geometry}
247
- renderOrder={entry.render_order}
248
- frustumCulled={false}
249
- >
250
- <T.MeshStandardMaterial
251
- color={entry.color}
252
- transparent
253
- opacity={entry.opacity}
254
- side={BackSide}
255
- depthWrite={false}
256
- metalness={0.1}
257
- roughness={0.6}
258
- />
259
- </T.Mesh>
260
- <T.Mesh
261
- geometry={entry.geometry}
262
- renderOrder={entry.render_order + 1}
263
- frustumCulled={false}
264
- >
265
- <T.MeshStandardMaterial
266
- color={entry.color}
267
- transparent
268
- opacity={entry.opacity}
269
- side={FrontSide}
270
- depthWrite={false}
271
- metalness={0.1}
272
- roughness={0.6}
273
- />
274
- </T.Mesh>
275
- {:else}
276
- <T.Mesh geometry={entry.geometry} frustumCulled={false}>
277
- <T.MeshStandardMaterial
278
- color={entry.color}
279
- side={DoubleSide}
280
- metalness={0.1}
281
- roughness={0.6}
282
- />
283
- </T.Mesh>
722
+ <!-- Render each mesh entry; material props react to layer changes without geometry rebuilds -->
723
+ {#each active_entries as entry (entry.key)}
724
+ {@const layer = resolved_layers[entry.layer_idx]}
725
+ {#if layer}
726
+ {@const vertex_colored = colored_keys.has(entry.key)}
727
+ {@const color = vertex_colored
728
+ ? `#ffffff`
729
+ : entry.sign > 0
730
+ ? layer.color
731
+ : layer.negative_color}
732
+ {@const opacity = layer.opacity}
733
+ {@const transparent = opacity < 1}
734
+ <!-- Recreate materials when vertexColors toggles (needs shader recompile) -->
735
+ {#key vertex_colored}
736
+ {#if settings.wireframe}
737
+ <T.Mesh geometry={entry.geometry} frustumCulled={false}>
738
+ <T.MeshBasicMaterial
739
+ {color}
740
+ vertexColors={vertex_colored}
741
+ wireframe
742
+ {transparent}
743
+ {opacity}
744
+ />
745
+ </T.Mesh>
746
+ {:else}
747
+ <!-- Transparent surfaces render back then front faces in two passes for
748
+ correct blending; opaque surfaces need a single double-sided pass -->
749
+ {#each transparent ? [BackSide, FrontSide] : [DoubleSide] as side, pass_idx (side)}
750
+ <T.Mesh
751
+ geometry={entry.geometry}
752
+ renderOrder={entry.render_order + pass_idx}
753
+ frustumCulled={false}
754
+ >
755
+ <T.MeshStandardMaterial
756
+ {color}
757
+ vertexColors={vertex_colored}
758
+ {transparent}
759
+ {opacity}
760
+ {side}
761
+ depthWrite={!transparent}
762
+ metalness={0.1}
763
+ roughness={0.6}
764
+ />
765
+ </T.Mesh>
766
+ {/each}
767
+ {/if}
768
+ {/key}
284
769
  {/if}
285
770
  {/each}