matterviz 0.4.0 → 0.4.2

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 (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  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 +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  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 +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  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 +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -6,23 +6,21 @@
6
6
  import TemperatureSlider from '../convex-hull/TemperatureSlider.svelte'
7
7
  import type { PhaseData } from '../convex-hull/types'
8
8
  import Spinner from '../feedback/Spinner.svelte'
9
- import Icon from '../Icon.svelte'
9
+ import type { ExportSection } from '../io'
10
+ import ExportPane from '../io/ExportPane.svelte'
10
11
  import { format_num } from '../labels'
11
- import { set_fullscreen_bg, SettingsSection, toggle_fullscreen } from '../layout'
12
- import type { Vec2, Vec3 } from '../math'
13
12
  import {
14
- convex_hull_2d,
15
- cross_3d,
16
- merge_coplanar_triangles,
17
- normalize_vec,
18
- } from '../math'
13
+ FullscreenButton,
14
+ type FullscreenToggleProp,
15
+ SettingsSection,
16
+ toggle_fullscreen,
17
+ } from '../layout'
18
+ import { sync_fullscreen } from '../layout/fullscreen.svelte'
19
+ import type { Vec2, Vec3 } from '../math'
20
+ import { convex_hull_2d, cross_3d, merge_coplanar_triangles, normalize_vec } from '../math'
19
21
  import DraggablePane from '../overlays/DraggablePane.svelte'
20
22
  import { ColorBar, ScatterPlot3DControls } from '../plot'
21
- import {
22
- constrain_tooltip_position,
23
- pad_rect,
24
- rects_overlap,
25
- } from '../plot/core/layout'
23
+ import { constrain_tooltip_position, pad_rect, rects_overlap } from '../plot/core/layout'
26
24
  import type {
27
25
  AxisConfig3D,
28
26
  CameraProjection3D,
@@ -32,15 +30,29 @@
32
30
  import { Canvas, T } from '@threlte/core'
33
31
  import * as extras from '@threlte/extras'
34
32
  import { scaleLinear } from 'd3-scale'
33
+ import type { Snippet } from 'svelte'
35
34
  import { onDestroy, onMount, untrack } from 'svelte'
36
35
  import { SvelteMap, SvelteSet } from 'svelte/reactivity'
37
36
  import * as THREE from 'three'
38
37
  import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
39
- import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
40
38
  import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js'
41
39
  import { compute_chempot_async } from './async-compute.svelte'
42
40
  import ChemPotScene3D from './ChemPotScene3D.svelte'
43
41
  import { get_chempot_color_bar_config, make_chempot_color_scale } from './color'
42
+ import {
43
+ CHEMPOT_COLOR_MODE_OPTIONS,
44
+ CHEMPOT_COLOR_SCALE_OPTIONS,
45
+ create_chempot_overrides,
46
+ } from './controls-state.svelte'
47
+ import {
48
+ export_glb_file,
49
+ export_json_file,
50
+ export_png_file,
51
+ export_svg_file,
52
+ export_view_json_file,
53
+ get_json_string,
54
+ get_view_settings,
55
+ } from './export'
44
56
  import {
45
57
  apply_element_padding,
46
58
  bbox_diagonal,
@@ -85,6 +97,11 @@
85
97
  max_interpolation_gap = CHEMPOT_DEFAULTS.max_interpolation_gap,
86
98
  hover_info = $bindable<ChemPotHoverInfo | null>(null),
87
99
  render_local_tooltip = true,
100
+ wrapper = $bindable(),
101
+ fullscreen = $bindable(false),
102
+ fullscreen_toggle = true,
103
+ controls_open = $bindable(false),
104
+ export_pane_open = $bindable(false),
88
105
  }: {
89
106
  entries: PhaseData[]
90
107
  config?: ChemPotDiagramConfig
@@ -95,62 +112,51 @@
95
112
  max_interpolation_gap?: number
96
113
  hover_info?: ChemPotHoverInfo | null
97
114
  render_local_tooltip?: boolean
115
+ // bindable: top-level wrapper element
116
+ wrapper?: HTMLDivElement
117
+ // bindable: fullscreen state
118
+ fullscreen?: boolean
119
+ // show/hide the fullscreen button (or custom snippet to render it)
120
+ fullscreen_toggle?: FullscreenToggleProp
121
+ // bindable: whether the controls pane is currently open
122
+ controls_open?: boolean
123
+ // bindable: whether the export pane is currently open
124
+ export_pane_open?: boolean
98
125
  } = $props()
99
126
 
100
- let formal_chempots_override = $state<boolean | null>(null)
101
- let label_stable_override = $state<boolean | null>(null)
102
- let element_padding_override = $state<number | null>(null)
103
- let default_min_limit_override = $state<number | null>(null)
104
- let draw_formula_meshes_override = $state<boolean | null>(null)
105
- let draw_formula_lines_override = $state<boolean | null>(null)
106
- const formal_chempots = $derived(
107
- formal_chempots_override ??
108
- (config.formal_chempots ?? CHEMPOT_DEFAULTS.formal_chempots),
109
- )
110
- const label_stable = $derived(
111
- label_stable_override ?? (config.label_stable ?? CHEMPOT_DEFAULTS.label_stable),
112
- )
113
- const element_padding = $derived(
114
- element_padding_override ??
115
- (config.element_padding ?? CHEMPOT_DEFAULTS.element_padding),
116
- )
117
- const default_min_limit = $derived(
118
- default_min_limit_override ??
119
- (config.default_min_limit ?? CHEMPOT_DEFAULTS.default_min_limit),
120
- )
121
- let formulas_to_draw_override = $state<string[] | null>(null)
122
- const formulas_to_draw = $derived(
123
- formulas_to_draw_override ?? (config.formulas_to_draw ?? []),
124
- )
125
- const draw_formula_meshes = $derived(
126
- draw_formula_meshes_override ??
127
- (config.draw_formula_meshes ?? CHEMPOT_DEFAULTS.draw_formula_meshes),
128
- )
129
- const draw_formula_lines = $derived(
130
- draw_formula_lines_override ??
131
- (config.draw_formula_lines ?? CHEMPOT_DEFAULTS.draw_formula_lines),
132
- )
133
- let color_mode_override = $state<ChemPotColorMode | null>(null)
134
- let color_scale_override = $state<D3InterpolateName | null>(null)
135
- let reverse_color_scale_override = $state<boolean | null>(null)
136
- const color_mode = $derived(
137
- color_mode_override ?? (config.color_mode ?? `arity`),
138
- )
139
- const color_scale = $derived(
140
- color_scale_override ?? (config.color_scale ?? CHEMPOT_DEFAULTS.color_scale),
141
- )
142
- const reverse_color_scale = $derived(
143
- reverse_color_scale_override ??
144
- (config.reverse_color_scale ?? CHEMPOT_DEFAULTS.reverse_color_scale),
127
+ // Control overrides (override ?? config ?? default, cleared by Reset)
128
+ const overrides = create_chempot_overrides(
129
+ () => config,
130
+ [
131
+ `formal_chempots`,
132
+ `label_stable`,
133
+ `element_padding`,
134
+ `default_min_limit`,
135
+ `formulas_to_draw`,
136
+ `draw_formula_meshes`,
137
+ `draw_formula_lines`,
138
+ `color_mode`,
139
+ `color_scale`,
140
+ `reverse_color_scale`,
141
+ ],
142
+ { color_mode: `arity`, formulas_to_draw: [] },
145
143
  )
144
+ const formal_chempots = $derived(overrides.resolve(`formal_chempots`))
145
+ const label_stable = $derived(overrides.resolve(`label_stable`))
146
+ const element_padding = $derived(overrides.resolve(`element_padding`))
147
+ const default_min_limit = $derived(overrides.resolve(`default_min_limit`))
148
+ const formulas_to_draw = $derived(overrides.resolve(`formulas_to_draw`))
149
+ const draw_formula_meshes = $derived(overrides.resolve(`draw_formula_meshes`))
150
+ const draw_formula_lines = $derived(overrides.resolve(`draw_formula_lines`))
151
+ const color_mode = $derived(overrides.resolve(`color_mode`))
152
+ const color_scale = $derived(overrides.resolve(`color_scale`))
153
+ const reverse_color_scale = $derived(overrides.resolve(`reverse_color_scale`))
146
154
  const show_tooltip = $derived(config.show_tooltip ?? CHEMPOT_DEFAULTS.show_tooltip)
147
155
  const tooltip_detail_level = $derived(
148
156
  config.tooltip_detail_level ?? CHEMPOT_DEFAULTS.tooltip_detail_level,
149
157
  )
150
158
  const formula_colors = $derived(
151
- config.formula_colors?.length
152
- ? config.formula_colors
153
- : CHEMPOT_DEFAULTS.formula_colors,
159
+ config.formula_colors?.length ? config.formula_colors : CHEMPOT_DEFAULTS.formula_colors,
154
160
  )
155
161
 
156
162
  const formula_label_segments = (formula: string): FormulaLabelSegment[] =>
@@ -167,33 +173,44 @@
167
173
  return deduped
168
174
  }
169
175
 
170
- let wrapper = $state<HTMLDivElement>()
171
- let fullscreen = $state(false)
172
- let export_pane_open = $state(false)
173
176
  let formula_picker_open = $state(false)
174
- let controls_open = $state(false)
175
177
 
176
178
  // Mutual exclusion: only one pane open at a time.
177
179
  // Separate effects so each reacts to its own pane opening independently —
178
180
  // a single $derived ternary would create priority ordering where opening
179
181
  // a "lower" pane while a "higher" one is open fails silently.
180
- $effect(() => { if (export_pane_open) { formula_picker_open = false; controls_open = false } })
181
- $effect(() => { if (formula_picker_open) { export_pane_open = false; controls_open = false } })
182
- $effect(() => { if (controls_open) { export_pane_open = false; formula_picker_open = false } })
183
- let copy_status = $state(false)
184
- let copy_timeout_id: ReturnType<typeof setTimeout> | null = null
182
+ $effect(() => {
183
+ if (export_pane_open) {
184
+ formula_picker_open = false
185
+ controls_open = false
186
+ }
187
+ })
188
+ $effect(() => {
189
+ if (formula_picker_open) {
190
+ export_pane_open = false
191
+ controls_open = false
192
+ }
193
+ })
194
+ $effect(() => {
195
+ if (controls_open) {
196
+ export_pane_open = false
197
+ formula_picker_open = false
198
+ }
199
+ })
185
200
  let container_width = $state(0)
186
201
  let container_height = $state(0)
187
202
  const base_aspect_ratio = $derived(height > 0 && width > 0 ? height / width : 1)
188
203
  const render_width = $derived(container_width > 0 ? container_width : width)
189
204
  const render_height = $derived(
190
205
  fullscreen
191
- ? (container_height > 0 ? container_height : height)
206
+ ? container_height > 0
207
+ ? container_height
208
+ : height
192
209
  : Math.round(render_width * base_aspect_ratio),
193
210
  )
194
211
 
195
212
  let mounted = $state(false)
196
- onMount(() => mounted = true)
213
+ onMount(() => (mounted = true))
197
214
  let orbit_controls_ref = $state<OrbitControls | undefined>(undefined)
198
215
  // Backside tracking: axes/ticks/labels render on the far side from the camera
199
216
  // back[i] = backside data coordinate value for data axis i
@@ -246,9 +263,7 @@
246
263
  if (next_temperature !== temperature) temperature = next_temperature
247
264
  })
248
265
 
249
- const show_temperature_slider = $derived(
250
- has_temp_data && available_temperatures.length > 0,
251
- )
266
+ const show_temperature_slider = $derived(has_temp_data && available_temperatures.length > 0)
252
267
 
253
268
  const projection_source_entries = $derived(
254
269
  get_projection_source_entries(entries, temp_filtered_entries),
@@ -260,10 +275,10 @@
260
275
  projection_source_entries.flatMap((entry) =>
261
276
  Object.entries(entry.composition)
262
277
  .filter(([, amount]) => amount > 0)
263
- .map(([element]) => element)
278
+ .map(([element]) => element),
264
279
  ),
265
280
  ),
266
- ).sort()
281
+ ).sort(),
267
282
  )
268
283
  const has_multinary_system = $derived(all_entry_elements.length > 3)
269
284
  let projection_elements_override = $state<string[] | null>(null)
@@ -285,9 +300,7 @@
285
300
  })
286
301
  const effective_config = $derived({
287
302
  ...config,
288
- elements: projection_elements.length === 3
289
- ? projection_elements
290
- : config.elements,
303
+ elements: projection_elements.length === 3 ? projection_elements : config.elements,
291
304
  formal_chempots,
292
305
  label_stable,
293
306
  element_padding,
@@ -317,7 +330,9 @@
317
330
  diagram_data = null
318
331
  diagram_computing = false
319
332
  })
320
- return () => { cancelled = true }
333
+ return () => {
334
+ cancelled = true
335
+ }
321
336
  })
322
337
 
323
338
  const plot_elements = $derived(diagram_data?.elements ?? projection_elements)
@@ -346,15 +361,11 @@
346
361
  })
347
362
  const current_projection_key = $derived(plot_elements.join(`|`))
348
363
  let formula_filter_query = $state(``)
349
- const available_formulas = $derived.by(() =>
350
- Object.keys(diagram_data?.domains ?? {}).sort()
351
- )
364
+ const available_formulas = $derived.by(() => Object.keys(diagram_data?.domains ?? {}).sort())
352
365
  const filtered_formulas = $derived.by(() => {
353
366
  const query = formula_filter_query.trim().toLowerCase()
354
367
  if (!query) return available_formulas
355
- return available_formulas.filter((formula) =>
356
- formula.toLowerCase().includes(query)
357
- )
368
+ return available_formulas.filter((formula) => formula.toLowerCase().includes(query))
358
369
  })
359
370
 
360
371
  // Process domains for rendering
@@ -378,45 +389,49 @@
378
389
  max_energy_per_atom: number | null
379
390
  }
380
391
  type NumericColorMode = Exclude<ChemPotColorMode, `none` | `arity`>
392
+ const domain_annotation_cache = new Map<string, number[]>()
393
+
394
+ function get_domain_ann_loc(points_3d: number[][]): number[] {
395
+ if (points_3d.length >= 3) {
396
+ const cache_key = points_3d.map((point) => point.join(`,`)).join(`;`)
397
+ const cached = domain_annotation_cache.get(cache_key)
398
+ if (cached) return cached
399
+ const ann_loc = get_3d_domain_simplexes_and_ann_loc(points_3d).ann_loc
400
+ domain_annotation_cache.set(cache_key, ann_loc)
401
+ return ann_loc
402
+ }
403
+ return points_3d[0].map(
404
+ (_, col_idx) =>
405
+ points_3d.reduce((sum, point) => sum + point[col_idx], 0) / points_3d.length,
406
+ )
407
+ }
381
408
 
382
409
  const render_domains = $derived.by((): DomainRenderData[] => {
383
410
  if (!diagram_data || plot_elements.length < 2) return []
384
411
 
385
412
  const dim = diagram_data.elements.length
386
413
  const indices = Array.from({ length: dim }, (_, idx) => idx)
387
- const new_lims = element_padding > 0
388
- ? apply_element_padding(
389
- diagram_data.domains,
390
- indices,
391
- element_padding,
392
- default_min_limit,
393
- )
394
- : null
414
+ const new_lims =
415
+ element_padding > 0
416
+ ? apply_element_padding(
417
+ diagram_data.domains,
418
+ indices,
419
+ element_padding,
420
+ default_min_limit,
421
+ )
422
+ : null
395
423
 
396
424
  const result: DomainRenderData[] = []
397
425
  for (const [formula, pts] of Object.entries(diagram_data.domains)) {
398
426
  const padded = new_lims
399
- ? pad_domain_points(
400
- pts,
401
- indices,
402
- new_lims,
403
- default_min_limit,
404
- element_padding,
405
- )
427
+ ? pad_domain_points(pts, indices, new_lims, default_min_limit, element_padding)
406
428
  : pts
407
429
  if (padded.length < 2) continue
408
- const is_draw = formulas_to_draw.includes(formula)
409
- const centroid = padded[0].map((_, col_idx) =>
410
- padded.reduce((sum, point) => sum + point[col_idx], 0) / padded.length
411
- )
412
- const ann_loc = padded.length >= 3
413
- ? get_3d_domain_simplexes_and_ann_loc(padded).ann_loc
414
- : centroid
415
430
  result.push({
416
431
  formula,
417
432
  points_3d: padded,
418
- ann_loc,
419
- is_draw_formula: is_draw,
433
+ ann_loc: get_domain_ann_loc(padded),
434
+ is_draw_formula: formulas_to_draw.includes(formula),
420
435
  label_font_size: bbox_diagonal(padded),
421
436
  })
422
437
  }
@@ -467,9 +482,7 @@
467
482
  // Original (non-renormalized) elemental references for formation energy computation.
468
483
  // diagram_data.el_refs may be renormalized to zero when formal_chempots is true,
469
484
  // so we compute our own from the raw entries to get true DFT reference energies.
470
- const raw_el_refs = $derived(
471
- get_min_entries_and_el_refs(temp_filtered_entries).el_refs,
472
- )
485
+ const raw_el_refs = $derived(get_min_entries_and_el_refs(temp_filtered_entries).el_refs)
473
486
 
474
487
  const color_mode_labels: Record<NumericColorMode, string> = {
475
488
  energy: `Energy per atom (eV)`,
@@ -484,11 +497,7 @@
484
497
  return entry_energy_stats_by_formula.get(formula)?.min_energy_per_atom ?? null
485
498
  }
486
499
  if (active_color_mode === `formation_energy`) {
487
- return best_form_energy_for_formula(
488
- temp_filtered_entries,
489
- formula,
490
- raw_el_refs,
491
- ) ?? null
500
+ return best_form_energy_for_formula(temp_filtered_entries, formula, raw_el_refs) ?? null
492
501
  }
493
502
  return entry_energy_stats_by_formula.get(formula)?.matching_entry_count ?? 0
494
503
  }
@@ -535,24 +544,22 @@
535
544
  })
536
545
 
537
546
  // Range and label for the color bar (null for none/arity which are categorical)
538
- const color_range = $derived.by(
539
- (): { min: number; max: number; label: string } | null => {
540
- const values = domain_color_values?.values ?? []
541
- if (values.length === 0) return null
542
- let lo = values[0], hi = values[0]
543
- for (let idx = 1; idx < values.length; idx++) {
544
- if (values[idx] < lo) lo = values[idx]
545
- if (values[idx] > hi) hi = values[idx]
546
- }
547
- return {
548
- min: lo,
549
- max: Math.max(hi, lo + 1e-6),
550
- label: color_mode === `none` || color_mode === `arity`
551
- ? ``
552
- : color_mode_labels[color_mode],
553
- }
554
- },
555
- )
547
+ const color_range = $derived.by((): { min: number; max: number; label: string } | null => {
548
+ const values = domain_color_values?.values ?? []
549
+ if (values.length === 0) return null
550
+ let lo = values[0],
551
+ hi = values[0]
552
+ for (let idx = 1; idx < values.length; idx++) {
553
+ if (values[idx] < lo) lo = values[idx]
554
+ if (values[idx] > hi) hi = values[idx]
555
+ }
556
+ return {
557
+ min: lo,
558
+ max: Math.max(hi, lo + 1e-6),
559
+ label:
560
+ color_mode === `none` || color_mode === `arity` ? `` : color_mode_labels[color_mode],
561
+ }
562
+ })
556
563
 
557
564
  const arity_legend_labels = $derived.by((): string[] => {
558
565
  let has_four_plus_regions = false
@@ -572,9 +579,12 @@
572
579
  const render_axis_scale = $derived.by((): Vec3 => {
573
580
  const points = render_domains.flatMap((domain) => domain.points_3d)
574
581
  if (points.length === 0) return [1, 1, 1]
575
- let min0 = Infinity, max0 = -Infinity
576
- let min1 = Infinity, max1 = -Infinity
577
- let min2 = Infinity, max2 = -Infinity
582
+ let min0 = Infinity,
583
+ max0 = -Infinity
584
+ let min1 = Infinity,
585
+ max1 = -Infinity
586
+ let min2 = Infinity,
587
+ max2 = -Infinity
578
588
  for (const point of points) {
579
589
  if (point[0] < min0) min0 = point[0]
580
590
  if (point[0] > max0) max0 = point[0]
@@ -614,11 +624,7 @@
614
624
  sum_z += z_val
615
625
  }
616
626
  const n_points = points.length
617
- const center = new THREE.Vector3(
618
- sum_x / n_points,
619
- sum_y / n_points,
620
- sum_z / n_points,
621
- )
627
+ const center = new THREE.Vector3(sum_x / n_points, sum_y / n_points, sum_z / n_points)
622
628
  // Compute max distance from center
623
629
  let max_dist = 0
624
630
  for (const point of points) {
@@ -633,30 +639,22 @@
633
639
  data_center.y + data_extent,
634
640
  data_center.z + data_extent,
635
641
  ])
636
- const default_camera_target = $derived<Vec3>([
637
- data_center.x,
638
- data_center.y,
639
- data_center.z,
640
- ])
642
+ const default_camera_target = $derived<Vec3>([data_center.x, data_center.y, data_center.z])
641
643
  const default_orthographic_zoom = $derived(
642
644
  Math.min(render_width, render_height) / (data_extent * 1.6),
643
645
  )
644
646
  let camera_position_override = $state<Vec3 | null>(null)
645
647
  let camera_target_override = $state<Vec3 | null>(null)
646
648
  let orthographic_zoom_override = $state<number | null>(null)
647
- const camera_position = $derived(
648
- camera_position_override ?? default_camera_position,
649
- )
650
- const camera_target = $derived(
651
- camera_target_override ?? default_camera_target,
652
- )
653
- const orthographic_zoom = $derived(
654
- orthographic_zoom_override ?? default_orthographic_zoom,
655
- )
649
+ const camera_position = $derived(camera_position_override ?? default_camera_position)
650
+ const camera_target = $derived(camera_target_override ?? default_camera_target)
651
+ const orthographic_zoom = $derived(orthographic_zoom_override ?? default_orthographic_zoom)
656
652
  // Label scale factor: zoom relative to default, so labels grow/shrink with zoom
657
653
  // Labels scale sub-linearly with zoom so they grow but don't dominate when zoomed in
658
654
  const zoom_scale = $derived(
659
- default_orthographic_zoom > 0 ? Math.sqrt(orthographic_zoom / default_orthographic_zoom) : 1,
655
+ default_orthographic_zoom > 0
656
+ ? Math.sqrt(orthographic_zoom / default_orthographic_zoom)
657
+ : 1,
660
658
  )
661
659
  let last_data_center: Vec3 | null = null
662
660
  let last_data_extent: number | null = null
@@ -665,14 +663,16 @@
665
663
  // Each domain in a chem pot diagram is a convex polygon/polyhedron. We project
666
664
  // to 2D (trying all 3 axis-aligned planes) and use the best projection's
667
665
  // convex hull boundary. This reliably handles both flat and 3D domains.
668
- function get_domain_edges(
669
- pts: number[][],
670
- ): [number[], number[]][] {
666
+ function get_domain_edges(pts: number[][]): [number[], number[]][] {
671
667
  const unique = dedup_3d(pts)
672
668
  if (unique.length < 2) return []
673
669
  if (unique.length === 2) return [[unique[0], unique[1]]]
674
670
  if (unique.length === 3) {
675
- return [[unique[0], unique[1]], [unique[1], unique[2]], [unique[0], unique[2]]]
671
+ return [
672
+ [unique[0], unique[1]],
673
+ [unique[1], unique[2]],
674
+ [unique[0], unique[2]],
675
+ ]
676
676
  }
677
677
  return get_2d_hull_edges(unique)
678
678
  }
@@ -692,9 +692,7 @@
692
692
  // (largest non-degenerate hull area). Unioning multiple projections can add
693
693
  // non-physical diagonals for nearly coplanar domains.
694
694
  // Called only from get_domain_edges with 4+ unique points
695
- function get_2d_hull_edges(
696
- pts: number[][],
697
- ): [number[], number[]][] {
695
+ function get_2d_hull_edges(pts: number[][]): [number[], number[]][] {
698
696
  let selected_hull: Vec2[] = []
699
697
  let selected_coord_to_idx: SvelteMap<string, number> | null = null
700
698
  let selected_hull_area = -1
@@ -704,15 +702,20 @@
704
702
 
705
703
  // Skip this projection if points collapse to a line (near-zero range in
706
704
  // either projected axis). This avoids spurious edges from edge-on views.
707
- let min0 = Infinity, max0 = -Infinity, min1 = Infinity, max1 = -Infinity
705
+ let min0 = Infinity,
706
+ max0 = -Infinity,
707
+ min1 = Infinity,
708
+ max1 = -Infinity
708
709
  for (const pt of pts) {
709
- const v0 = pt[axes[0]], v1 = pt[axes[1]]
710
+ const v0 = pt[axes[0]],
711
+ v1 = pt[axes[1]]
710
712
  if (v0 < min0) min0 = v0
711
713
  if (v0 > max0) max0 = v0
712
714
  if (v1 < min1) min1 = v1
713
715
  if (v1 > max1) max1 = v1
714
716
  }
715
- const range0 = max0 - min0, range1 = max1 - min1
717
+ const range0 = max0 - min0,
718
+ range1 = max1 - min1
716
719
  const max_2d_range = Math.max(range0, range1)
717
720
  if (max_2d_range < 1e-6 || Math.min(range0, range1) < max_2d_range * 0.01) {
718
721
  continue
@@ -750,7 +753,9 @@
750
753
  `${point_b[0].toFixed(6)},${point_b[1].toFixed(6)}`,
751
754
  )
752
755
  if (
753
- point_a_idx == null || point_b_idx == null || point_a_idx >= pts.length ||
756
+ point_a_idx == null ||
757
+ point_b_idx == null ||
758
+ point_a_idx >= pts.length ||
754
759
  point_b_idx >= pts.length
755
760
  ) {
756
761
  console.warn(`get_2d_hull_edges: invalid edge`, {
@@ -797,7 +802,7 @@
797
802
  for (const [pa, pb] of get_domain_edges(swizzled)) {
798
803
  const ka = pa.map((coord) => coord.toFixed(4)).join(`,`)
799
804
  const kb = pb.map((coord) => coord.toFixed(4)).join(`,`)
800
- const key = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`
805
+ const key = edge_key(ka, kb)
801
806
  if (seen.has(key)) continue
802
807
  seen.add(key)
803
808
  positions.push(pa[0], pa[1], pa[2], pb[0], pb[1], pb[2])
@@ -844,7 +849,9 @@
844
849
  const n_verts = pos.count
845
850
  const n_faces = n_verts / 3
846
851
  // Hull centroid for orienting face normals outward
847
- let hx = 0, hy = 0, hz = 0
852
+ let hx = 0,
853
+ hy = 0,
854
+ hz = 0
848
855
  for (let vert_idx = 0; vert_idx < n_verts; vert_idx++) {
849
856
  hx += pos.getX(vert_idx)
850
857
  hy += pos.getY(vert_idx)
@@ -898,7 +905,9 @@
898
905
  const centroids = render_domains
899
906
  .filter((domain) => !domain.is_draw_formula && domain.points_3d.length > 0)
900
907
  .map((domain) => {
901
- let sx = 0, sy = 0, sz = 0
908
+ let sx = 0,
909
+ sy = 0,
910
+ sz = 0
902
911
  for (const pt of domain.points_3d) {
903
912
  const [x_val, y_val, z_val] = to_render_xyz(pt)
904
913
  sx += x_val
@@ -906,7 +915,12 @@
906
915
  sz += z_val
907
916
  }
908
917
  const n_points = domain.points_3d.length
909
- return { formula: domain.formula, cx: sx / n_points, cy: sy / n_points, cz: sz / n_points }
918
+ return {
919
+ formula: domain.formula,
920
+ cx: sx / n_points,
921
+ cy: sy / n_points,
922
+ cz: sz / n_points,
923
+ }
910
924
  })
911
925
 
912
926
  // Assign each face to the nearest domain centroid
@@ -934,11 +948,11 @@
934
948
  // coplanar neighbors, then assign each group to its most-common domain.
935
949
  if (n_faces > 1) {
936
950
  const tol = 1e-3
937
- const round = (v: number): number => Math.round(v / tol)
951
+ const round = (val: number): number => Math.round(val / tol)
938
952
  const vkey = (vert_idx: number): string =>
939
- `${round(pos.getX(vert_idx))},${round(pos.getY(vert_idx))},${
940
- round(pos.getZ(vert_idx))
941
- }`
953
+ `${round(pos.getX(vert_idx))},${round(pos.getY(vert_idx))},${round(
954
+ pos.getZ(vert_idx),
955
+ )}`
942
956
  // Compute face normals
943
957
  const normals: Vec3[] = []
944
958
  for (let face_idx = 0; face_idx < n_faces; face_idx++) {
@@ -960,13 +974,11 @@
960
974
  for (let face_idx = 0; face_idx < n_faces; face_idx++) {
961
975
  const base = face_idx * 3
962
976
  const keys = [vkey(base), vkey(base + 1), vkey(base + 2)]
963
- for (
964
- const ek of [
965
- edge_key(keys[0], keys[1]),
966
- edge_key(keys[1], keys[2]),
967
- edge_key(keys[0], keys[2]),
968
- ]
969
- ) {
977
+ for (const ek of [
978
+ edge_key(keys[0], keys[1]),
979
+ edge_key(keys[1], keys[2]),
980
+ edge_key(keys[0], keys[2]),
981
+ ]) {
970
982
  const list = edge_faces.get(ek)
971
983
  if (list) list.push(face_idx)
972
984
  else edge_faces.set(ek, [face_idx])
@@ -974,21 +986,23 @@
974
986
  }
975
987
  // Union-find for coplanar adjacent faces
976
988
  const parent = Array.from({ length: n_faces }, (_, idx) => idx)
977
- const find = (x: number): number => {
978
- while (parent[x] !== x) {
979
- parent[x] = parent[parent[x]]
980
- x = parent[x]
989
+ const find = (node: number): number => {
990
+ while (parent[node] !== node) {
991
+ parent[node] = parent[parent[node]]
992
+ node = parent[node]
981
993
  }
982
- return x
994
+ return node
983
995
  }
984
996
  const union = (a_idx: number, b_idx: number): void => {
985
- const ra = find(a_idx), rb = find(b_idx)
997
+ const ra = find(a_idx),
998
+ rb = find(b_idx)
986
999
  if (ra !== rb) parent[ra] = rb
987
1000
  }
988
1001
  for (const pair of edge_faces.values()) {
989
1002
  if (pair.length !== 2) continue
990
1003
  const [fa, fb] = pair
991
- const na = normals[fa], nb = normals[fb]
1004
+ const na = normals[fa],
1005
+ nb = normals[fb]
992
1006
  if (Math.abs(na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2]) > 1 - tol) {
993
1007
  union(fa, fb)
994
1008
  }
@@ -1138,15 +1152,9 @@
1138
1152
 
1139
1153
  const controls_series = $derived<DataSeries3D[]>([
1140
1154
  {
1141
- x: render_domains.flatMap((domain) =>
1142
- domain.points_3d.map((point) => point[1])
1143
- ),
1144
- y: render_domains.flatMap((domain) =>
1145
- domain.points_3d.map((point) => point[2])
1146
- ),
1147
- z: render_domains.flatMap((domain) =>
1148
- domain.points_3d.map((point) => point[0])
1149
- ),
1155
+ x: render_domains.flatMap((domain) => domain.points_3d.map((point) => point[1])),
1156
+ y: render_domains.flatMap((domain) => domain.points_3d.map((point) => point[2])),
1157
+ z: render_domains.flatMap((domain) => domain.points_3d.map((point) => point[0])),
1150
1158
  label: `domains`,
1151
1159
  },
1152
1160
  ])
@@ -1157,8 +1165,7 @@
1157
1165
  const result: { geometry: THREE.BufferGeometry; color: string }[] = []
1158
1166
  for (const domain of render_domains) {
1159
1167
  if (!domain.is_draw_formula) continue
1160
- const color_idx = formulas_to_draw.indexOf(domain.formula) %
1161
- formula_colors.length
1168
+ const color_idx = formulas_to_draw.indexOf(domain.formula) % formula_colors.length
1162
1169
  const swizzled = domain.points_3d.map((point) => to_render_xyz(point))
1163
1170
  const positions: number[] = []
1164
1171
  for (const [pa, pb] of get_domain_edges(swizzled)) {
@@ -1177,8 +1184,7 @@
1177
1184
  if (!draw_formula_meshes) return result
1178
1185
  for (const domain of render_domains) {
1179
1186
  if (!domain.is_draw_formula || domain.points_3d.length < 4) continue
1180
- const color_idx = formulas_to_draw.indexOf(domain.formula) %
1181
- formula_colors.length
1187
+ const color_idx = formulas_to_draw.indexOf(domain.formula) % formula_colors.length
1182
1188
  const unique = dedup_3d(domain.points_3d)
1183
1189
  if (unique.length < 4) continue
1184
1190
  const vectors = unique.map((pt) => to_vec3(pt))
@@ -1192,10 +1198,7 @@
1192
1198
  return result
1193
1199
  })
1194
1200
 
1195
- function get_touches_limits(
1196
- points_3d: number[][],
1197
- lims: [number, number][],
1198
- ): string[] {
1201
+ function get_touches_limits(points_3d: number[][], lims: Vec2[]): string[] {
1199
1202
  const limit_tol = 1e-3
1200
1203
  const touches_limits: string[] = []
1201
1204
  for (
@@ -1205,11 +1208,11 @@
1205
1208
  ) {
1206
1209
  const [axis_min, axis_max] = lims[axis_idx]
1207
1210
  const axis_name = plot_elements[axis_idx] ?? `axis_${axis_idx}`
1208
- const touches_min = points_3d.some((point) =>
1209
- Math.abs(point[axis_idx] - axis_min) < limit_tol
1211
+ const touches_min = points_3d.some(
1212
+ (point) => Math.abs(point[axis_idx] - axis_min) < limit_tol,
1210
1213
  )
1211
- const touches_max = points_3d.some((point) =>
1212
- Math.abs(point[axis_idx] - axis_max) < limit_tol
1214
+ const touches_max = points_3d.some(
1215
+ (point) => Math.abs(point[axis_idx] - axis_max) < limit_tol,
1213
1216
  )
1214
1217
  if (touches_min) touches_limits.push(`${axis_name} lower bound`)
1215
1218
  if (touches_max) touches_limits.push(`${axis_name} upper bound`)
@@ -1267,9 +1270,7 @@
1267
1270
  const vertex_owners = new SvelteMap<string, string[]>()
1268
1271
  for (const domain of render_domains) {
1269
1272
  for (const pt of domain.points_3d) {
1270
- const key = pt.map((val) => (Math.round(val / tol) * tol).toFixed(4)).join(
1271
- `,`,
1272
- )
1273
+ const key = pt.map((val) => (Math.round(val / tol) * tol).toFixed(4)).join(`,`)
1273
1274
  const owners = vertex_owners.get(key)
1274
1275
  if (owners) {
1275
1276
  if (!owners.includes(domain.formula)) owners.push(domain.formula)
@@ -1347,9 +1348,7 @@
1347
1348
  geometry.dispose()
1348
1349
  }
1349
1350
 
1350
- function dispose_geometries(
1351
- geometries: (THREE.BufferGeometry | null | undefined)[],
1352
- ): void {
1351
+ function dispose_geometries(geometries: (THREE.BufferGeometry | null | undefined)[]): void {
1353
1352
  for (const geometry of geometries) dispose_geometry(geometry)
1354
1353
  }
1355
1354
 
@@ -1453,10 +1452,7 @@
1453
1452
  })
1454
1453
 
1455
1454
  // Helper to create a line geometry from two Vec3 arrays
1456
- function make_line_geom(
1457
- start: Vec3,
1458
- end: Vec3,
1459
- ): THREE.BufferGeometry {
1455
+ function make_line_geom(start: Vec3, end: Vec3): THREE.BufferGeometry {
1460
1456
  const geom = new THREE.BufferGeometry()
1461
1457
  geom.setAttribute(
1462
1458
  `position`,
@@ -1486,10 +1482,10 @@
1486
1482
  // Place axis label just past the outer end of the axis (the end closer to 0).
1487
1483
  // In isometric 3D, the end near 0 projects outward at the front edge of the
1488
1484
  // bounding box, while the negative end projects inward toward the center.
1489
- const outer_end = (range: [number, number]): number =>
1485
+ const outer_end = (range: Vec2): number =>
1490
1486
  Math.abs(range[0]) <= Math.abs(range[1]) ? range[0] : range[1]
1491
1487
  // Direction from range center toward outer end (to extend the label beyond the grid)
1492
- const outer_direction = (range: [number, number]): number => {
1488
+ const outer_direction = (range: Vec2): number => {
1493
1489
  const end = outer_end(range)
1494
1490
  const mid = (range[0] + range[1]) / 2
1495
1491
  return end >= mid ? 1 : -1
@@ -1504,11 +1500,12 @@
1504
1500
  return [0, 1, 2].map((axis) => {
1505
1501
  const ticks = data_ticks[axis]
1506
1502
  const color = axis_colors[axis]
1507
- const label = axis === 0
1508
- ? (z_axis.label || chem_axis_label(0))
1509
- : axis === 1
1510
- ? (x_axis.label || chem_axis_label(1))
1511
- : (y_axis.label || chem_axis_label(2))
1503
+ const label =
1504
+ axis === 0
1505
+ ? z_axis.label || chem_axis_label(0)
1506
+ : axis === 1
1507
+ ? x_axis.label || chem_axis_label(1)
1508
+ : y_axis.label || chem_axis_label(2)
1512
1509
 
1513
1510
  const tick_geoms: THREE.BufferGeometry[] = []
1514
1511
  const grid_geoms: THREE.BufferGeometry[] = []
@@ -1528,16 +1525,14 @@
1528
1525
  back[2] + out_y * tick_label_dist,
1529
1526
  )
1530
1527
  for (const val of ticks) {
1531
- tick_geoms.push(make_line_geom(
1532
- swiz(val, back[1], back[2]),
1533
- swiz(val, back[1], back[2] + out_y * tick_size),
1534
- ))
1535
- grid_geoms.push(
1536
- make_line_geom(swiz(val, r1[0], back[2]), swiz(val, r1[1], back[2])),
1537
- )
1538
- grid_geoms.push(
1539
- make_line_geom(swiz(val, back[1], r2[0]), swiz(val, back[1], r2[1])),
1528
+ tick_geoms.push(
1529
+ make_line_geom(
1530
+ swiz(val, back[1], back[2]),
1531
+ swiz(val, back[1], back[2] + out_y * tick_size),
1532
+ ),
1540
1533
  )
1534
+ grid_geoms.push(make_line_geom(swiz(val, r1[0], back[2]), swiz(val, r1[1], back[2])))
1535
+ grid_geoms.push(make_line_geom(swiz(val, back[1], r2[0]), swiz(val, back[1], r2[1])))
1541
1536
  tick_labels.push({
1542
1537
  pos: swiz(
1543
1538
  val,
@@ -1558,16 +1553,14 @@
1558
1553
  back[2] + out_y * tick_label_dist,
1559
1554
  )
1560
1555
  for (const val of ticks) {
1561
- tick_geoms.push(make_line_geom(
1562
- swiz(back[0], val, back[2]),
1563
- swiz(back[0], val, back[2] + out_y * tick_size),
1564
- ))
1565
- grid_geoms.push(
1566
- make_line_geom(swiz(r0[0], val, back[2]), swiz(r0[1], val, back[2])),
1567
- )
1568
- grid_geoms.push(
1569
- make_line_geom(swiz(back[0], val, r2[0]), swiz(back[0], val, r2[1])),
1556
+ tick_geoms.push(
1557
+ make_line_geom(
1558
+ swiz(back[0], val, back[2]),
1559
+ swiz(back[0], val, back[2] + out_y * tick_size),
1560
+ ),
1570
1561
  )
1562
+ grid_geoms.push(make_line_geom(swiz(r0[0], val, back[2]), swiz(r0[1], val, back[2])))
1563
+ grid_geoms.push(make_line_geom(swiz(back[0], val, r2[0]), swiz(back[0], val, r2[1])))
1571
1564
  tick_labels.push({
1572
1565
  pos: swiz(back[0], val, back[2] + out_y * tick_label_dist),
1573
1566
  text: format_num(val, `.3~g`),
@@ -1584,16 +1577,14 @@
1584
1577
  outer_end(r2) + outer_direction(r2) * axis_label_dist,
1585
1578
  )
1586
1579
  for (const val of ticks) {
1587
- tick_geoms.push(make_line_geom(
1588
- swiz(back[0], back[1], val),
1589
- swiz(back[0], back[1] + out_x * tick_size, val),
1590
- ))
1591
- grid_geoms.push(
1592
- make_line_geom(swiz(r0[0], back[1], val), swiz(r0[1], back[1], val)),
1593
- )
1594
- grid_geoms.push(
1595
- make_line_geom(swiz(back[0], r1[0], val), swiz(back[0], r1[1], val)),
1580
+ tick_geoms.push(
1581
+ make_line_geom(
1582
+ swiz(back[0], back[1], val),
1583
+ swiz(back[0], back[1] + out_x * tick_size, val),
1584
+ ),
1596
1585
  )
1586
+ grid_geoms.push(make_line_geom(swiz(r0[0], back[1], val), swiz(r0[1], back[1], val)))
1587
+ grid_geoms.push(make_line_geom(swiz(back[0], r1[0], val), swiz(back[0], r1[1], val)))
1597
1588
  tick_labels.push({
1598
1589
  pos: swiz(back[0], back[1] + out_x * tick_label_dist, val),
1599
1590
  text: format_num(val, `.3~g`),
@@ -1621,16 +1612,12 @@
1621
1612
 
1622
1613
  function update_label_occlusion(): void {
1623
1614
  if (!wrapper) return
1624
- const tick_labels = Array.from(
1625
- wrapper.querySelectorAll<HTMLElement>(`.axis-tick-label`),
1626
- )
1615
+ const tick_labels = Array.from(wrapper.querySelectorAll<HTMLElement>(`.axis-tick-label`))
1627
1616
  tick_labels_occluded = false
1628
1617
  for (const tick_label of tick_labels) {
1629
1618
  tick_label.style.visibility = ``
1630
1619
  }
1631
- const domain_rects = Array.from(
1632
- wrapper.querySelectorAll<HTMLElement>(`.domain-label`),
1633
- )
1620
+ const domain_rects = Array.from(wrapper.querySelectorAll<HTMLElement>(`.domain-label`))
1634
1621
  .filter((domain_label) => {
1635
1622
  const style = getComputedStyle(domain_label)
1636
1623
  return style.display !== `none` && style.visibility !== `hidden`
@@ -1698,11 +1685,7 @@
1698
1685
  }
1699
1686
  const controls_target = controls?.target
1700
1687
  if (controls_target) {
1701
- camera_target_override = [
1702
- controls_target.x,
1703
- controls_target.y,
1704
- controls_target.z,
1705
- ]
1688
+ camera_target_override = [controls_target.x, controls_target.y, controls_target.z]
1706
1689
  }
1707
1690
  }
1708
1691
 
@@ -1759,8 +1742,12 @@
1759
1742
  schedule_label_occlusion_update()
1760
1743
  })
1761
1744
 
1762
- $effect(() => {
1763
- set_fullscreen_bg(wrapper, fullscreen, `--chempot-3d-bg-fullscreen`)
1745
+ // Drive the browser Fullscreen API from the bindable `fullscreen` prop (parent-controllable), keep it in sync with Esc/external exits
1746
+ sync_fullscreen({
1747
+ get_wrapper: () => wrapper,
1748
+ get_fullscreen: () => fullscreen,
1749
+ set_fullscreen: (val) => (fullscreen = val),
1750
+ bg_css_var: `--chempot-3d-bg-fullscreen`,
1764
1751
  })
1765
1752
 
1766
1753
  $effect(() => {
@@ -1782,14 +1769,13 @@
1782
1769
  const projections = display.projections
1783
1770
  if (!projections) return []
1784
1771
  const [r0, r1, r2] = niced_range
1785
- const s0 = (r0[1] - r0[0]) * (display.projection_scale ?? 0.5)
1786
- const s1 = (r1[1] - r1[0]) * (display.projection_scale ?? 0.5)
1787
- const s2 = (r2[1] - r2[0]) * (display.projection_scale ?? 0.5)
1772
+ const projection_scale = display.projection_scale ?? 0.5
1773
+ const [s0, s1, s2] = niced_range.map(([lo, hi]) => (hi - lo) * projection_scale)
1788
1774
  const planes: {
1789
1775
  key: string
1790
1776
  pos: Vec3
1791
1777
  rot: Vec3
1792
- size: [number, number]
1778
+ size: Vec2
1793
1779
  color: string
1794
1780
  }[] = []
1795
1781
  if (projections.xy) {
@@ -1860,17 +1846,8 @@
1860
1846
  })
1861
1847
 
1862
1848
  function reset_controls(): void {
1863
- formal_chempots_override = null
1864
- label_stable_override = null
1865
- element_padding_override = null
1866
- default_min_limit_override = null
1867
- draw_formula_meshes_override = null
1868
- draw_formula_lines_override = null
1869
- color_mode_override = null
1870
- color_scale_override = null
1871
- reverse_color_scale_override = null
1849
+ overrides.reset()
1872
1850
  projection_elements_override = null
1873
- formulas_to_draw_override = null
1874
1851
  formula_filter_query = ``
1875
1852
  }
1876
1853
 
@@ -1883,18 +1860,12 @@
1883
1860
  next_projection[current_owner_idx] = next_projection[axis_idx]
1884
1861
  }
1885
1862
  next_projection[axis_idx] = element
1886
- const normalized = normalize_projection_triplet(
1887
- next_projection,
1888
- all_entry_elements,
1889
- )
1863
+ const normalized = normalize_projection_triplet(next_projection, all_entry_elements)
1890
1864
  if (normalized) projection_elements_override = normalized
1891
1865
  }
1892
1866
 
1893
1867
  function apply_projection_preset(preset_elements: string[]): void {
1894
- const normalized = normalize_projection_triplet(
1895
- preset_elements,
1896
- all_entry_elements,
1897
- )
1868
+ const normalized = normalize_projection_triplet(preset_elements, all_entry_elements)
1898
1869
  if (normalized) projection_elements_override = normalized
1899
1870
  }
1900
1871
 
@@ -1902,270 +1873,96 @@
1902
1873
  const selected_formulas = new SvelteSet(formulas_to_draw)
1903
1874
  if (selected_formulas.has(formula)) selected_formulas.delete(formula)
1904
1875
  else selected_formulas.add(formula)
1905
- formulas_to_draw_override = [...selected_formulas]
1876
+ overrides.set(`formulas_to_draw`, [...selected_formulas])
1906
1877
  }
1907
1878
 
1908
1879
  function select_surface_formulas(): void {
1909
- formulas_to_draw_override = render_domains
1910
- .filter((domain) => surface_formulas.has(domain.formula))
1911
- .map((domain) => domain.formula)
1880
+ overrides.set(
1881
+ `formulas_to_draw`,
1882
+ render_domains
1883
+ .filter((domain) => surface_formulas.has(domain.formula))
1884
+ .map((domain) => domain.formula),
1885
+ )
1912
1886
  }
1913
1887
 
1914
1888
  function select_neighbor_formulas(): void {
1915
1889
  if (hover_info?.view !== `3d`) return
1916
1890
  const neighbors = domain_neighbors.get(hover_info.formula) ?? []
1917
- formulas_to_draw_override = [hover_info.formula, ...neighbors]
1918
- }
1919
-
1920
- function download_blob(blob: Blob, filename: string): void {
1921
- const url = URL.createObjectURL(blob)
1922
- const link = document.createElement(`a`)
1923
- link.href = url
1924
- link.download = filename
1925
- link.click()
1926
- URL.revokeObjectURL(url)
1891
+ overrides.set(`formulas_to_draw`, [hover_info.formula, ...neighbors])
1927
1892
  }
1928
1893
 
1929
1894
  let png_dpi = $state(150)
1930
1895
  const export_basename = $derived(`chempot-${plot_elements.join(`-`)}`)
1931
1896
 
1932
- function get_view_settings(): Record<string, unknown> {
1933
- const view_camera_position = orbit_controls_ref?.object?.position
1934
- const view_camera_target = orbit_controls_ref?.target
1935
- return {
1897
+ const current_view_settings = (): Record<string, unknown> =>
1898
+ get_view_settings({
1936
1899
  elements: plot_elements,
1937
1900
  camera_projection,
1938
1901
  auto_rotate,
1939
1902
  color_mode,
1940
1903
  color_scale,
1941
1904
  reverse_color_scale,
1942
- camera_position: view_camera_position
1943
- ? [view_camera_position.x, view_camera_position.y, view_camera_position.z]
1944
- : null,
1945
- camera_target: view_camera_target
1946
- ? [view_camera_target.x, view_camera_target.y, view_camera_target.z]
1947
- : null,
1948
- }
1949
- }
1950
-
1951
- interface OverlayTextItem {
1952
- x: number
1953
- y: number
1954
- text: string
1955
- font: string
1956
- font_size: string
1957
- font_family: string
1958
- font_weight: string
1959
- color: string
1960
- }
1961
- function get_overlay_text_items(canvas_rect: DOMRect): OverlayTextItem[] {
1962
- if (!wrapper) return []
1963
- const text_items: OverlayTextItem[] = []
1964
- for (
1965
- const element of wrapper.querySelectorAll(
1966
- `.tick-label, .axis-label, .domain-label`,
1967
- )
1968
- ) {
1969
- const html_element = element as HTMLElement
1970
- const style = getComputedStyle(html_element)
1971
- if (style.display === `none` || style.visibility === `hidden`) continue
1972
- const element_rect = html_element.getBoundingClientRect()
1973
- text_items.push({
1974
- x: element_rect.left + element_rect.width / 2 - canvas_rect.left,
1975
- y: element_rect.top + element_rect.height / 2 - canvas_rect.top,
1976
- text: html_element.textContent ?? ``,
1977
- font: style.font || `${style.fontSize} ${style.fontFamily}`,
1978
- font_size: style.fontSize || `11px`,
1979
- font_family: style.fontFamily || `sans-serif`,
1980
- font_weight: style.fontWeight || `400`,
1981
- color: style.color || `#333`,
1982
- })
1983
- }
1984
- return text_items
1985
- }
1986
-
1987
- function export_png_file(): void {
1988
- if (!wrapper) return
1989
- const gl_canvas = wrapper.querySelector(`canvas`)
1990
- if (!(gl_canvas instanceof HTMLCanvasElement)) return
1991
-
1992
- // Composite WebGL canvas + HTML overlay labels into a single image
1993
- const rect = gl_canvas.getBoundingClientRect()
1994
- const scale = Math.min(png_dpi / 72, 10)
1995
- const out = document.createElement(`canvas`)
1996
- out.width = Math.round(rect.width * scale)
1997
- out.height = Math.round(rect.height * scale)
1998
- const ctx = out.getContext(`2d`)
1999
- if (!ctx) return
2000
- ctx.scale(scale, scale)
2001
-
2002
- // Draw the WebGL canvas as background
2003
- ctx.drawImage(gl_canvas, 0, 0, rect.width, rect.height)
2004
-
2005
- // Draw all HTML overlay text (tick labels, axis labels, domain labels)
2006
- for (const text_item of get_overlay_text_items(rect)) {
2007
- ctx.font = text_item.font
2008
- ctx.fillStyle = text_item.color
2009
- ctx.textAlign = `center`
2010
- ctx.textBaseline = `middle`
2011
- ctx.fillText(text_item.text, text_item.x, text_item.y)
2012
- }
2013
-
2014
- out.toBlob((blob) => {
2015
- if (!blob) return
2016
- download_blob(blob, `${export_basename}.png`)
2017
- }, `image/png`)
2018
- }
2019
-
2020
- const xml_escape = (text: string): string =>
2021
- text
2022
- .replaceAll(`&`, `&amp;`)
2023
- .replaceAll(`<`, `&lt;`)
2024
- .replaceAll(`>`, `&gt;`)
2025
- .replaceAll(`"`, `&quot;`)
2026
- .replaceAll(`'`, `&#39;`)
2027
-
2028
- function export_svg_file(): void {
2029
- if (!wrapper) return
2030
- const gl_canvas = wrapper.querySelector(`canvas`)
2031
- if (!(gl_canvas instanceof HTMLCanvasElement)) return
2032
- const canvas_rect = gl_canvas.getBoundingClientRect()
2033
- if (canvas_rect.width === 0 || canvas_rect.height === 0) return
2034
- const png_data_url = gl_canvas.toDataURL(`image/png`)
2035
- const text_nodes = get_overlay_text_items(canvas_rect).map((text_item) =>
2036
- `<text x="${text_item.x.toFixed(2)}" y="${
2037
- text_item.y.toFixed(2)
2038
- }" text-anchor="middle" dominant-baseline="central" fill="${
2039
- xml_escape(text_item.color)
2040
- }" font-size="${xml_escape(text_item.font_size)}" font-family="${
2041
- xml_escape(text_item.font_family)
2042
- }" font-weight="${xml_escape(text_item.font_weight)}">${
2043
- xml_escape(text_item.text)
2044
- }</text>`
2045
- )
2046
- const metadata = xml_escape(JSON.stringify(get_view_settings()))
2047
- const svg = [
2048
- `<?xml version="1.0" encoding="UTF-8"?>`,
2049
- `<svg xmlns="http://www.w3.org/2000/svg" width="${canvas_rect.width}" height="${canvas_rect.height}" viewBox="0 0 ${canvas_rect.width} ${canvas_rect.height}">`,
2050
- `<metadata>${metadata}</metadata>`,
2051
- `<image href="${png_data_url}" x="0" y="0" width="${canvas_rect.width}" height="${canvas_rect.height}" />`,
2052
- ...text_nodes,
2053
- `</svg>`,
2054
- ].join(``)
2055
- download_blob(
2056
- new Blob([svg], { type: `image/svg+xml` }),
2057
- `${export_basename}.svg`,
2058
- )
2059
- }
2060
-
2061
- function export_view_json_file(): void {
2062
- const json_text = JSON.stringify(get_view_settings(), null, 2)
2063
- download_blob(
2064
- new Blob([json_text], { type: `application/json` }),
2065
- `${export_basename}-view.json`,
2066
- )
2067
- }
2068
-
2069
- function export_glb_file(): void {
2070
- const gltf_exporter = new GLTFExporter()
2071
- const export_root = new THREE.Group()
2072
- if (colored_hull_geometry) {
2073
- export_root.add(
2074
- new THREE.Mesh(
2075
- colored_hull_geometry.clone(),
2076
- new THREE.MeshBasicMaterial({
2077
- vertexColors: true,
2078
- transparent: true,
2079
- opacity: color_mode === `none` ? 0.25 : 0.4,
2080
- side: THREE.DoubleSide,
2081
- }),
2082
- ),
2083
- )
2084
- }
2085
- export_root.add(
2086
- new THREE.LineSegments(
2087
- edge_geometry.clone(),
2088
- new THREE.LineBasicMaterial({ color: 0x333333 }),
2089
- ),
2090
- )
2091
- for (const { geometry, color } of formula_mesh_data) {
2092
- export_root.add(
2093
- new THREE.Mesh(
2094
- geometry.clone(),
2095
- new THREE.MeshBasicMaterial({
2096
- color: new THREE.Color(color),
2097
- transparent: true,
2098
- opacity: 0.13,
2099
- side: THREE.DoubleSide,
2100
- }),
2101
- ),
2102
- )
2103
- }
2104
- if (draw_formula_lines) {
2105
- for (const { geometry, color } of formula_edge_data) {
2106
- export_root.add(
2107
- new THREE.LineSegments(
2108
- geometry.clone(),
2109
- new THREE.LineBasicMaterial({ color: new THREE.Color(color) }),
2110
- ),
2111
- )
2112
- }
2113
- }
2114
- gltf_exporter.parse(
2115
- export_root,
2116
- (result) => {
2117
- if (!(result instanceof ArrayBuffer)) return
2118
- download_blob(
2119
- new Blob([result], { type: `model/gltf-binary` }),
2120
- `${export_basename}.glb`,
2121
- )
2122
- },
2123
- (err) => {
2124
- console.error(`Failed to export GLB:`, err)
2125
- },
2126
- { binary: true, onlyVisible: false },
2127
- )
2128
- }
2129
-
2130
- const get_json_string = (): string =>
2131
- JSON.stringify(
2132
- {
2133
- elements: diagram_data?.elements ?? [],
2134
- domains: render_domains.map((domain) => ({
2135
- formula: domain.formula,
2136
- points_3d: domain.points_3d,
2137
- })),
2138
- lims: diagram_data?.lims ?? [],
2139
- view: get_view_settings(),
2140
- },
2141
- null,
2142
- 2,
2143
- )
1905
+ camera_position: orbit_controls_ref?.object?.position ?? null,
1906
+ camera_target: orbit_controls_ref?.target ?? null,
1907
+ })
2144
1908
 
2145
- function export_json_file(): void {
2146
- download_blob(
2147
- new Blob([get_json_string()], { type: `application/json` }),
2148
- `${export_basename}.json`,
2149
- )
2150
- }
1909
+ const export_json_payload = (): Record<string, unknown> => ({
1910
+ elements: diagram_data?.elements ?? [],
1911
+ domains: render_domains.map((domain) => ({
1912
+ formula: domain.formula,
1913
+ points_3d: domain.points_3d,
1914
+ })),
1915
+ lims: diagram_data?.lims ?? [],
1916
+ view: current_view_settings(),
1917
+ })
2151
1918
 
2152
- async function copy_json(): Promise<void> {
2153
- try {
2154
- await navigator.clipboard.writeText(get_json_string())
2155
- copy_status = true
2156
- } catch (err) {
2157
- copy_status = false
2158
- console.error(`Failed to copy JSON to clipboard:`, err)
2159
- }
2160
- if (copy_timeout_id !== null) clearTimeout(copy_timeout_id)
2161
- copy_timeout_id = setTimeout(() => {
2162
- copy_status = false
2163
- copy_timeout_id = null
2164
- }, 1000)
2165
- }
1919
+ const export_sections = $derived<ExportSection[]>([
1920
+ {
1921
+ title: `Export Image`,
1922
+ items: [
1923
+ {
1924
+ label: `SVG`,
1925
+ on_download: () =>
1926
+ export_svg_file(wrapper, export_basename, current_view_settings()),
1927
+ },
1928
+ {
1929
+ label: `PNG`,
1930
+ show_dpi: true,
1931
+ on_download: () => export_png_file(wrapper, export_basename, png_dpi),
1932
+ },
1933
+ ],
1934
+ },
1935
+ {
1936
+ title: `Export Data`,
1937
+ items: [
1938
+ {
1939
+ label: `JSON`,
1940
+ on_download: () => export_json_file(export_json_payload(), export_basename),
1941
+ copy_text: () => get_json_string(export_json_payload()),
1942
+ },
1943
+ {
1944
+ label: `View`,
1945
+ on_download: () => export_view_json_file(current_view_settings(), export_basename),
1946
+ },
1947
+ {
1948
+ label: `GLB`,
1949
+ on_download: () =>
1950
+ export_glb_file(
1951
+ {
1952
+ hull_geometry: colored_hull_geometry,
1953
+ hull_opacity: color_mode === `none` ? 0.25 : 0.4,
1954
+ edge_geometry,
1955
+ formula_meshes: formula_mesh_data,
1956
+ formula_edges: draw_formula_lines ? formula_edge_data : [],
1957
+ },
1958
+ export_basename,
1959
+ ),
1960
+ },
1961
+ ],
1962
+ },
1963
+ ])
2166
1964
 
2167
1965
  onDestroy(() => {
2168
- if (copy_timeout_id !== null) clearTimeout(copy_timeout_id)
2169
1966
  if (label_occlusion_frame !== null) cancelAnimationFrame(label_occlusion_frame)
2170
1967
  })
2171
1968
 
@@ -2176,10 +1973,12 @@
2176
1973
  const pointer = hover_info?.pointer
2177
1974
  if (!pointer) return { x: 4, y: 4 }
2178
1975
  return constrain_tooltip_position(
2179
- pointer.x, pointer.y,
1976
+ pointer.x,
1977
+ pointer.y,
2180
1978
  tooltip_el?.offsetWidth ?? 200,
2181
1979
  tooltip_el?.offsetHeight ?? 100,
2182
- container_width, container_height,
1980
+ container_width,
1981
+ container_height,
2183
1982
  { offset: 0 },
2184
1983
  )
2185
1984
  })
@@ -2197,12 +1996,25 @@
2197
1996
  hover_info = null
2198
1997
  }
2199
1998
 
1999
+ function stop_phase_pointer_event(raw_event: unknown): void {
2000
+ if (!raw_event || typeof raw_event !== `object`) return
2001
+
2002
+ const event = raw_event as { nativeEvent?: unknown; stopPropagation?: () => void }
2003
+ event.stopPropagation?.()
2004
+
2005
+ const native_event = event.nativeEvent
2006
+ if (!native_event || typeof native_event !== `object`) return
2007
+ const native_pointer_event = native_event as { stopPropagation?: () => void }
2008
+ native_pointer_event.stopPropagation?.()
2009
+ }
2010
+
2200
2011
  function handle_phase_hover(domain_data: HoverMeshData, raw_event: unknown): void {
2201
2012
  if (locked_hover_formula && locked_hover_formula !== domain_data.formula) return
2202
2013
  set_hover_info(domain_data, raw_event)
2203
2014
  }
2204
2015
 
2205
2016
  function toggle_phase_lock(domain_data: HoverMeshData, raw_event: unknown): void {
2017
+ stop_phase_pointer_event(raw_event)
2206
2018
  if (locked_hover_formula === domain_data.formula) {
2207
2019
  clear_hover_lock()
2208
2020
  return
@@ -2212,25 +2024,13 @@
2212
2024
  }
2213
2025
 
2214
2026
  // Color mode cycling (keyboard shortcut 'c')
2215
- const color_modes: ChemPotColorMode[] = [
2216
- `none`,
2217
- `energy`,
2218
- `formation_energy`,
2219
- `arity`,
2220
- `entries`,
2221
- ]
2027
+ const color_modes = CHEMPOT_COLOR_MODE_OPTIONS.map(([value]) => value)
2222
2028
  function cycle_color_mode(): void {
2223
2029
  const idx = color_modes.indexOf(color_mode)
2224
- color_mode_override = color_modes[(idx + 1) % color_modes.length]
2030
+ overrides.set(`color_mode`, color_modes[(idx + 1) % color_modes.length])
2225
2031
  }
2226
2032
  </script>
2227
2033
 
2228
- <svelte:document
2229
- onfullscreenchange={() => {
2230
- fullscreen = document.fullscreenElement === wrapper
2231
- }}
2232
- />
2233
-
2234
2034
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
2235
2035
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
2236
2036
  <div
@@ -2244,91 +2044,30 @@
2244
2044
  role="application"
2245
2045
  tabindex="0"
2246
2046
  onkeydown={(event) => {
2247
- if (
2248
- event.target instanceof HTMLInputElement ||
2249
- event.target instanceof HTMLSelectElement
2250
- ) return
2047
+ if (event.target instanceof HTMLInputElement || event.target instanceof HTMLSelectElement)
2048
+ return
2251
2049
  if (event.key === `Escape`) clear_hover_lock()
2252
2050
  else if (event.key === `c`) cycle_color_mode()
2253
- else if (event.key === `f`) toggle_fullscreen(wrapper)
2051
+ else if (event.key === `f` && fullscreen_toggle) toggle_fullscreen(wrapper)
2254
2052
  }}
2255
2053
  onpointerdown={(event) => {
2256
2054
  const target = event.target
2257
- if (
2258
- locked_hover_formula &&
2259
- (target === wrapper || target instanceof HTMLCanvasElement)
2260
- ) {
2055
+ if (locked_hover_formula && (target === wrapper || target instanceof HTMLCanvasElement)) {
2261
2056
  clear_hover_lock()
2262
2057
  }
2263
2058
  }}
2264
2059
  >
2265
2060
  <section>
2266
- <DraggablePane
2267
- bind:show={export_pane_open}
2268
- open_icon="Cross"
2269
- closed_icon="Export"
2061
+ <ExportPane
2062
+ bind:export_pane_open
2063
+ bind:png_dpi
2064
+ sections={export_sections}
2270
2065
  pane_props={{ class: `chempot-export-pane` }}
2271
2066
  toggle_props={{
2272
2067
  class: `chempot-export-toggle`,
2273
2068
  title: `Export chemical potential diagram`,
2274
2069
  }}
2275
- >
2276
- <h4 id="export-image">Export Image</h4>
2277
- <div class="export-row">
2278
- <label>
2279
- SVG
2280
- <button type="button" onclick={export_svg_file} title="SVG snapshot export">
2281
-
2282
- </button>
2283
- </label>
2284
- <label>
2285
- PNG
2286
- <button type="button" onclick={export_png_file} title="PNG ({png_dpi} DPI)">
2287
-
2288
- </button>
2289
- &nbsp;(DPI: <input
2290
- type="number"
2291
- min={50}
2292
- max={500}
2293
- bind:value={png_dpi}
2294
- title="Export resolution in dots per inch"
2295
- style="margin: 0 0 0 2pt"
2296
- />)
2297
- </label>
2298
- </div>
2299
- <h4 id="export-data">Export Data</h4>
2300
- <div class="export-row">
2301
- <label>
2302
- JSON
2303
- <button type="button" onclick={export_json_file} aria-label="Download JSON">
2304
-
2305
- </button>
2306
- <button
2307
- type="button"
2308
- onclick={copy_json}
2309
- aria-label="Copy JSON to clipboard"
2310
- >
2311
- {copy_status ? `✅` : `📋`}
2312
- </button>
2313
- </label>
2314
- <label>
2315
- View
2316
- <button
2317
- type="button"
2318
- onclick={export_view_json_file}
2319
- aria-label="Download view JSON"
2320
- >
2321
-
2322
- </button>
2323
- </label>
2324
- <label>
2325
- GLB
2326
- <button type="button" onclick={export_glb_file} aria-label="Download GLB">
2327
-
2328
- </button>
2329
- </label>
2330
- </div>
2331
- </DraggablePane>
2070
+ />
2332
2071
  <DraggablePane
2333
2072
  bind:show={formula_picker_open}
2334
2073
  open_icon="Cross"
@@ -2341,7 +2080,7 @@
2341
2080
  >
2342
2081
  <h4 id="formula-overlays">Formula Overlays</h4>
2343
2082
  <div class="overlay-actions">
2344
- <button type="button" onclick={() => formulas_to_draw_override = []}>
2083
+ <button type="button" onclick={() => overrides.set(`formulas_to_draw`, [])}>
2345
2084
  Clear
2346
2085
  </button>
2347
2086
  <button type="button" onclick={select_surface_formulas}>Surface</button>
@@ -2349,11 +2088,7 @@
2349
2088
  </div>
2350
2089
  <label class="overlay-search">
2351
2090
  Search:
2352
- <input
2353
- type="text"
2354
- placeholder="Formula filter"
2355
- bind:value={formula_filter_query}
2356
- />
2091
+ <input type="text" placeholder="Formula filter" bind:value={formula_filter_query} />
2357
2092
  </label>
2358
2093
  <div class="formula-list">
2359
2094
  {#if filtered_formulas.length === 0}
@@ -2364,14 +2099,14 @@
2364
2099
  <label>
2365
2100
  <input
2366
2101
  type="checkbox"
2367
- checked={formulas_to_draw.includes(formula)}
2102
+ checked={formula_overlay_idx !== -1}
2368
2103
  onchange={() => toggle_formula_selection(formula)}
2369
2104
  />
2370
2105
  <span
2371
2106
  class="formula-color-dot"
2372
2107
  style:background={formula_colors[
2373
2108
  (formula_overlay_idx >= 0 ? formula_overlay_idx : formula_idx) %
2374
- formula_colors.length
2109
+ formula_colors.length
2375
2110
  ]}
2376
2111
  ></span>
2377
2112
  {get_electro_neg_formula(formula, true, ``, `.3~s`)}
@@ -2464,36 +2199,28 @@
2464
2199
  <input
2465
2200
  type="checkbox"
2466
2201
  checked={formal_chempots}
2467
- onchange={() => {
2468
- formal_chempots_override = !formal_chempots
2469
- }}
2202
+ onchange={() => overrides.set(`formal_chempots`, !formal_chempots)}
2470
2203
  /> Formal
2471
2204
  </label>
2472
2205
  <label>
2473
2206
  <input
2474
2207
  type="checkbox"
2475
2208
  checked={label_stable}
2476
- onchange={() => {
2477
- label_stable_override = !label_stable
2478
- }}
2209
+ onchange={() => overrides.set(`label_stable`, !label_stable)}
2479
2210
  /> Labels
2480
2211
  </label>
2481
2212
  <label>
2482
2213
  <input
2483
2214
  type="checkbox"
2484
2215
  checked={draw_formula_meshes}
2485
- onchange={() => {
2486
- draw_formula_meshes_override = !draw_formula_meshes
2487
- }}
2216
+ onchange={() => overrides.set(`draw_formula_meshes`, !draw_formula_meshes)}
2488
2217
  /> Meshes
2489
2218
  </label>
2490
2219
  <label>
2491
2220
  <input
2492
2221
  type="checkbox"
2493
2222
  checked={draw_formula_lines}
2494
- onchange={() => {
2495
- draw_formula_lines_override = !draw_formula_lines
2496
- }}
2223
+ onchange={() => overrides.set(`draw_formula_lines`, !draw_formula_lines)}
2497
2224
  /> Lines
2498
2225
  </label>
2499
2226
  </div>
@@ -2505,9 +2232,8 @@
2505
2232
  min="0"
2506
2233
  step="0.1"
2507
2234
  value={element_padding}
2508
- oninput={(event) => {
2509
- element_padding_override = Number(event.currentTarget.value)
2510
- }}
2235
+ oninput={(event) =>
2236
+ overrides.set(`element_padding`, Number(event.currentTarget.value))}
2511
2237
  />
2512
2238
  </label>
2513
2239
  <label>
@@ -2517,11 +2243,8 @@
2517
2243
  max="0"
2518
2244
  step="1"
2519
2245
  value={default_min_limit}
2520
- oninput={(event) => {
2521
- default_min_limit_override = Number(
2522
- event.currentTarget.value,
2523
- )
2524
- }}
2246
+ oninput={(event) =>
2247
+ overrides.set(`default_min_limit`, Number(event.currentTarget.value))}
2525
2248
  />
2526
2249
  </label>
2527
2250
  </div>
@@ -2530,16 +2253,12 @@
2530
2253
  <select
2531
2254
  id="chempot-color-mode"
2532
2255
  value={color_mode}
2533
- onchange={(event) => {
2534
- color_mode_override = event.currentTarget
2535
- .value as ChemPotColorMode
2536
- }}
2256
+ onchange={(event) =>
2257
+ overrides.set(`color_mode`, event.currentTarget.value as ChemPotColorMode)}
2537
2258
  >
2538
- <option value="none">None</option>
2539
- <option value="energy">Energy/atom</option>
2540
- <option value="formation_energy">Formation energy</option>
2541
- <option value="arity">Element count</option>
2542
- <option value="entries">Entry count</option>
2259
+ {#each CHEMPOT_COLOR_MODE_OPTIONS as [value, label] (value)}
2260
+ <option {value}>{label}</option>
2261
+ {/each}
2543
2262
  </select>
2544
2263
  </div>
2545
2264
  {#if color_mode !== `none` && color_mode !== `arity`}
@@ -2548,27 +2267,18 @@
2548
2267
  <select
2549
2268
  id="chempot-color-scale"
2550
2269
  value={color_scale}
2551
- onchange={(event) => {
2552
- color_scale_override = event.currentTarget
2553
- .value as D3InterpolateName
2554
- }}
2270
+ onchange={(event) =>
2271
+ overrides.set(`color_scale`, event.currentTarget.value as D3InterpolateName)}
2555
2272
  >
2556
- <option value="interpolateViridis">Viridis</option>
2557
- <option value="interpolatePlasma">Plasma</option>
2558
- <option value="interpolateInferno">Inferno</option>
2559
- <option value="interpolateMagma">Magma</option>
2560
- <option value="interpolateCividis">Cividis</option>
2561
- <option value="interpolateTurbo">Turbo</option>
2562
- <option value="interpolateRdYlBu">RdYlBu</option>
2563
- <option value="interpolateSpectral">Spectral</option>
2273
+ {#each CHEMPOT_COLOR_SCALE_OPTIONS as [value, label] (value)}
2274
+ <option {value}>{label}</option>
2275
+ {/each}
2564
2276
  </select>
2565
2277
  <label>
2566
2278
  <input
2567
2279
  type="checkbox"
2568
2280
  checked={reverse_color_scale}
2569
- onchange={() => {
2570
- reverse_color_scale_override = !reverse_color_scale
2571
- }}
2281
+ onchange={() => overrides.set(`reverse_color_scale`, !reverse_color_scale)}
2572
2282
  /> Rev
2573
2283
  </label>
2574
2284
  </div>
@@ -2576,321 +2286,305 @@
2576
2286
  </SettingsSection>
2577
2287
  </ScatterPlot3DControls>
2578
2288
 
2579
- <button
2580
- type="button"
2581
- onclick={() => toggle_fullscreen(wrapper)}
2582
- title="{fullscreen ? `Exit` : `Enter`} fullscreen"
2583
- >
2584
- <Icon icon="{fullscreen ? `Exit` : ``}Fullscreen" />
2585
- </button>
2289
+ {#if fullscreen_toggle}
2290
+ <FullscreenButton {fullscreen} toggle={fullscreen_toggle} {wrapper} />
2291
+ {/if}
2586
2292
  </section>
2587
2293
  {#if show_temperature_slider && temperature !== undefined}
2588
- <TemperatureSlider
2589
- class="chempot-temp-slider"
2590
- {available_temperatures}
2591
- bind:temperature
2592
- />
2294
+ <TemperatureSlider class="chempot-temp-slider" {available_temperatures} bind:temperature />
2593
2295
  {/if}
2594
2296
  <div class="canvas-clip">
2595
- {#if diagram_computing}
2596
- <div class="computing-state">
2597
- <Spinner text="Computing chemical potential domains..." style="--spinner-size: 1.2em" />
2598
- </div>
2599
- {:else if !diagram_data}
2600
- <div class="error-state" role="alert" aria-live="polite">
2601
- <p>Cannot compute chemical potential diagram.</p>
2602
- <p>Need at least 2 elements with elemental reference entries.</p>
2603
- </div>
2604
- {:else if mounted && typeof WebGLRenderingContext !== `undefined`}
2605
- <Canvas
2606
- createRenderer={(cvs) =>
2607
- new THREE.WebGLRenderer({
2608
- canvas: cvs,
2609
- alpha: true,
2610
- antialias: true,
2611
- preserveDrawingBuffer: true,
2612
- })}
2613
- >
2614
- <ChemPotScene3D>
2615
- {#if camera_projection === `orthographic`}
2616
- <!-- Orthographic camera matching pymatgen's projection style -->
2617
- <T.OrthographicCamera
2618
- makeDefault
2619
- position={camera_position}
2620
- zoom={orthographic_zoom}
2621
- near={0.1}
2622
- far={data_extent * 10}
2623
- >
2624
- <extras.OrbitControls
2625
- bind:ref={orbit_controls_ref}
2626
- enableRotate
2627
- enableZoom
2628
- enablePan
2629
- autoRotate={auto_rotate > 0}
2630
- autoRotateSpeed={auto_rotate}
2631
- target={camera_target}
2632
- />
2633
- </T.OrthographicCamera>
2634
- {:else}
2635
- <T.PerspectiveCamera
2636
- makeDefault
2637
- position={camera_position}
2638
- fov={50}
2639
- near={0.1}
2640
- far={data_extent * 10}
2641
- >
2642
- <extras.OrbitControls
2643
- bind:ref={orbit_controls_ref}
2644
- enableRotate
2645
- enableZoom
2646
- enablePan
2647
- autoRotate={auto_rotate > 0}
2648
- autoRotateSpeed={auto_rotate}
2649
- target={camera_target}
2650
- />
2651
- </T.PerspectiveCamera>
2652
- {/if}
2297
+ {#if diagram_computing}
2298
+ <div class="computing-state">
2299
+ <Spinner
2300
+ text="Computing chemical potential domains..."
2301
+ style="--spinner-size: 1.2em"
2302
+ />
2303
+ </div>
2304
+ {:else if !diagram_data}
2305
+ <div class="error-state" role="alert" aria-live="polite">
2306
+ <p>Cannot compute chemical potential diagram.</p>
2307
+ <p>Need at least 2 elements with elemental reference entries.</p>
2308
+ </div>
2309
+ {:else if mounted && typeof WebGLRenderingContext !== `undefined`}
2310
+ <Canvas
2311
+ createRenderer={(cvs) =>
2312
+ new THREE.WebGLRenderer({
2313
+ canvas: cvs,
2314
+ alpha: true,
2315
+ antialias: true,
2316
+ preserveDrawingBuffer: true,
2317
+ })}
2318
+ >
2319
+ <ChemPotScene3D>
2320
+ {#if camera_projection === `orthographic`}
2321
+ <!-- Orthographic camera matching pymatgen's projection style -->
2322
+ <T.OrthographicCamera
2323
+ makeDefault
2324
+ position={camera_position}
2325
+ zoom={orthographic_zoom}
2326
+ near={0.1}
2327
+ far={data_extent * 10}
2328
+ >
2329
+ <extras.OrbitControls
2330
+ bind:ref={orbit_controls_ref}
2331
+ enableRotate
2332
+ enableZoom
2333
+ enablePan
2334
+ autoRotate={auto_rotate > 0}
2335
+ autoRotateSpeed={auto_rotate}
2336
+ target={camera_target}
2337
+ />
2338
+ </T.OrthographicCamera>
2339
+ {:else}
2340
+ <T.PerspectiveCamera
2341
+ makeDefault
2342
+ position={camera_position}
2343
+ fov={50}
2344
+ near={0.1}
2345
+ far={data_extent * 10}
2346
+ >
2347
+ <extras.OrbitControls
2348
+ bind:ref={orbit_controls_ref}
2349
+ enableRotate
2350
+ enableZoom
2351
+ enablePan
2352
+ autoRotate={auto_rotate > 0}
2353
+ autoRotateSpeed={auto_rotate}
2354
+ target={camera_target}
2355
+ />
2356
+ </T.PerspectiveCamera>
2357
+ {/if}
2653
2358
 
2654
- <!-- Ambient light for visibility -->
2655
- <T.AmbientLight intensity={0.8} />
2656
- <T.DirectionalLight position={[1, 1, 1]} intensity={0.5} />
2359
+ <!-- Ambient light for visibility -->
2360
+ <T.AmbientLight intensity={0.8} />
2361
+ <T.DirectionalLight position={[1, 1, 1]} intensity={0.5} />
2657
2362
 
2658
- <!-- Vertex-colored hull for both plain and colored modes.
2363
+ <!-- Vertex-colored hull for both plain and colored modes.
2659
2364
  {#key domain_colors} forces Threlte to re-create the mesh whenever
2660
2365
  colors change (covers color_mode, color_scale, and data updates),
2661
2366
  since on-demand rendering won't detect mutated vertex color buffers. -->
2662
- {#if colored_hull_geometry}
2663
- {#key domain_colors}
2664
- <T.Mesh geometry={colored_hull_geometry}>
2367
+ {#if colored_hull_geometry}
2368
+ {#key domain_colors}
2369
+ <T.Mesh geometry={colored_hull_geometry}>
2370
+ <T.MeshBasicMaterial
2371
+ vertexColors
2372
+ transparent
2373
+ opacity={color_mode === `none` ? 0.25 : 0.4}
2374
+ side={THREE.DoubleSide}
2375
+ polygonOffset
2376
+ polygonOffsetFactor={1}
2377
+ polygonOffsetUnits={1}
2378
+ />
2379
+ </T.Mesh>
2380
+ {/key}
2381
+ {/if}
2382
+
2383
+ <!-- Domain boundary edges (wireframe on top of opaque fills) -->
2384
+ <T.LineSegments geometry={edge_geometry}>
2385
+ <T.LineBasicMaterial color={0x333333} linewidth={1} />
2386
+ </T.LineSegments>
2387
+
2388
+ <!-- Invisible pick meshes for per-phase hover tooltip -->
2389
+ {#each hover_mesh_data as domain_hover (domain_hover.formula)}
2390
+ <T.Mesh
2391
+ geometry={domain_hover.geometry}
2392
+ onpointerenter={(event: unknown) => handle_phase_hover(domain_hover, event)}
2393
+ onpointermove={(event: unknown) => handle_phase_hover(domain_hover, event)}
2394
+ onpointerdown={(event: unknown) => toggle_phase_lock(domain_hover, event)}
2395
+ onpointerleave={() => {
2396
+ if (!locked_hover_formula && hover_info?.formula === domain_hover.formula) {
2397
+ hover_info = null
2398
+ }
2399
+ }}
2400
+ >
2665
2401
  <T.MeshBasicMaterial
2666
- vertexColors
2667
2402
  transparent
2668
- opacity={color_mode === `none` ? 0.25 : 0.4}
2403
+ opacity={0}
2669
2404
  side={THREE.DoubleSide}
2670
- polygonOffset
2671
- polygonOffsetFactor={1}
2672
- polygonOffsetUnits={1}
2405
+ depthWrite={false}
2673
2406
  />
2674
2407
  </T.Mesh>
2675
- {/key}
2676
- {/if}
2408
+ {/each}
2677
2409
 
2678
- <!-- Domain boundary edges (wireframe on top of opaque fills) -->
2679
- <T.LineSegments geometry={edge_geometry}>
2680
- <T.LineBasicMaterial color={0x333333} linewidth={1} />
2681
- </T.LineSegments>
2682
-
2683
- <!-- Invisible pick meshes for per-phase hover tooltip -->
2684
- {#each hover_mesh_data as domain_hover (domain_hover.formula)}
2685
- <T.Mesh
2686
- geometry={domain_hover.geometry}
2687
- onpointerenter={(event: unknown) => handle_phase_hover(domain_hover, event)}
2688
- onpointermove={(event: unknown) => handle_phase_hover(domain_hover, event)}
2689
- onpointerdown={(event: unknown) => toggle_phase_lock(domain_hover, event)}
2690
- onpointerleave={() => {
2691
- if (
2692
- !locked_hover_formula &&
2693
- hover_info?.formula === domain_hover.formula
2694
- ) {
2695
- hover_info = null
2696
- }
2697
- }}
2698
- >
2699
- <T.MeshBasicMaterial
2700
- transparent
2701
- opacity={0}
2702
- side={THREE.DoubleSide}
2703
- depthWrite={false}
2704
- />
2705
- </T.Mesh>
2706
- {/each}
2707
-
2708
- <!-- Formula overlay meshes (semi-transparent colored fill) -->
2709
- {#each formula_mesh_data as { geometry, color }, mesh_idx (mesh_idx)}
2710
- <T.Mesh {geometry}>
2711
- <T.MeshBasicMaterial
2712
- color={new THREE.Color(color)}
2713
- transparent
2714
- opacity={0.13}
2715
- side={THREE.DoubleSide}
2716
- depthWrite={false}
2717
- />
2718
- </T.Mesh>
2719
- {/each}
2720
-
2721
- <!-- Formula overlay edges (colored, thicker) -->
2722
- {#if draw_formula_lines}
2723
- {#each formula_edge_data as { geometry, color }, edge_idx (edge_idx)}
2724
- <T.LineSegments {geometry}>
2725
- <T.LineBasicMaterial color={new THREE.Color(color)} linewidth={2} />
2726
- </T.LineSegments>
2410
+ <!-- Formula overlay meshes (semi-transparent colored fill) -->
2411
+ {#each formula_mesh_data as { geometry, color }, mesh_idx (mesh_idx)}
2412
+ <T.Mesh {geometry}>
2413
+ <T.MeshBasicMaterial
2414
+ color={new THREE.Color(color)}
2415
+ transparent
2416
+ opacity={0.13}
2417
+ side={THREE.DoubleSide}
2418
+ depthWrite={false}
2419
+ />
2420
+ </T.Mesh>
2727
2421
  {/each}
2728
- {/if}
2729
2422
 
2730
- {#each projection_planes as plane (`${plane.key}-${projection_opacity}`)}
2731
- <T.Mesh position={plane.pos} rotation={plane.rot}>
2732
- <T.PlaneGeometry args={plane.size} />
2733
- <T.MeshBasicMaterial
2734
- color={plane.color}
2735
- opacity={projection_opacity}
2736
- transparent
2737
- side={THREE.DoubleSide}
2738
- depthWrite={false}
2739
- />
2740
- </T.Mesh>
2741
- {/each}
2423
+ <!-- Formula overlay edges (colored, thicker) -->
2424
+ {#if draw_formula_lines}
2425
+ {#each formula_edge_data as { geometry, color }, edge_idx (edge_idx)}
2426
+ <T.LineSegments {geometry}>
2427
+ <T.LineBasicMaterial color={new THREE.Color(color)} linewidth={2} />
2428
+ </T.LineSegments>
2429
+ {/each}
2430
+ {/if}
2742
2431
 
2743
- {#if display.show_bounding_box}
2744
- <T.LineSegments geometry={bounding_box_geometry}>
2745
- <T.LineBasicMaterial color="#666" opacity={0.6} transparent />
2746
- </T.LineSegments>
2747
- {/if}
2432
+ {#each projection_planes as plane (`${plane.key}-${projection_opacity}`)}
2433
+ <T.Mesh position={plane.pos} rotation={plane.rot}>
2434
+ <T.PlaneGeometry args={plane.size} />
2435
+ <T.MeshBasicMaterial
2436
+ color={plane.color}
2437
+ opacity={projection_opacity}
2438
+ transparent
2439
+ side={THREE.DoubleSide}
2440
+ depthWrite={false}
2441
+ />
2442
+ </T.Mesh>
2443
+ {/each}
2748
2444
 
2749
- <!-- Axes, ticks, grid lines, and labels -->
2750
- {#each grid_config as gc (gc.axis)}
2751
- {#if display.show_axes}
2752
- <!-- Main axis line -->
2753
- <T.Line geometry={gc.line_geom}>
2754
- <T.LineBasicMaterial color={gc.color} linewidth={2} />
2755
- </T.Line>
2756
- <!-- Tick marks -->
2757
- {#each gc.tick_geoms as tick_geom, tdx (tdx)}
2758
- <T.Line geometry={tick_geom}>
2759
- <T.LineBasicMaterial color={gc.color} />
2760
- </T.Line>
2761
- {/each}
2445
+ {#if display.show_bounding_box}
2446
+ <T.LineSegments geometry={bounding_box_geometry}>
2447
+ <T.LineBasicMaterial color="#666" opacity={0.6} transparent />
2448
+ </T.LineSegments>
2762
2449
  {/if}
2763
- {#if display.show_grid}
2764
- <!-- Grid lines -->
2765
- {#each gc.grid_geoms as grid_geom, gdx (gdx)}
2766
- <T.Line geometry={grid_geom}>
2767
- <T.LineBasicMaterial color="#888" opacity={0.3} transparent />
2450
+
2451
+ <!-- Axes, ticks, grid lines, and labels -->
2452
+ {#each grid_config as gc (gc.axis)}
2453
+ {#if display.show_axes}
2454
+ <!-- Main axis line -->
2455
+ <T.Line geometry={gc.line_geom}>
2456
+ <T.LineBasicMaterial color={gc.color} linewidth={2} />
2768
2457
  </T.Line>
2769
- {/each}
2770
- {/if}
2771
- {#if display.show_axis_labels}
2772
- <!-- Tick labels (billboarded, always face camera) -->
2773
- {#each gc.tick_labels as tick, tick_idx (tick_idx)}
2458
+ <!-- Tick marks -->
2459
+ {#each gc.tick_geoms as tick_geom, tdx (tdx)}
2460
+ <T.Line geometry={tick_geom}>
2461
+ <T.LineBasicMaterial color={gc.color} />
2462
+ </T.Line>
2463
+ {/each}
2464
+ {/if}
2465
+ {#if display.show_grid}
2466
+ <!-- Grid lines -->
2467
+ {#each gc.grid_geoms as grid_geom, gdx (gdx)}
2468
+ <T.Line geometry={grid_geom}>
2469
+ <T.LineBasicMaterial color="#888" opacity={0.3} transparent />
2470
+ </T.Line>
2471
+ {/each}
2472
+ {/if}
2473
+ {#if display.show_axis_labels}
2474
+ <!-- Tick labels (billboarded, always face camera) -->
2475
+ {#each gc.tick_labels as tick, tick_idx (tick_idx)}
2476
+ <extras.HTML position={tick.pos} center portal={wrapper} zIndexRange={[1, 0]}>
2477
+ <span class="tick-label axis-tick-label">{tick.text}</span>
2478
+ </extras.HTML>
2479
+ {/each}
2480
+ <!-- Axis label -->
2774
2481
  <extras.HTML
2775
- position={tick.pos}
2482
+ position={gc.label_pos}
2776
2483
  center
2777
2484
  portal={wrapper}
2778
2485
  zIndexRange={[1, 0]}
2779
2486
  >
2780
- <span class="tick-label axis-tick-label">{tick.text}</span>
2487
+ <span class="axis-label" style:color={gc.color}>{@html gc.label}</span>
2488
+ </extras.HTML>
2489
+ {/if}
2490
+ {/each}
2491
+
2492
+ <!-- Domain labels -->
2493
+ {#if label_stable}
2494
+ {#each visible_domain_labels as domain (domain.formula)}
2495
+ <extras.HTML
2496
+ position={domain.position}
2497
+ center
2498
+ portal={wrapper}
2499
+ zIndexRange={[5, 5]}
2500
+ >
2501
+ <span
2502
+ class="domain-label"
2503
+ style:font-size="{(domain.label_font_size * zoom_scale).toFixed(1)}px"
2504
+ >
2505
+ {#each formula_label_segments(domain.formula) as segment}
2506
+ <span class:formula-subscript={segment.subscript}>{segment.text}</span>
2507
+ {/each}
2508
+ </span>
2781
2509
  </extras.HTML>
2782
2510
  {/each}
2783
- <!-- Axis label -->
2784
- <extras.HTML
2785
- position={gc.label_pos}
2786
- center
2787
- portal={wrapper}
2788
- zIndexRange={[1, 0]}
2789
- >
2790
- <span class="axis-label" style:color={gc.color}>{@html gc.label}</span>
2791
- </extras.HTML>
2792
2511
  {/if}
2793
- {/each}
2794
-
2795
- <!-- Domain labels -->
2796
- {#if label_stable}
2797
- {#each visible_domain_labels as domain (domain.formula)}
2798
- <extras.HTML
2799
- position={domain.position}
2800
- center
2801
- portal={wrapper}
2802
- zIndexRange={[5, 5]}
2803
- >
2804
- <span
2805
- class="domain-label"
2806
- style:font-size="{(domain.label_font_size * zoom_scale).toFixed(1)}px"
2807
- >
2808
- {#each formula_label_segments(domain.formula) as segment}
2809
- <span class:formula-subscript={segment.subscript}>{segment.text}</span>
2810
- {/each}
2811
- </span>
2812
- </extras.HTML>
2512
+ </ChemPotScene3D>
2513
+ </Canvas>
2514
+ <!-- Color bar for continuous modes -->
2515
+ {#if color_mode !== `none` && color_mode !== `arity` && color_range}
2516
+ {@const color_bar_config = get_chempot_color_bar_config(
2517
+ color_scale,
2518
+ reverse_color_scale,
2519
+ )}
2520
+ <ColorBar
2521
+ title={color_range.label}
2522
+ range={[color_range.min, color_range.max]}
2523
+ color_scale_fn={color_bar_config.color_scale_fn}
2524
+ color_scale_domain={color_bar_config.color_scale_domain}
2525
+ wrapper_style="position: absolute; bottom: 16px; left: 1em; width: 200px; z-index: 10;"
2526
+ bar_style="height: 12px;"
2527
+ title_style="margin-bottom: 4px;"
2528
+ />
2529
+ {/if}
2530
+ <!-- Categorical legend for arity mode -->
2531
+ {#if color_mode === `arity`}
2532
+ <div class="arity-legend">
2533
+ {#each arity_legend_labels as label, idx (label)}
2534
+ <span>
2535
+ <span style:background={arity_colors[idx]}></span>
2536
+ {label}
2537
+ </span>
2813
2538
  {/each}
2814
- {/if}
2815
- </ChemPotScene3D>
2816
- </Canvas>
2817
- <!-- Color bar for continuous modes -->
2818
- {#if color_mode !== `none` && color_mode !== `arity` && color_range}
2819
- {@const color_bar_config = get_chempot_color_bar_config(
2820
- color_scale,
2821
- reverse_color_scale,
2822
- )}
2823
- <ColorBar
2824
- title={color_range.label}
2825
- range={[color_range.min, color_range.max]}
2826
- color_scale_fn={color_bar_config.color_scale_fn}
2827
- color_scale_domain={color_bar_config.color_scale_domain}
2828
- wrapper_style="position: absolute; bottom: 16px; left: 1em; width: 200px; z-index: 10;"
2829
- bar_style="height: 12px;"
2830
- title_style="margin-bottom: 4px;"
2831
- />
2832
- {/if}
2833
- <!-- Categorical legend for arity mode -->
2834
- {#if color_mode === `arity`}
2835
- <div class="arity-legend">
2836
- {#each arity_legend_labels as label, idx (label)}
2837
- <span>
2838
- <span style:background={arity_colors[idx]}></span>
2839
- {label}
2840
- </span>
2841
- {/each}
2842
- </div>
2843
- {/if}
2844
- {/if}
2845
- {#if render_local_tooltip && show_tooltip && hover_info?.view === `3d`}
2846
- <aside
2847
- bind:this={tooltip_el}
2848
- class="phase-tooltip"
2849
- style:left="{tooltip_pos.x}px"
2850
- style:top="{tooltip_pos.y}px"
2851
- >
2852
- <h4>
2853
- {#each formula_label_segments(hover_info.formula) as segment}
2854
- <span class:formula-subscript={segment.subscript}>{segment.text}</span>
2855
- {/each}
2856
- </h4>
2857
- {#if locked_hover_formula === hover_info.formula}
2858
- <p>Pinned · Press Esc to unlock</p>
2539
+ </div>
2859
2540
  {/if}
2860
- <p>
2861
- Vertices: {hover_info.n_vertices} · Edges: {hover_info.n_edges} · Points:
2862
- {hover_info.n_points}
2863
- </p>
2864
- <p>
2865
- Entries: {hover_info.matching_entry_count}
2866
- {#if hover_info.min_energy_per_atom !== null &&
2867
- hover_info.max_energy_per_atom !== null}
2868
- · E/atom: {format_num(hover_info.min_energy_per_atom, `.4~g`)}
2869
- to {format_num(hover_info.max_energy_per_atom, `.4~g`)} eV
2541
+ {/if}
2542
+ {#if render_local_tooltip && show_tooltip && hover_info?.view === `3d`}
2543
+ <aside
2544
+ bind:this={tooltip_el}
2545
+ class="phase-tooltip"
2546
+ style:left="{tooltip_pos.x}px"
2547
+ style:top="{tooltip_pos.y}px"
2548
+ >
2549
+ <h4>
2550
+ {#each formula_label_segments(hover_info.formula) as segment}
2551
+ <span class:formula-subscript={segment.subscript}>{segment.text}</span>
2552
+ {/each}
2553
+ </h4>
2554
+ {#if locked_hover_formula === hover_info.formula}
2555
+ <p>Pinned · Press Esc to unlock</p>
2870
2556
  {/if}
2871
- </p>
2872
- {#if tooltip_detail_level === `detailed`}
2873
- <h5 id="axis-ranges">Axis ranges</h5>
2874
- {#each hover_info.axis_ranges as axis_range (axis_range.element)}
2875
- <p>
2876
- {axis_range.element}: {format_num(axis_range.min_val, `.4~g`)} to
2877
- {format_num(axis_range.max_val, `.4~g`)} eV
2878
- </p>
2879
- {/each}
2880
2557
  <p>
2881
- Centroid: ({
2882
- hover_info.ann_loc.map((value) => format_num(value, `.3~g`)).join(
2883
- `, `,
2884
- )
2885
- })
2558
+ Vertices: {hover_info.n_vertices} · Edges: {hover_info.n_edges} · Points:
2559
+ {hover_info.n_points}
2560
+ </p>
2561
+ <p>
2562
+ Entries: {hover_info.matching_entry_count}
2563
+ {#if hover_info.min_energy_per_atom !== null && hover_info.max_energy_per_atom !== null}
2564
+ · E/atom: {format_num(hover_info.min_energy_per_atom, `.4~g`)}
2565
+ to {format_num(hover_info.max_energy_per_atom, `.4~g`)} eV
2566
+ {/if}
2886
2567
  </p>
2887
- {#if hover_info.touches_limits.length > 0}
2888
- <h5 id="touches-bounds">Touches bounds</h5>
2889
- <p>{hover_info.touches_limits.join(`, `)}</p>
2568
+ {#if tooltip_detail_level === `detailed`}
2569
+ <h5 id="axis-ranges">Axis ranges</h5>
2570
+ {#each hover_info.axis_ranges as axis_range (axis_range.element)}
2571
+ <p>
2572
+ {axis_range.element}: {format_num(axis_range.min_val, `.4~g`)} to
2573
+ {format_num(axis_range.max_val, `.4~g`)} eV
2574
+ </p>
2575
+ {/each}
2576
+ <p>
2577
+ Centroid: ({hover_info.ann_loc
2578
+ .map((value) => format_num(value, `.3~g`))
2579
+ .join(`, `)})
2580
+ </p>
2581
+ {#if hover_info.touches_limits.length > 0}
2582
+ <h5 id="touches-bounds">Touches bounds</h5>
2583
+ <p>{hover_info.touches_limits.join(`, `)}</p>
2584
+ {/if}
2890
2585
  {/if}
2891
- {/if}
2892
- </aside>
2893
- {/if}
2586
+ </aside>
2587
+ {/if}
2894
2588
  </div>
2895
2589
  </div>
2896
2590
 
@@ -2963,23 +2657,6 @@
2963
2657
  gap: 4pt;
2964
2658
  font-size: 0.9em;
2965
2659
  }
2966
- .chempot-diagram-3d :global(.export-row) {
2967
- display: flex;
2968
- flex-wrap: wrap;
2969
- gap: 4pt 10pt;
2970
- margin: 0 0 4pt;
2971
- }
2972
- .chempot-diagram-3d :global(.export-row > label) {
2973
- margin: 0;
2974
- }
2975
- .chempot-diagram-3d :global(.export-row button) {
2976
- width: 1.4em;
2977
- height: 1.4em;
2978
- padding: 0;
2979
- display: inline-flex;
2980
- align-items: center;
2981
- justify-content: center;
2982
- }
2983
2660
  .chempot-diagram-3d :global(.chempot-checks) {
2984
2661
  display: flex;
2985
2662
  flex-wrap: wrap;
@@ -2996,8 +2673,10 @@
2996
2673
  }
2997
2674
  .chempot-diagram-3d :global(.projection-controls .pane-row) {
2998
2675
  display: grid;
2999
- grid-template-columns:
3000
- auto minmax(4.5em, 1fr) auto minmax(4.5em, 1fr) auto minmax(4.5em, 1fr);
2676
+ grid-template-columns: auto minmax(4.5em, 1fr) auto minmax(4.5em, 1fr) auto minmax(
2677
+ 4.5em,
2678
+ 1fr
2679
+ );
3001
2680
  align-items: center;
3002
2681
  gap: 3pt;
3003
2682
  }
@@ -3137,10 +2816,7 @@
3137
2816
  .phase-tooltip {
3138
2817
  position: absolute;
3139
2818
  max-width: min(32rem, 92vw);
3140
- background: var(
3141
- --tooltip-bg,
3142
- light-dark(rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.9))
3143
- );
2819
+ background: var(--tooltip-bg, light-dark(rgba(255, 255, 255, 0.95), rgba(0, 0, 0, 0.9)));
3144
2820
  color: var(--tooltip-text, var(--text-color, #222));
3145
2821
  border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
3146
2822
  border-radius: 6px;