matterviz 0.4.0 → 0.4.1

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 (326) hide show
  1. package/dist/brillouin/BrillouinZone.svelte +68 -145
  2. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -14
  3. package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
  4. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  7. package/dist/brillouin/BrillouinZoneScene.svelte +49 -203
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  9. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  10. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  11. package/dist/brillouin/compute.d.ts +2 -0
  12. package/dist/brillouin/compute.js +80 -77
  13. package/dist/brillouin/geometry.d.ts +8 -0
  14. package/dist/brillouin/geometry.js +57 -0
  15. package/dist/brillouin/index.d.ts +2 -0
  16. package/dist/brillouin/index.js +2 -0
  17. package/dist/brillouin/types.d.ts +2 -2
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +100 -191
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +176 -464
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  23. package/dist/chempot-diagram/color.d.ts +3 -6
  24. package/dist/chempot-diagram/color.js +5 -5
  25. package/dist/chempot-diagram/compute.d.ts +3 -3
  26. package/dist/chempot-diagram/compute.js +3 -1
  27. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  28. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  29. package/dist/chempot-diagram/export.d.ts +47 -0
  30. package/dist/chempot-diagram/export.js +133 -0
  31. package/dist/chempot-diagram/index.d.ts +1 -0
  32. package/dist/chempot-diagram/index.js +1 -0
  33. package/dist/chempot-diagram/pointer.d.ts +0 -10
  34. package/dist/chempot-diagram/pointer.js +4 -4
  35. package/dist/chempot-diagram/types.d.ts +3 -3
  36. package/dist/colors/index.js +2 -2
  37. package/dist/composition/FormulaFilter.svelte +6 -5
  38. package/dist/composition/PieChart.svelte +5 -5
  39. package/dist/composition/chem-sys.js +3 -2
  40. package/dist/composition/format.js +3 -2
  41. package/dist/composition/parse.d.ts +0 -1
  42. package/dist/composition/parse.js +17 -19
  43. package/dist/controls.d.ts +1 -0
  44. package/dist/controls.js +0 -1
  45. package/dist/convex-hull/ConvexHull.svelte +8 -10
  46. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  47. package/dist/convex-hull/ConvexHull2D.svelte +94 -175
  48. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHull3D.svelte +176 -680
  50. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  51. package/dist/convex-hull/ConvexHull4D.svelte +180 -680
  52. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  53. package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
  54. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  55. package/dist/convex-hull/ConvexHullControls.svelte +88 -7
  56. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  57. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  58. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  59. package/dist/convex-hull/ConvexHullStats.svelte +29 -168
  60. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  61. package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
  62. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  63. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  64. package/dist/convex-hull/barycentric-coords.js +6 -33
  65. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  66. package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
  67. package/dist/convex-hull/helpers.d.ts +39 -7
  68. package/dist/convex-hull/helpers.js +154 -69
  69. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  70. package/dist/convex-hull/hull-state.svelte.js +124 -0
  71. package/dist/convex-hull/index.d.ts +9 -7
  72. package/dist/convex-hull/index.js +7 -2
  73. package/dist/convex-hull/thermodynamics.js +91 -920
  74. package/dist/convex-hull/types.d.ts +12 -4
  75. package/dist/convex-hull/types.js +12 -0
  76. package/dist/coordination/CoordinationBarPlot.svelte +4 -11
  77. package/dist/element/BohrAtom.svelte +2 -1
  78. package/dist/element/ElementTile.svelte.d.ts +1 -1
  79. package/dist/element/index.d.ts +4 -0
  80. package/dist/element/index.js +18 -0
  81. package/dist/feedback/DragOverlay.svelte +3 -1
  82. package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
  83. package/dist/feedback/StatusMessage.svelte +13 -3
  84. package/dist/fermi-surface/FermiSurface.svelte +67 -146
  85. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
  86. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -224
  88. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  89. package/dist/fermi-surface/compute.js +11 -10
  90. package/dist/fermi-surface/export.js +4 -15
  91. package/dist/fermi-surface/index.d.ts +0 -1
  92. package/dist/fermi-surface/index.js +0 -1
  93. package/dist/fermi-surface/parse.d.ts +1 -1
  94. package/dist/fermi-surface/parse.js +64 -75
  95. package/dist/fermi-surface/types.d.ts +2 -2
  96. package/dist/heatmap-matrix/HeatmapMatrix.svelte +55 -40
  97. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
  98. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
  99. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  100. package/dist/heatmap-matrix/index.d.ts +3 -2
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/io/ExportPane.svelte +166 -0
  104. package/dist/io/ExportPane.svelte.d.ts +17 -0
  105. package/dist/io/decompress.js +1 -2
  106. package/dist/io/export.d.ts +5 -1
  107. package/dist/io/export.js +32 -28
  108. package/dist/io/fetch.d.ts +2 -1
  109. package/dist/io/file-drop.d.ts +7 -0
  110. package/dist/io/file-drop.js +13 -0
  111. package/dist/io/index.d.ts +2 -0
  112. package/dist/io/index.js +10 -0
  113. package/dist/io/types.d.ts +13 -0
  114. package/dist/isosurface/parse.js +46 -44
  115. package/dist/labels.js +1 -1
  116. package/dist/layout/FullscreenButton.svelte +33 -0
  117. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  118. package/dist/layout/FullscreenToggle.svelte +8 -14
  119. package/dist/layout/ViewerChrome.svelte +116 -0
  120. package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
  121. package/dist/layout/fullscreen.d.ts +4 -0
  122. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  123. package/dist/layout/fullscreen.svelte.js +37 -0
  124. package/dist/layout/index.d.ts +3 -0
  125. package/dist/layout/index.js +3 -0
  126. package/dist/math.d.ts +7 -3
  127. package/dist/math.js +18 -21
  128. package/dist/overlays/index.d.ts +4 -0
  129. package/dist/periodic-table/PeriodicTable.svelte +9 -8
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  131. package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  133. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  137. package/dist/phase-diagram/colors.js +1 -1
  138. package/dist/phase-diagram/parse.d.ts +2 -1
  139. package/dist/plot/bar/BarPlot.svelte +79 -316
  140. package/dist/plot/bar/BarPlot.svelte.d.ts +7 -15
  141. package/dist/plot/bar/BarPlotControls.svelte.d.ts +1 -1
  142. package/dist/plot/bar/SpacegroupBarPlot.svelte +2 -1
  143. package/dist/plot/box/BoxPlot.svelte +76 -246
  144. package/dist/plot/box/BoxPlot.svelte.d.ts +4 -3
  145. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  146. package/dist/plot/box/Violin.svelte.d.ts +1 -1
  147. package/dist/plot/box/box-plot.d.ts +3 -2
  148. package/dist/plot/box/box-plot.js +5 -2
  149. package/dist/plot/box/kde.d.ts +2 -1
  150. package/dist/plot/box/kde.js +4 -4
  151. package/dist/plot/core/auto-place.d.ts +1 -1
  152. package/dist/plot/core/auto-place.js +4 -1
  153. package/dist/plot/core/components/ColorBar.svelte +5 -5
  154. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  155. package/dist/plot/core/components/Line.svelte +3 -2
  156. package/dist/plot/core/components/Line.svelte.d.ts +3 -2
  157. package/dist/plot/core/components/PlotAxis.svelte +2 -1
  158. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  159. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  160. package/dist/plot/core/components/ReferenceLine3D.svelte +2 -2
  161. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  162. package/dist/plot/core/components/ReferencePlane.svelte +2 -2
  163. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  164. package/dist/plot/core/data-cleaning.js +18 -18
  165. package/dist/plot/core/fill-utils.d.ts +4 -3
  166. package/dist/plot/core/fill-utils.js +6 -3
  167. package/dist/plot/core/interactions.d.ts +5 -1
  168. package/dist/plot/core/interactions.js +14 -0
  169. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  170. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  171. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  172. package/dist/plot/core/placed-tween.svelte.js +68 -0
  173. package/dist/plot/core/reference-line.d.ts +10 -10
  174. package/dist/plot/core/reference-line.js +6 -6
  175. package/dist/plot/core/scales.d.ts +17 -25
  176. package/dist/plot/core/scales.js +10 -8
  177. package/dist/plot/core/svg.d.ts +2 -1
  178. package/dist/plot/core/types.d.ts +18 -7
  179. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  180. package/dist/plot/core/utils/label-placement.js +3 -3
  181. package/dist/plot/core/utils.d.ts +2 -1
  182. package/dist/plot/histogram/Histogram.svelte +77 -314
  183. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  184. package/dist/plot/sankey/Sankey.svelte +2 -5
  185. package/dist/plot/sankey/Sankey.svelte.d.ts +1 -1
  186. package/dist/plot/sankey/sankey.js +3 -1
  187. package/dist/plot/scatter/BinnedScatterPlot.svelte +3 -5
  188. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +4 -4
  189. package/dist/plot/scatter/ScatterPlot.svelte +160 -450
  190. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +7 -15
  191. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  192. package/dist/plot/scatter/binned-scatter-types.d.ts +4 -11
  193. package/dist/plot/scatter/index.d.ts +1 -1
  194. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +15 -26
  195. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  196. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +9 -10
  197. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  198. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +122 -121
  199. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  200. package/dist/plot/scatter-3d/Surface3D.svelte +6 -5
  201. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  202. package/dist/plot/sunburst/Sunburst.svelte +16 -20
  203. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  204. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +1 -1
  205. package/dist/plot/sunburst/sunburst.js +4 -1
  206. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  207. package/dist/sanitize.js +13 -2
  208. package/dist/scene/SceneCamera.svelte +62 -0
  209. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  210. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  211. package/dist/scene/bind-renderer.svelte.js +14 -0
  212. package/dist/scene/index.d.ts +4 -0
  213. package/dist/scene/index.js +5 -0
  214. package/dist/scene/props.js +52 -0
  215. package/dist/scene/types.d.ts +26 -0
  216. package/dist/scene/types.js +1 -0
  217. package/dist/settings.d.ts +14 -2
  218. package/dist/settings.js +59 -1
  219. package/dist/spectral/Bands.svelte +8 -7
  220. package/dist/spectral/Bands.svelte.d.ts +3 -2
  221. package/dist/spectral/BandsAndDos.svelte +22 -24
  222. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  223. package/dist/spectral/Dos.svelte +5 -4
  224. package/dist/spectral/Dos.svelte.d.ts +2 -1
  225. package/dist/spectral/helpers.d.ts +6 -6
  226. package/dist/spectral/helpers.js +43 -37
  227. package/dist/state.svelte.d.ts +0 -7
  228. package/dist/state.svelte.js +0 -6
  229. package/dist/structure/Arrow.svelte +2 -4
  230. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  231. package/dist/structure/CanvasTooltip.svelte +1 -0
  232. package/dist/structure/CellSelect.svelte +11 -3
  233. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  234. package/dist/structure/Lattice.svelte +2 -2
  235. package/dist/structure/Structure.svelte +291 -355
  236. package/dist/structure/Structure.svelte.d.ts +5 -15
  237. package/dist/structure/StructureControls.svelte +217 -2
  238. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  239. package/dist/structure/StructureExportPane.svelte +54 -156
  240. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  241. package/dist/structure/StructureInfoPane.svelte +5 -3
  242. package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
  243. package/dist/structure/StructureScene.svelte +365 -198
  244. package/dist/structure/StructureScene.svelte.d.ts +22 -20
  245. package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
  246. package/dist/structure/{label-placement.js → atom-label-placement.js} +12 -2
  247. package/dist/structure/atom-properties.d.ts +1 -1
  248. package/dist/structure/atom-properties.js +11 -16
  249. package/dist/structure/bond-order-perception.js +2 -4
  250. package/dist/structure/bonding.d.ts +3 -0
  251. package/dist/structure/bonding.js +91 -48
  252. package/dist/structure/export.d.ts +24 -4
  253. package/dist/structure/export.js +64 -122
  254. package/dist/structure/index.d.ts +2 -0
  255. package/dist/structure/index.js +2 -0
  256. package/dist/structure/parse.d.ts +3 -2
  257. package/dist/structure/parse.js +333 -370
  258. package/dist/structure/partial-occupancy.d.ts +0 -1
  259. package/dist/structure/partial-occupancy.js +1 -1
  260. package/dist/structure/pbc.d.ts +1 -1
  261. package/dist/structure/pbc.js +186 -13
  262. package/dist/structure/polyhedra.d.ts +41 -0
  263. package/dist/structure/polyhedra.js +602 -0
  264. package/dist/structure/site.d.ts +4 -0
  265. package/dist/structure/site.js +1 -0
  266. package/dist/structure/supercell.js +3 -2
  267. package/dist/structure/validation.js +5 -6
  268. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  269. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  270. package/dist/symmetry/SymmetryElements.svelte +354 -0
  271. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  272. package/dist/symmetry/SymmetryStats.svelte +111 -6
  273. package/dist/symmetry/WyckoffTable.svelte +68 -7
  274. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  275. package/dist/symmetry/cell-transform.js +7 -14
  276. package/dist/symmetry/index.d.ts +14 -4
  277. package/dist/symmetry/index.js +301 -80
  278. package/dist/symmetry/spacegroups.d.ts +5 -1
  279. package/dist/symmetry/spacegroups.js +15 -1
  280. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  281. package/dist/symmetry/symmetry-elements.js +521 -0
  282. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  283. package/dist/symmetry/wyckoff-db.js +87 -0
  284. package/dist/table/HeatmapTable.svelte +4 -15
  285. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  286. package/dist/trajectory/Trajectory.svelte +58 -61
  287. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  288. package/dist/trajectory/TrajectoryExportPane.svelte +15 -24
  289. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  290. package/dist/trajectory/TrajectoryInfoPane.svelte +3 -2
  291. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  292. package/dist/trajectory/constants.js +6 -2
  293. package/dist/trajectory/extract.js +17 -37
  294. package/dist/trajectory/format-detect.d.ts +0 -1
  295. package/dist/trajectory/format-detect.js +3 -9
  296. package/dist/trajectory/frame-reader.d.ts +0 -1
  297. package/dist/trajectory/frame-reader.js +62 -128
  298. package/dist/trajectory/helpers.d.ts +10 -2
  299. package/dist/trajectory/helpers.js +56 -36
  300. package/dist/trajectory/parse/ase.d.ts +9 -1
  301. package/dist/trajectory/parse/ase.js +47 -32
  302. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  303. package/dist/trajectory/parse/diagnostics.js +14 -0
  304. package/dist/trajectory/parse/index.d.ts +1 -1
  305. package/dist/trajectory/parse/index.js +54 -102
  306. package/dist/trajectory/parse/lammps.d.ts +0 -2
  307. package/dist/trajectory/parse/lammps.js +8 -6
  308. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  309. package/dist/trajectory/parse/pymatgen.js +74 -0
  310. package/dist/trajectory/parse/vasp.js +4 -3
  311. package/dist/trajectory/parse/xyz.d.ts +9 -21
  312. package/dist/trajectory/parse/xyz.js +28 -33
  313. package/dist/trajectory/plotting.d.ts +0 -1
  314. package/dist/trajectory/plotting.js +3 -100
  315. package/dist/utils.d.ts +1 -0
  316. package/dist/utils.js +1 -1
  317. package/dist/xrd/XrdPlot.svelte +14 -29
  318. package/dist/xrd/broadening.d.ts +2 -1
  319. package/dist/xrd/calc-xrd.js +6 -11
  320. package/dist/xrd/index.d.ts +2 -2
  321. package/package.json +29 -16
  322. package/dist/element/data.json +0 -11864
  323. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  324. package/dist/fermi-surface/marching-cubes.js +0 -2
  325. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  326. package/dist/plot/core/hover-lock.svelte.js +0 -45
@@ -9,14 +9,8 @@
9
9
  } from '../colors'
10
10
  import { normalize_show_controls } from '../controls'
11
11
  import { sanitize_html } from '../sanitize'
12
- import { ClickFeedback, DragOverlay, Spinner } from '../feedback'
13
- import Icon from '../Icon.svelte'
14
- import {
15
- set_fullscreen_bg,
16
- setup_fullscreen_effect,
17
- toggle_fullscreen,
18
- } from '../layout'
19
- import { ColorBar, PlotTooltip } from '../plot'
12
+ import { Spinner } from '../feedback'
13
+ import { ColorBar } from '../plot'
20
14
  import { create_pulse_animation } from '../effects.svelte'
21
15
  import { DEFAULTS } from '../settings'
22
16
  import type { AnyStructure } from '../structure'
@@ -25,38 +19,41 @@
25
19
  compute_4d_coords,
26
20
  TETRAHEDRON_VERTICES,
27
21
  } from './barycentric-coords'
28
- import ConvexHullControls from './ConvexHullControls.svelte'
29
- import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
30
- import ConvexHullTooltip from './ConvexHullTooltip.svelte'
22
+ import { create_canvas_interactions } from './canvas-interactions.svelte'
23
+ import ConvexHullChrome from './ConvexHullChrome.svelte'
31
24
  import GasPressureControls from './GasPressureControls.svelte'
32
25
  import * as helpers from './helpers'
26
+ import { create_hull_data_pipeline } from './hull-state.svelte'
33
27
  import type { BaseConvexHullProps, Hull3DProps } from './index'
34
28
  import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
35
- import StructurePopup from './StructurePopup.svelte'
36
29
  import TemperatureSlider from './TemperatureSlider.svelte'
37
30
  import type { Point4D } from './thermodynamics'
38
31
  import * as thermo from './thermodynamics'
39
32
  import type {
40
33
  ConvexHullEntry,
41
34
  HighlightStyle,
42
- HoverData3D,
43
35
  HullFaceColorMode,
36
+ PhaseData,
44
37
  } from './types'
38
+ import { MAGNETIC_ORDERING_CATEGORY } from './types'
45
39
  import { compute_hull_stability } from './helpers'
46
40
 
47
41
  let {
48
42
  entries = [],
49
43
  controls = {},
50
44
  config = {},
45
+ show_controls,
51
46
  on_point_click,
52
47
  on_point_hover,
53
48
  fullscreen = $bindable(DEFAULTS.convex_hull.quaternary.fullscreen),
54
- enable_fullscreen = true,
49
+ fullscreen_toggle = true,
55
50
  enable_info_pane = true,
56
51
  wrapper = $bindable(),
57
52
  label_threshold = 50,
58
53
  show_stable = $bindable(DEFAULTS.convex_hull.quaternary.show_stable),
59
54
  show_unstable = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable),
55
+ entry_category = MAGNETIC_ORDERING_CATEGORY,
56
+ hidden_categories = $bindable([]),
60
57
  show_hull_faces = $bindable(DEFAULTS.convex_hull.quaternary.show_hull_faces),
61
58
  hull_face_opacity = $bindable(DEFAULTS.convex_hull.quaternary.hull_face_opacity),
62
59
  hull_face_color_mode = $bindable(
@@ -68,7 +65,7 @@
68
65
  DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName,
69
66
  ),
70
67
  info_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.info_pane_open),
71
- legend_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open),
68
+ controls_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open),
72
69
  max_hull_dist_show_phases = $bindable(
73
70
  DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
74
71
  ),
@@ -104,7 +101,7 @@
104
101
  } = $props()
105
102
 
106
103
  const merged_controls = $derived({ ...default_controls, ...controls })
107
- const controls_config = $derived(normalize_show_controls(merged_controls.show))
104
+ const controls_config = $derived(normalize_show_controls(show_controls))
108
105
  const merged_config = $derived({
109
106
  ...default_hull_config,
110
107
  ...config,
@@ -117,87 +114,36 @@
117
114
  $effect(() => watch_dark_mode((dark) => dark_mode = dark))
118
115
  const text_color = $derived(helpers.get_canvas_text_color(dark_mode))
119
116
 
120
- // Temperature-dependent free energy support
121
- const { has_temp_data, available_temperatures } = $derived(
122
- helpers.analyze_temperature_data(entries),
123
- )
124
-
125
- // Initialize or reset temperature when it's undefined or no longer valid
126
- $effect(() => {
127
- if (
128
- has_temp_data &&
129
- available_temperatures.length > 0 &&
130
- (temperature === undefined || !available_temperatures.includes(temperature))
131
- ) temperature = available_temperatures[0]
132
- })
133
-
134
- // Filter entries by temperature when in temperature mode
135
- const temp_filtered_entries = $derived(
136
- has_temp_data && temperature !== undefined
137
- ? helpers.filter_entries_at_temperature(entries, temperature, {
138
- interpolate: interpolate_temperature,
139
- max_interpolation_gap,
140
- })
141
- : entries,
142
- )
143
-
144
- // Gas-dependent chemical potential support (corrections based on T, P)
145
- const {
146
- entries: gas_corrected_entries,
147
- analysis: gas_analysis,
148
- merged_config: merged_gas_config,
149
- } = $derived(
150
- helpers.get_gas_corrected_entries(
151
- temp_filtered_entries,
152
- gas_config,
153
- gas_pressures,
154
- temperature ?? helpers.DEFAULT_GAS_TEMP,
155
- ),
156
- )
157
-
158
- let { // Compute energy mode information
159
- has_precomputed_e_form,
160
- has_precomputed_hull,
161
- can_compute_e_form,
162
- can_compute_hull,
163
- energy_mode,
164
- unary_refs,
165
- } = $derived(
166
- helpers.compute_energy_mode_info(
167
- gas_corrected_entries,
168
- thermo.find_lowest_energy_unary_refs,
169
- energy_source_mode,
170
- ),
171
- )
172
-
173
- const effective_entries = $derived(
174
- helpers.get_effective_entries(
175
- gas_corrected_entries,
176
- energy_mode,
177
- unary_refs,
178
- thermo.compute_e_form_per_atom,
179
- ),
180
- )
181
-
182
- // Process convex hull data with unified PhaseData interface using effective entries
183
- const pd_data = $derived(thermo.process_hull_entries(effective_entries))
184
-
185
- // Pre-compute polymorph stats once for O(1) tooltip lookups
186
- const polymorph_stats_map = $derived(
187
- helpers.compute_all_polymorph_stats(effective_entries),
188
- )
189
-
190
- const elements = $derived.by(() => {
191
- if (pd_data.elements.length > 4) {
192
- console.error(
193
- `ConvexHull4D: Dataset contains ${pd_data.elements.length} elements, but quaternary diagrams require exactly 4. Found: [${
194
- pd_data.elements.join(`, `)
195
- }]`,
196
- )
197
- return []
198
- }
199
- return pd_data.elements
117
+ // Shared reactive data pipeline (temperature → gas → energy mode → hull data → threshold)
118
+ // Explicit generic breaks the circular type inference through the all_enriched_entries thunk
119
+ const hull_data = create_hull_data_pipeline<ConvexHullEntry>({
120
+ dim: 4,
121
+ entries: () => entries,
122
+ temperature: () => temperature,
123
+ interpolate_temperature: () => interpolate_temperature,
124
+ max_interpolation_gap: () => max_interpolation_gap,
125
+ gas_config: () => gas_config,
126
+ gas_pressures: () => gas_pressures,
127
+ energy_source_mode: () => energy_source_mode,
128
+ all_enriched_entries: () => all_enriched_entries,
129
+ max_hull_dist_show_phases: () => max_hull_dist_show_phases,
130
+ show_stable: () => show_stable,
131
+ show_unstable: () => show_unstable,
132
+ entry_category: () => entry_category,
133
+ hidden_categories: () => hidden_categories,
134
+ // Always include stable entries and elemental reference points
135
+ keep_plot_entry: (entry, max_dist) =>
136
+ helpers.entry_is_stable(entry) ||
137
+ (typeof entry.e_above_hull === `number` && entry.e_above_hull <= max_dist),
138
+ set_temperature: (next_temp) => temperature = next_temp,
139
+ set_max_hull_dist_show_phases: (value) => max_hull_dist_show_phases = value,
140
+ set_stable_entries: (value) => stable_entries = value,
141
+ set_unstable_entries: (value) => unstable_entries = value,
200
142
  })
143
+ const merged_gas_config = $derived(hull_data.merged_gas_config)
144
+ const pd_data = $derived(hull_data.pd_data)
145
+ const elements = $derived(hull_data.elements)
146
+ const plot_entries = $derived(hull_data.plot_entries)
201
147
 
202
148
  // Compute 4D hull for visualization (always compute when we have formation energies)
203
149
  const hull_4d = $derived.by(() => {
@@ -234,11 +180,12 @@
234
180
  })
235
181
 
236
182
  // Enrich coords with e_above_hull (before filtering)
237
- const all_enriched_entries = $derived.by(() => {
183
+ // Explicit return type breaks circular type inference with the hull_data pipeline
184
+ const all_enriched_entries = $derived.by((): ConvexHullEntry[] => {
238
185
  if (elements.length !== 4) return []
239
186
  try {
240
187
  const coords = compute_4d_coords(pd_data.entries, elements)
241
- if (energy_mode !== `on-the-fly` || hull_4d.length === 0) return coords
188
+ if (hull_data.energy_mode !== `on-the-fly` || hull_4d.length === 0) return coords
242
189
 
243
190
  // Build 4D points, tracking original indices for mapping hull distances back
244
191
  const valid = coords.flatMap((entry, idx) => {
@@ -267,51 +214,8 @@
267
214
  }
268
215
  })
269
216
 
270
- // Auto threshold: show all for few entries, use default for many, interpolate between
271
- const max_hull_dist_in_data = $derived(
272
- helpers.calc_max_hull_dist_in_data(all_enriched_entries),
273
- )
274
- const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
275
- all_enriched_entries.length,
276
- max_hull_dist_in_data,
277
- DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
278
- ))
279
-
280
- const next_auto_threshold = helpers.auto_threshold_reset(
281
- DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
282
- )
283
- $effect(() => {
284
- max_hull_dist_show_phases = next_auto_threshold(
285
- entries,
286
- max_hull_dist_show_phases,
287
- auto_default_threshold,
288
- ) ?? max_hull_dist_show_phases
289
- })
290
-
291
- // Filter by threshold; visibility is a view predicate, not entry state.
292
- const plot_entries = $derived(
293
- all_enriched_entries.filter((entry) => {
294
- // Always include stable entries and elemental reference points
295
- if (helpers.entry_is_stable(entry)) return true
296
- return typeof entry.e_above_hull === `number` &&
297
- entry.e_above_hull <= max_hull_dist_show_phases
298
- }),
299
- )
300
- const visible_entries = $derived(helpers.visible_entries(
301
- plot_entries,
302
- show_stable,
303
- show_unstable,
304
- ))
305
-
306
- // Stable and unstable entries exposed as bindable props
307
- $effect(() => {
308
- stable_entries = plot_entries.filter(helpers.entry_is_stable)
309
- unstable_entries = plot_entries.filter(helpers.entry_is_unstable)
310
- })
311
-
312
217
  let canvas: HTMLCanvasElement | undefined = undefined
313
218
  let ctx: CanvasRenderingContext2D | null = null
314
- let frame_id = 0 // Performance optimization
315
219
 
316
220
  // Camera state - following Materials Project's 3D camera setup
317
221
  let camera = $state({
@@ -322,43 +226,60 @@
322
226
  center_y: 20, // Slight offset to avoid legend overlap
323
227
  })
324
228
 
325
- // Interaction state
326
- let is_dragging = $state(false)
327
- let drag_started = $state(false)
328
- let last_mouse = $state({ x: 0, y: 0 })
329
- let hover_data = $state.raw<HoverData3D<ConvexHullEntry> | null>(null)
330
- let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
331
-
332
- // Drag and drop state
333
- let drag_over = $state(false)
334
-
335
- // Structure popup state
336
- let modal_open = $state(false)
337
- let selected_structure = $state<AnyStructure | null>(null)
338
- let modal_place_right = $state(true)
339
- $effect(() => {
340
- const current_selection = helpers.current_entry(selected_entry, plot_entries)
341
- const stale_selection = selected_entry && !current_selection
342
- if (stale_selection) selected_entry = null
343
- else if (current_selection && !helpers.same_entry(current_selection, selected_entry)) {
344
- selected_entry = current_selection
345
- }
346
- const current_hover = helpers.current_entry(hover_data?.entry, plot_entries)
347
- if (hover_data?.entry && !current_hover) {
348
- hover_data = null
349
- on_point_hover?.(null)
350
- } else if (hover_data && current_hover && current_hover !== hover_data.entry) {
351
- hover_data = { ...hover_data, entry: current_hover }
352
- }
353
- if (modal_open) {
354
- const structure = current_selection && extract_structure_from_entry(current_selection)
355
- if (structure) selected_structure = structure
356
- else {
357
- modal_open = false
358
- selected_structure = null
229
+ // Shared canvas-interaction scaffold (mouse/keyboard handlers, hover/drag/popup
230
+ // state, canvas sizing, render scheduler). Rotation math + keydown actions stay local.
231
+ const interactions = create_canvas_interactions({
232
+ wheel_clamp: [1.0, 15],
233
+ fullscreen_bg_var: `--hull-4d-bg-fullscreen`,
234
+ canvas: () => canvas,
235
+ wrapper: () => wrapper,
236
+ ctx: () => ctx,
237
+ set_ctx: (context) => ctx = context,
238
+ set_canvas_dims: (dims) => canvas_dims = dims,
239
+ visible_entries: () => hull_data.visible_entries,
240
+ plot_entries: () => plot_entries,
241
+ selected_entry: () => selected_entry,
242
+ set_selected_entry: (entry) => selected_entry = entry,
243
+ fullscreen: () => fullscreen,
244
+ enable_click_selection: () => enable_click_selection,
245
+ enable_structure_preview: () => enable_structure_preview,
246
+ on_point_click: () => on_point_click,
247
+ on_point_hover: () => on_point_hover,
248
+ on_file_drop: () => on_file_drop,
249
+ entry_category: () => entry_category,
250
+ zoom: () => camera.zoom,
251
+ set_zoom: (zoom) => camera.zoom = zoom,
252
+ project_point: project_3d_point,
253
+ extract_structure: extract_structure_from_entry,
254
+ render_frame,
255
+ on_drag: (dx, dy, panning) => {
256
+ if (panning) {
257
+ camera.center_x += dx
258
+ camera.center_y += dy
259
+ } else {
260
+ camera.rotation_y += dx * 0.005
261
+ camera.rotation_x = Math.max(
262
+ -Math.PI / 3,
263
+ Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005),
264
+ )
359
265
  }
360
- }
266
+ },
267
+ // Reset pan center when entering/exiting fullscreen
268
+ on_fullscreen_change: () => {
269
+ camera.center_x = 0
270
+ camera.center_y = 20
271
+ },
272
+ actions: () => ({
273
+ r: reset_camera,
274
+ b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
275
+ s: () => show_stable = !show_stable,
276
+ u: () => show_unstable = !show_unstable,
277
+ h: () => show_hull_faces = !show_hull_faces,
278
+ l: () => show_stable_labels = !show_stable_labels,
279
+ }),
361
280
  })
281
+ const { render_once } = interactions
282
+ const sorted_points_cache = $derived(interactions.sorted_points_cache)
362
283
 
363
284
  // Hull face color (customizable via controls)
364
285
  let hull_face_color = $state(`#4caf50`)
@@ -382,31 +303,27 @@
382
303
  // Re-render when important state changes
383
304
  $effect(() => {
384
305
  // oxfmt-ignore
385
- void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
306
+ void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_data.visible_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
386
307
 
387
308
  render_once()
388
309
  })
389
310
 
390
311
  // Visibility toggles are now bindable props
391
312
 
392
- // Smart label defaults - hide labels if too many entries
313
+ // Smart label defaults: hide labels for large datasets. Applied once per dataset
314
+ // (keyed on the entries prop) so later entry-count changes from temperature/gas
315
+ // filtering don't clobber the user's label toggles.
316
+ let label_defaults_applied_for: PhaseData[] | null = null
393
317
  $effect(() => {
394
- const total_entries = effective_entries.length
395
- if (total_entries > label_threshold) {
396
- show_stable_labels = false
397
- } else {
398
- // For smaller datasets, show stable labels by default
399
- show_stable_labels = true
400
- }
318
+ if (label_defaults_applied_for === entries) return
319
+ label_defaults_applied_for = entries
320
+ show_stable_labels = hull_data.effective_entries.length <= label_threshold
401
321
  show_unstable_labels = false
402
322
  })
403
323
 
404
- // Function to extract structure data from a convex hull entry
405
- function extract_structure_from_entry(
406
- entry: ConvexHullEntry,
407
- ): AnyStructure | null {
408
- const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
409
- return orig_entry?.structure as AnyStructure || null
324
+ // function (not const) so the create_canvas_interactions options above can reference it
325
+ function extract_structure_from_entry(entry: ConvexHullEntry): AnyStructure | null {
326
+ return helpers.extract_structure_from_entry(entries, entry)
410
327
  }
411
328
 
412
329
  const reset_camera = () => {
@@ -420,15 +337,16 @@
420
337
  reset_camera()
421
338
  fullscreen = DEFAULTS.convex_hull.quaternary.fullscreen
422
339
  info_pane_open = DEFAULTS.convex_hull.quaternary.info_pane_open
423
- legend_pane_open = DEFAULTS.convex_hull.quaternary.legend_pane_open
340
+ controls_open = DEFAULTS.convex_hull.quaternary.legend_pane_open
424
341
  color_mode = DEFAULTS.convex_hull.quaternary.color_mode
425
342
  color_scale = DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName
426
343
  show_stable = DEFAULTS.convex_hull.quaternary.show_stable
427
344
  show_unstable = DEFAULTS.convex_hull.quaternary.show_unstable
345
+ hidden_categories = []
428
346
  show_stable_labels = DEFAULTS.convex_hull.quaternary.show_stable_labels
429
347
  show_unstable_labels = DEFAULTS.convex_hull.quaternary.show_unstable_labels
430
348
  // Use auto-computed threshold based on entry count instead of static default
431
- max_hull_dist_show_phases = auto_default_threshold
349
+ max_hull_dist_show_phases = hull_data.auto_default_threshold
432
350
  max_hull_dist_show_labels =
433
351
  DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels
434
352
  show_hull_faces = DEFAULTS.convex_hull.quaternary.show_hull_faces
@@ -438,69 +356,6 @@
438
356
  .hull_face_color_mode as HullFaceColorMode
439
357
  }
440
358
 
441
- const handle_keydown = (event: KeyboardEvent) => {
442
- const target = event.target
443
- // Skip if focus is on an interactive element that handles Enter natively
444
- const interactive_selector =
445
- `input,textarea,select,button,a,[contenteditable="true"],[role="button"],[tabindex]:not([tabindex="-1"])`
446
- if (
447
- target instanceof HTMLElement &&
448
- target.matches(interactive_selector) &&
449
- target !== canvas
450
- ) return
451
-
452
- // Prevent double handling from canvas + wrapper bubbling
453
- if (event.target !== event.currentTarget && event.currentTarget !== canvas) return
454
-
455
- // Handle Enter for keyboard accessibility - select hovered entry
456
- if (event.key === `Enter`) {
457
- const entry = hover_data?.entry
458
- if (entry) {
459
- on_point_click?.(entry)
460
- if (enable_click_selection) {
461
- selected_entry = entry
462
- if (enable_structure_preview) {
463
- const structure = extract_structure_from_entry(entry)
464
- if (structure) {
465
- selected_structure = structure
466
- modal_place_right = helpers.calculate_modal_side(wrapper)
467
- modal_open = true
468
- }
469
- }
470
- }
471
- } else if (modal_open) {
472
- close_structure_popup()
473
- }
474
- return
475
- }
476
-
477
- const actions: Record<string, () => void> = {
478
- r: reset_camera,
479
- b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
480
- s: () => show_stable = !show_stable,
481
- u: () => show_unstable = !show_unstable,
482
- h: () => show_hull_faces = !show_hull_faces,
483
- l: () => show_stable_labels = !show_stable_labels,
484
- }
485
- actions[event.key.toLowerCase()]?.()
486
- }
487
-
488
- async function handle_file_drop(event: DragEvent): Promise<void> {
489
- drag_over = false
490
- const data = await helpers.parse_hull_entries_from_drop(event)
491
- if (data) on_file_drop?.(data)
492
- }
493
-
494
- async function copy_entry_data(
495
- entry: ConvexHullEntry,
496
- position: { x: number; y: number },
497
- ) {
498
- await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
499
- copy_feedback.visible = visible
500
- copy_feedback.position = pos
501
- })
502
- }
503
-
504
359
  const get_point_color = (entry: ConvexHullEntry): string =>
505
360
  helpers.get_point_color_for_entry(
506
361
  entry,
@@ -736,14 +591,14 @@
736
591
  triangles.sort((a, b) => a.avg_depth - b.avg_depth)
737
592
 
738
593
  // Lazy computation for uniform mode: normalize alpha by formation energy
739
- let norm_alpha: ((w: number) => number) | null = null
594
+ let norm_alpha: ((energy: number) => number) | null = null
740
595
  if (hull_face_color_mode === `uniform`) {
741
596
  norm_alpha = (energy: number) => {
742
- const t = Math.max(
597
+ const frac = Math.max(
743
598
  0,
744
599
  Math.min(1, (0 - energy) / Math.max(1e-6, 0 - formation_energy_min)),
745
600
  )
746
- return t * hull_face_opacity
601
+ return frac * hull_face_opacity
747
602
  }
748
603
  }
749
604
 
@@ -809,59 +664,16 @@
809
664
 
810
665
  function draw_data_points(): void {
811
666
  if (!ctx || sorted_points_cache.length === 0) return
812
-
813
- for (const { entry, projected } of sorted_points_cache) {
814
- const is_stable = helpers.entry_is_stable(entry)
815
- const is_entry_highlighted = is_highlighted(entry)
816
- const color = get_point_color(entry)
817
- const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
818
- const marker = entry.marker || `circle`
819
-
820
- // Shadow
821
- const shadow_offset = Math.abs(entry.z) * 2 * canvas_dims.scale
822
- ctx.fillStyle = `rgba(0, 0, 0, 0.2)`
823
- const shadow_path = helpers.create_marker_path(size * 0.8, marker)
824
- ctx.save()
825
- ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset)
826
- ctx.fill(shadow_path)
827
- ctx.restore()
828
-
829
- // Highlights
830
- if (selected_entry && entry.entry_id === selected_entry.entry_id) {
831
- helpers.draw_selection_highlight(
832
- ctx,
833
- projected,
834
- size,
835
- canvas_dims.scale,
836
- pulse.time,
837
- pulse_opacity,
838
- )
839
- }
840
- if (is_entry_highlighted) {
841
- helpers.draw_highlight_effect(
842
- ctx,
843
- projected,
844
- size,
845
- canvas_dims.scale,
846
- pulse.time,
847
- merged_highlight_style,
848
- )
849
- }
850
-
851
- // Main point with marker symbol
852
- ctx.fillStyle =
853
- is_entry_highlighted && merged_highlight_style.effect === `color`
854
- ? merged_highlight_style.color
855
- : color
856
- ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`
857
- ctx.lineWidth = 0.5 * canvas_dims.scale
858
- const marker_path = helpers.create_marker_path(size, marker)
859
- ctx.save()
860
- ctx.translate(projected.x, projected.y)
861
- ctx.fill(marker_path)
862
- ctx.stroke(marker_path)
863
- ctx.restore()
864
- }
667
+ helpers.draw_hull_points(ctx, sorted_points_cache, {
668
+ scale: canvas_dims.scale,
669
+ shadow_factor: 2,
670
+ selected_entry,
671
+ is_highlighted,
672
+ get_point_color,
673
+ highlight_style: merged_highlight_style,
674
+ pulse_time: pulse.time,
675
+ pulse_opacity,
676
+ })
865
677
 
866
678
  if (!merged_config.show_labels) return
867
679
 
@@ -925,169 +737,7 @@
925
737
  draw_data_points() // Draw data points (on top)
926
738
  }
927
739
 
928
- function handle_mouse_down(event: MouseEvent) {
929
- is_dragging = true
930
- drag_started = false
931
- hover_data = null
932
- on_point_hover?.(null)
933
- last_mouse = { x: event.clientX, y: event.clientY }
934
- }
935
-
936
- const handle_mouse_move = (event: MouseEvent) => {
937
- if (!is_dragging) return
938
- const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y]
939
-
940
- // Mark as drag if any movement occurred
941
- if (dx !== 0 || dy !== 0) drag_started = true
942
-
943
- // With Cmd/Ctrl held: pan the view instead of rotating
944
- if (event.metaKey || event.ctrlKey) {
945
- camera.center_x += dx
946
- camera.center_y += dy
947
- } else {
948
- camera.rotation_y += dx * 0.005
949
- camera.rotation_x = Math.max(
950
- -Math.PI / 3,
951
- Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005),
952
- )
953
- }
954
- last_mouse = { x: event.clientX, y: event.clientY }
955
- }
956
-
957
- const handle_wheel = (event: WheelEvent) => {
958
- event.preventDefault()
959
- camera.zoom = Math.max(
960
- 1.0,
961
- Math.min(15, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)),
962
- )
963
- }
964
-
965
- const handle_hover = (event: MouseEvent) => {
966
- if (is_dragging) return
967
- const entry = find_entry_at_mouse(event)
968
- hover_data = entry
969
- ? { entry, position: { x: event.clientX, y: event.clientY } }
970
- : null
971
- on_point_hover?.(hover_data)
972
- }
973
-
974
- const find_entry_at_mouse = (event: MouseEvent): ConvexHullEntry | null =>
975
- helpers.find_hull_entry_at_mouse(
976
- canvas,
977
- event,
978
- visible_entries,
979
- (x: number, y: number, z: number) => {
980
- const projected = project_3d_point(x, y, z)
981
- return { x: projected.x, y: projected.y }
982
- },
983
- )
984
-
985
- const handle_click = (event: MouseEvent) => {
986
- event.stopPropagation()
987
-
988
- // Check if this was a drag operation (any mouse movement during drag)
989
- const was_drag = drag_started
990
- drag_started = false // Reset for next interaction
991
- if (was_drag) return // Don't trigger click if this was a drag
992
-
993
- const entry = find_entry_at_mouse(event)
994
- if (entry) {
995
- on_point_click?.(entry)
996
- if (enable_click_selection) {
997
- selected_entry = entry
998
- if (enable_structure_preview) {
999
- const structure = extract_structure_from_entry(entry)
1000
- if (structure) {
1001
- selected_structure = structure
1002
- modal_place_right = helpers.calculate_modal_side(wrapper)
1003
- modal_open = true
1004
- }
1005
- }
1006
- }
1007
- } else if (modal_open) close_structure_popup()
1008
- }
1009
-
1010
- function close_structure_popup() {
1011
- modal_open = false
1012
- selected_structure = null
1013
- selected_entry = null
1014
- }
1015
-
1016
- const handle_double_click = (event: MouseEvent) => {
1017
- const entry = find_entry_at_mouse(event)
1018
- if (entry) {
1019
- copy_entry_data(entry, {
1020
- x: event.clientX,
1021
- y: event.clientY,
1022
- })
1023
- }
1024
- }
1025
-
1026
- function render_once() {
1027
- if (!frame_id) {
1028
- frame_id = requestAnimationFrame(() => {
1029
- render_frame()
1030
- frame_id = 0
1031
- })
1032
- }
1033
- }
1034
-
1035
- function update_canvas_size() {
1036
- if (!canvas) return
1037
- const dpr = globalThis.devicePixelRatio || 1
1038
- const container = canvas.parentElement
1039
- const rect = container?.getBoundingClientRect()
1040
- const [width, height] = rect ? [rect.width, rect.height] : [400, 400]
1041
-
1042
- const new_width = Math.max(0, Math.round(width * dpr))
1043
- const new_height = Math.max(0, Math.round(height * dpr))
1044
- canvas_dims = { width, height, scale: Math.min(width, height) / 600 }
1045
-
1046
- if (!ctx || canvas.width !== new_width || canvas.height !== new_height) {
1047
- canvas.width = new_width
1048
- canvas.height = new_height
1049
- ctx = canvas.getContext(`2d`)
1050
- if (ctx) {
1051
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
1052
- ctx.imageSmoothingEnabled = true
1053
- ctx.imageSmoothingQuality = `high`
1054
- }
1055
- }
1056
- render_once()
1057
- }
1058
-
1059
- $effect(() => {
1060
- if (!canvas) return
1061
-
1062
- // Initial setup
1063
- update_canvas_size()
1064
-
1065
- // Watch for resize events - only update canvas, don't reset camera
1066
- const resize_observer = new ResizeObserver(update_canvas_size)
1067
-
1068
- const container = canvas.parentElement
1069
- if (container) resize_observer.observe(container)
1070
-
1071
- return () => { // Cleanup on unmount
1072
- if (frame_id) cancelAnimationFrame(frame_id)
1073
- resize_observer.disconnect()
1074
- }
1075
- })
1076
-
1077
- // Fullscreen handling with camera reset
1078
- let was_fullscreen = $state(fullscreen)
1079
- $effect(() => {
1080
- setup_fullscreen_effect(fullscreen, wrapper, (entering_fullscreen) => {
1081
- if (entering_fullscreen !== was_fullscreen) {
1082
- camera.center_x = 0
1083
- camera.center_y = 20
1084
- was_fullscreen = entering_fullscreen
1085
- }
1086
- })
1087
- set_fullscreen_bg(wrapper, fullscreen, `--hull-4d-bg-fullscreen`)
1088
- })
1089
-
1090
- // Performance: Cache canvas dimensions and pre-compute sorted point projections
740
+ // Performance: Cache canvas dimensions and formation energy minimum
1091
741
  let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
1092
742
  const formation_energy_min = $derived.by(() => {
1093
743
  let min_energy = 0
@@ -1096,57 +746,33 @@
1096
746
  }
1097
747
  return min_energy
1098
748
  })
1099
- const sorted_points_cache = $derived.by(() => {
1100
- if (!canvas || visible_entries.length === 0) return []
1101
- return visible_entries
1102
- .map((entry) => ({
1103
- entry,
1104
- projected: project_3d_point(entry.x, entry.y, entry.z),
1105
- }))
1106
- .sort((a, b) => a.projected.depth - b.projected.depth)
1107
- })
1108
749
 
1109
- let style = $derived(
1110
- `--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
1111
- --hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
1112
- --hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
1113
- --hull-text-color:${
1114
- merged_config.colors?.annotation || `var(--text-color, #212121)`
1115
- }`,
1116
- )
750
+ let style = $derived(helpers.hull_style_css(merged_config.colors))
1117
751
  </script>
1118
752
 
1119
753
  <svelte:document
1120
754
  onfullscreenchange={() => {
1121
- fullscreen = Boolean(document.fullscreenElement)
755
+ // tie fullscreen state to this component's own wrapper, not any fullscreen element
756
+ fullscreen = document.fullscreenElement === wrapper
1122
757
  }}
1123
- onmousemove={handle_mouse_move}
1124
- onmouseup={() => [is_dragging, drag_started] = [false, false]}
758
+ onmousemove={interactions.handle_mouse_move}
759
+ onmouseup={interactions.handle_mouse_up}
1125
760
  />
1126
761
 
1127
762
  <div
1128
763
  {...rest}
1129
764
  class="convex-hull-4d {rest.class ?? ``}"
1130
- class:dragover={drag_over}
765
+ class:dragover={interactions.drag_over}
1131
766
  style={`${style}; ${rest.style ?? ``}`}
1132
767
  data-has-selection={selected_entry !== null}
1133
- data-has-hover={hover_data !== null}
1134
- data-is-dragging={is_dragging}
768
+ data-has-hover={interactions.hover_data !== null}
769
+ data-is-dragging={interactions.is_dragging}
1135
770
  data-rotation-x={camera.rotation_x.toFixed(4)}
1136
771
  data-rotation-y={camera.rotation_y.toFixed(4)}
1137
772
  bind:this={wrapper}
1138
773
  role="application"
1139
774
  tabindex="-1"
1140
- onkeydown={handle_keydown}
1141
- ondrop={handle_file_drop}
1142
- ondragover={(event) => {
1143
- event.preventDefault()
1144
- drag_over = true
1145
- }}
1146
- ondragleave={(event) => {
1147
- event.preventDefault()
1148
- drag_over = false
1149
- }}
775
+ {...interactions.wrapper_handlers}
1150
776
  aria-label="Convex hull visualization"
1151
777
  >
1152
778
  {@render children?.({
@@ -1163,12 +789,7 @@
1163
789
  bind:this={canvas}
1164
790
  tabindex="0"
1165
791
  aria-label={merged_controls.title || phase_stats?.chemical_system || `4D Convex Hull`}
1166
- onmousedown={handle_mouse_down}
1167
- onmousemove={handle_hover}
1168
- onclick={handle_click}
1169
- onkeydown={handle_keydown}
1170
- ondblclick={handle_double_click}
1171
- onwheel={handle_wheel}
792
+ {...interactions.canvas_handlers}
1172
793
  ></canvas>
1173
794
 
1174
795
  {#if entries.length === 0}
@@ -1180,14 +801,9 @@
1180
801
 
1181
802
  <!-- Energy above hull Color Bar -->
1182
803
  {#if color_mode === `energy` && plot_entries.length > 0}
1183
- {@const hull_distances = plot_entries
1184
- .map((entry) => entry.e_above_hull)
1185
- .filter((val): val is number => typeof val === `number`)}
1186
- {@const min_energy = hull_distances.length > 0 ? Math.min(...hull_distances) : 0}
1187
- {@const max_energy = hull_distances.length > 0 ? Math.max(...hull_distances, 0.1) : 0.1}
1188
804
  <ColorBar
1189
805
  title="Energy above hull (eV/atom)"
1190
- range={[min_energy, max_energy]}
806
+ range={helpers.hull_distance_range(plot_entries)}
1191
807
  {color_scale}
1192
808
  wrapper_style="position: absolute; bottom: 2em; left: 1em; width: 200px;"
1193
809
  bar_style="height: 12px;"
@@ -1195,88 +811,52 @@
1195
811
  />
1196
812
  {/if}
1197
813
 
1198
- <!-- Control buttons (top-right corner like Structure.svelte) -->
1199
- {#if controls_config.mode !== `never`}
1200
- <section class="control-buttons {controls_config.class}">
1201
- {#if controls_config.visible(`reset`)}
1202
- <button
1203
- type="button"
1204
- onclick={reset_all}
1205
- title="Reset camera view (R key)"
1206
- class="reset-camera-btn"
1207
- >
1208
- <Icon icon="Reset" />
1209
- </button>
1210
- {/if}
1211
-
1212
- {#if enable_info_pane && phase_stats && controls_config.visible(`info-pane`)}
1213
- <ConvexHullInfoPane
1214
- bind:pane_open={info_pane_open}
1215
- {phase_stats}
1216
- {stable_entries}
1217
- {unstable_entries}
1218
- {show_stable}
1219
- {show_unstable}
1220
- {max_hull_dist_show_phases}
1221
- {max_hull_dist_show_labels}
1222
- {label_threshold}
1223
- toggle_props={{ class: `info-btn` }}
1224
- />
1225
- {/if}
1226
-
1227
- {#if enable_fullscreen && controls_config.visible(`fullscreen`)}
1228
- <button
1229
- type="button"
1230
- onclick={() => toggle_fullscreen(wrapper)}
1231
- title="{fullscreen ? `Exit` : `Enter`} fullscreen"
1232
- class="fullscreen-btn"
1233
- >
1234
- <Icon icon="{fullscreen ? `Exit` : ``}Fullscreen" />
1235
- </button>
1236
- {/if}
1237
-
1238
- <!-- Legend controls pane -->
1239
- {#if controls_config.visible(`controls`)}
1240
- <ConvexHullControls
1241
- bind:controls_open={legend_pane_open}
1242
- bind:color_mode
1243
- bind:color_scale
1244
- bind:show_stable
1245
- bind:show_unstable
1246
- bind:show_stable_labels
1247
- bind:show_unstable_labels
1248
- bind:max_hull_dist_show_phases
1249
- bind:max_hull_dist_show_labels
1250
- {max_hull_dist_in_data}
1251
- {stable_entries}
1252
- {unstable_entries}
1253
- {camera}
1254
- {merged_controls}
1255
- toggle_props={{ class: `legend-controls-btn` }}
1256
- {show_hull_faces}
1257
- on_hull_faces_change={(value: boolean) => show_hull_faces = value}
1258
- {hull_face_color}
1259
- on_hull_face_color_change={(value: string) => hull_face_color = value}
1260
- {hull_face_opacity}
1261
- on_hull_face_opacity_change={(value: number) => hull_face_opacity = value}
1262
- {hull_face_color_mode}
1263
- on_hull_face_color_mode_change={(value: HullFaceColorMode) =>
1264
- hull_face_color_mode = value}
1265
- bind:energy_source_mode
1266
- {has_precomputed_e_form}
1267
- {can_compute_e_form}
1268
- {has_precomputed_hull}
1269
- {can_compute_hull}
1270
- />
1271
- {/if}
1272
- </section>
814
+ <!-- Toolbar + tooltip/copy-feedback/drag/structure-popup chrome -->
815
+ <ConvexHullChrome
816
+ {interactions}
817
+ {hull_data}
818
+ {controls_config}
819
+ {reset_all}
820
+ reset_title="Reset view and settings"
821
+ {enable_info_pane}
822
+ {phase_stats}
823
+ {label_threshold}
824
+ {fullscreen}
825
+ {fullscreen_toggle}
826
+ {wrapper}
827
+ {camera}
828
+ {merged_controls}
829
+ {stable_entries}
830
+ {unstable_entries}
831
+ {get_point_color}
832
+ {merged_highlight_style}
833
+ {is_highlighted}
834
+ {tooltip}
835
+ {selected_entry}
836
+ bind:show_hull_faces
837
+ bind:hull_face_color
838
+ bind:hull_face_opacity
839
+ bind:hull_face_color_mode
840
+ bind:info_pane_open
841
+ bind:controls_open
842
+ bind:color_mode
843
+ bind:color_scale
844
+ bind:show_stable
845
+ bind:show_unstable
846
+ {entry_category}
847
+ bind:hidden_categories
848
+ bind:show_stable_labels
849
+ bind:show_unstable_labels
850
+ bind:max_hull_dist_show_phases
851
+ bind:max_hull_dist_show_labels
852
+ bind:energy_source_mode
853
+ />
854
+
855
+ {#if hull_data.has_temp_data && temperature !== undefined}
856
+ <TemperatureSlider available_temperatures={hull_data.available_temperatures} bind:temperature />
1273
857
  {/if}
1274
858
 
1275
- {#if has_temp_data && temperature !== undefined}
1276
- <TemperatureSlider {available_temperatures} bind:temperature />
1277
- {/if}
1278
-
1279
- {#if gas_analysis.has_gas_dependent_elements && merged_gas_config}
859
+ {#if hull_data.gas_analysis.has_gas_dependent_elements && merged_gas_config}
1280
860
  <GasPressureControls
1281
861
  config={merged_gas_config}
1282
862
  bind:pressures={gas_pressures}
@@ -1284,48 +864,6 @@
1284
864
  />
1285
865
  {/if}
1286
866
 
1287
- <!-- Hover tooltip -->
1288
- {#if hover_data}
1289
- {@const { entry, position } = hover_data}
1290
- {@const entry_highlight = is_highlighted(entry) ? merged_highlight_style : undefined}
1291
- {@const tooltip_style =
1292
- `z-index: ${CONVEX_HULL_STYLE.z_index.tooltip}; backdrop-filter: blur(4px);
1293
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);`}
1294
- <PlotTooltip
1295
- x={position.x}
1296
- y={position.y}
1297
- offset={{ x: 10, y: -10 }}
1298
- bg_color={get_point_color(entry)}
1299
- fixed
1300
- style={tooltip_style}
1301
- >
1302
- <ConvexHullTooltip
1303
- {entry}
1304
- {polymorph_stats_map}
1305
- highlight_style={entry_highlight}
1306
- {tooltip}
1307
- />
1308
- </PlotTooltip>
1309
- {/if}
1310
-
1311
- <!-- Copy-to-clipboard feedback (double-click on point) -->
1312
- <ClickFeedback bind:visible={copy_feedback.visible} position={copy_feedback.position} />
1313
-
1314
- <!-- Drag over overlay -->
1315
- <DragOverlay visible={drag_over} />
1316
-
1317
- {#if modal_open && selected_structure}
1318
- <StructurePopup
1319
- structure={selected_structure}
1320
- place_right={modal_place_right}
1321
- stats={{
1322
- id: selected_entry?.entry_id,
1323
- e_above_hull: selected_entry?.e_above_hull,
1324
- e_form: selected_entry?.e_form_per_atom,
1325
- }}
1326
- onclose={close_structure_popup}
1327
- />
1328
- {/if}
1329
867
  </div>
1330
868
 
1331
869
  <style>
@@ -1353,42 +891,4 @@
1353
891
  canvas:active {
1354
892
  cursor: grabbing;
1355
893
  }
1356
- .control-buttons {
1357
- position: absolute;
1358
- top: 1ex;
1359
- right: 1ex;
1360
- display: flex;
1361
- gap: 8px;
1362
- transition: opacity 0.2s ease-in-out;
1363
- }
1364
- .control-buttons.hover-visible {
1365
- opacity: 0;
1366
- pointer-events: none;
1367
- }
1368
- .convex-hull-4d:hover .control-buttons.hover-visible,
1369
- .convex-hull-4d:focus-within .control-buttons.hover-visible {
1370
- opacity: 1;
1371
- pointer-events: auto;
1372
- }
1373
- .control-buttons.always-visible {
1374
- opacity: 1;
1375
- pointer-events: auto;
1376
- }
1377
- .control-buttons :global(.draggable-pane) {
1378
- z-index: 1001 !important;
1379
- }
1380
- .control-buttons :global(button) {
1381
- background: transparent;
1382
- border: none;
1383
- padding: 4px;
1384
- cursor: pointer;
1385
- border-radius: 3px;
1386
- color: var(--text-color, currentColor);
1387
- transition: background-color 0.2s;
1388
- display: flex;
1389
- font-size: clamp(0.85em, 2cqmin, 1.3em);
1390
- }
1391
- .control-buttons :global(button):hover {
1392
- background-color: color-mix(in srgb, currentColor 8%, transparent);
1393
- }
1394
894
  </style>