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
@@ -0,0 +1,166 @@
1
+ <script lang="ts">
2
+ import type { PaneProps, PaneToggleProps } from '../overlays'
3
+ import type { ExportItem, ExportSection } from './types'
4
+ import DraggablePane from '../overlays/DraggablePane.svelte'
5
+ import { sanitize_html } from '../sanitize'
6
+ import { type ComponentProps, onDestroy, type Snippet } from 'svelte'
7
+ import { tooltip } from 'svelte-multiselect/attachments'
8
+ import type { HTMLAttributes } from 'svelte/elements'
9
+
10
+ let {
11
+ export_pane_open = $bindable(false),
12
+ sections = [],
13
+ png_dpi = $bindable(150),
14
+ dpi_range = [50, 600],
15
+ icon_style = ``,
16
+ toggle_props = {},
17
+ pane_props = {},
18
+ children = undefined,
19
+ ...rest
20
+ }: HTMLAttributes<HTMLDivElement> & {
21
+ export_pane_open?: boolean
22
+ sections?: ExportSection[]
23
+ png_dpi?: number
24
+ dpi_range?: readonly [number, number]
25
+ icon_style?: string
26
+ toggle_props?: PaneToggleProps
27
+ pane_props?: PaneProps
28
+ // Pane-specific extras rendered below the sections (e.g. video export controls)
29
+ children?: Snippet
30
+ } = $props()
31
+
32
+ // Clamp DPI into dpi_range on input change (fires on blur, before any download click)
33
+ function clamp_dpi(): void {
34
+ const [min_dpi, max_dpi] = dpi_range
35
+ if (typeof png_dpi !== `number` || !Number.isFinite(png_dpi)) png_dpi = 150
36
+ else png_dpi = Math.round(Math.min(max_dpi, Math.max(min_dpi, png_dpi)))
37
+ }
38
+
39
+ // Copy-to-clipboard with temporary ✅ feedback; copy_text runs only on click
40
+ let copied_key = $state<string | null>(null)
41
+ let copied_timeout: ReturnType<typeof setTimeout> | undefined
42
+ async function handle_copy(item: ExportItem, key: string): Promise<void> {
43
+ if (item.disabled) return
44
+ const text = item.copy_text?.()
45
+ if (!text) return
46
+ try {
47
+ await navigator.clipboard.writeText(text)
48
+ copied_key = key
49
+ clearTimeout(copied_timeout)
50
+ copied_timeout = setTimeout(() => (copied_key = null), 1000)
51
+ } catch (error) {
52
+ console.error(`Failed to copy ${item.label} to clipboard`, error)
53
+ }
54
+ }
55
+ onDestroy(() => clearTimeout(copied_timeout))
56
+ </script>
57
+
58
+ <DraggablePane
59
+ bind:show={export_pane_open}
60
+ open_icon="Cross"
61
+ closed_icon="Export"
62
+ {icon_style}
63
+ pane_props={{
64
+ ...rest,
65
+ ...pane_props,
66
+ class: `export-pane ${rest.class ?? ``} ${pane_props?.class ?? ``}`.trim(),
67
+ }}
68
+ {toggle_props}
69
+ >
70
+ {#each sections as section, sec_idx (section.title ?? sec_idx)}
71
+ {#if section.title}
72
+ <h4
73
+ {@attach section.tooltip
74
+ ? tooltip({ allow_html: true, content: sanitize_html(section.tooltip) })
75
+ : () => {}}
76
+ >{section.title}</h4>
77
+ {/if}
78
+ <div class="export-grid">
79
+ {#each section.items as item, item_idx (item.label)}
80
+ {@const copy_key = `${sec_idx}-${item_idx}`}
81
+ <!-- not a <label>: it would forward label-text clicks to the first (download) button -->
82
+ <span class="export-item">
83
+ {#if item.hint}
84
+ <span
85
+ {@attach tooltip({ allow_html: true, content: sanitize_html(item.hint) })}
86
+ >{item.label}</span>
87
+ {:else}
88
+ {item.label}
89
+ {/if}
90
+ {#if item.on_download}
91
+ <button
92
+ type="button"
93
+ onclick={item.on_download}
94
+ disabled={item.disabled ?? false}
95
+ aria-label={`Download ${item.label}`}
96
+ title={`Download ${item.label}${item.show_dpi ? ` (${png_dpi} DPI)` : ``}`}
97
+ >
98
+
99
+ </button>
100
+ {/if}
101
+ {#if item.copy_text}
102
+ <button
103
+ type="button"
104
+ onclick={() => handle_copy(item, copy_key)}
105
+ disabled={item.disabled ?? false}
106
+ aria-label="Copy {item.label} to clipboard"
107
+ title="Copy {item.label} to clipboard"
108
+ >
109
+ {copied_key === copy_key ? `✅` : `📋`}
110
+ </button>
111
+ {/if}
112
+ {#if item.show_dpi}
113
+ <span class="dpi-input">(DPI: <input
114
+ type="number"
115
+ min={dpi_range[0]}
116
+ max={dpi_range[1]}
117
+ bind:value={png_dpi}
118
+ onchange={clamp_dpi}
119
+ title="Export resolution in dots per inch"
120
+ />)</span>
121
+ {/if}
122
+ </span>
123
+ {/each}
124
+ </div>
125
+ {/each}
126
+ {@render children?.()}
127
+ </DraggablePane>
128
+
129
+ <style>
130
+ h4 {
131
+ display: flex;
132
+ align-items: center;
133
+ margin: 0;
134
+ }
135
+ .export-grid {
136
+ display: flex;
137
+ flex-wrap: wrap;
138
+ align-items: center;
139
+ gap: 4pt 10pt;
140
+ font-size: 0.95em;
141
+ }
142
+ .export-item {
143
+ display: flex;
144
+ align-items: center;
145
+ gap: 4pt;
146
+ white-space: nowrap;
147
+ }
148
+ .export-grid button {
149
+ min-width: 1.9em;
150
+ height: 1.6em;
151
+ padding: 0 4pt;
152
+ box-sizing: border-box;
153
+ display: inline-flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ }
157
+ .export-grid input[type='number'] {
158
+ width: 3.5em;
159
+ }
160
+ .dpi-input {
161
+ display: inline-flex;
162
+ align-items: center;
163
+ gap: 2pt;
164
+ white-space: nowrap;
165
+ }
166
+ </style>
@@ -0,0 +1,17 @@
1
+ import type { PaneProps, PaneToggleProps } from '../overlays';
2
+ import type { ExportSection } from './types';
3
+ import { type Snippet } from 'svelte';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ type $$ComponentProps = HTMLAttributes<HTMLDivElement> & {
6
+ export_pane_open?: boolean;
7
+ sections?: ExportSection[];
8
+ png_dpi?: number;
9
+ dpi_range?: readonly [number, number];
10
+ icon_style?: string;
11
+ toggle_props?: PaneToggleProps;
12
+ pane_props?: PaneProps;
13
+ children?: Snippet;
14
+ };
15
+ declare const ExportPane: import("svelte").Component<$$ComponentProps, {}, "export_pane_open" | "png_dpi">;
16
+ type ExportPane = ReturnType<typeof ExportPane>;
17
+ export default ExportPane;
@@ -16,7 +16,6 @@ export async function decompress_data(data, format) {
16
16
  // Decompress data and return as ArrayBuffer (for binary files like .brml.gz)
17
17
  export async function decompress_data_binary(data, format) {
18
18
  try {
19
- // Handle unsupported formats
20
19
  if (format === `zip` || format === `xz` || format === `bz2`) {
21
20
  throw new Error(`${format.toUpperCase()} decompression is not supported in the browser. ` +
22
21
  `Please extract the ${format.toUpperCase()} file first.`);
@@ -40,7 +39,7 @@ export async function decompress_data_binary(data, format) {
40
39
  }
41
40
  export function decompress_file(file) {
42
41
  const format = detect_compression_format(file.name);
43
- const is_supported = Boolean(format && ![`zip`, `xz`, `bz2`].includes(format));
42
+ const is_supported = format !== null && format !== `zip` && format !== `xz` && format !== `bz2`;
44
43
  return new Promise((resolve, reject) => {
45
44
  const reader = new FileReader();
46
45
  reader.addEventListener(`load`, () => {
@@ -1,11 +1,15 @@
1
1
  import type { AnyStructure } from '../structure';
2
- import { type Camera, type Scene } from 'three';
2
+ import { type Camera, type Scene, type WebGLRenderer } from 'three';
3
+ export declare const renderer_registry: WeakMap<HTMLCanvasElement, WebGLRenderer>;
4
+ export declare const dpi_to_scale: (png_dpi: number) => number;
3
5
  export declare function canvas_to_png_blob(canvas: HTMLCanvasElement, png_dpi?: number, scene?: Scene | null, camera?: Camera | null): Promise<Blob>;
4
6
  export declare function export_canvas_as_png(canvas: HTMLCanvasElement | null, structure_or_filename: AnyStructure | string | undefined, png_dpi?: number, scene?: Scene | null, camera?: Camera | null): void;
5
7
  export declare function svg_to_svg_string(svg_element: SVGElement, inline_styles?: readonly string[]): string;
6
8
  export declare function export_svg_as_svg(svg_element: SVGElement | null, filename: string, inline_styles?: readonly string[]): void;
7
9
  export declare function svg_to_png_blob(svg_element: SVGElement, png_dpi?: number, inline_styles?: readonly string[]): Promise<Blob>;
8
10
  export declare function export_svg_as_png(svg_element: SVGElement | null, filename: string, png_dpi?: number, inline_styles?: readonly string[]): void;
11
+ export declare function observe_canvas_presence(wrapper: HTMLElement | undefined, set: (has_canvas: boolean) => void): (() => void) | undefined;
12
+ export declare const estimate_video_bitrate: (pixel_count: number, fps: number) => number;
9
13
  export declare function get_ffmpeg_conversion_command(input_filename: string): string;
10
14
  export declare function export_trajectory_video(canvas: HTMLCanvasElement | null, filename: string, options?: {
11
15
  fps?: number;
package/dist/io/export.js CHANGED
@@ -2,29 +2,20 @@ import { download } from './fetch';
2
2
  import { create_structure_filename } from '../structure/export';
3
3
  import { to_error } from '../utils';
4
4
  import { Vector2 } from 'three';
5
- function is_webgl_renderer_like(value) {
6
- if (typeof value !== `object` || !value)
7
- return false;
8
- const renderer_obj = value;
9
- return (typeof renderer_obj.render === `function` &&
10
- typeof renderer_obj.getPixelRatio === `function` &&
11
- typeof renderer_obj.setPixelRatio === `function` &&
12
- typeof renderer_obj.getSize === `function` &&
13
- typeof renderer_obj.setSize === `function`);
14
- }
15
- function get_canvas_renderer(canvas) {
16
- // oxlint-disable-next-line no-underscore-dangle -- three.js stores its renderer here
17
- const renderer_val = canvas.__renderer;
18
- return is_webgl_renderer_like(renderer_val) ? renderer_val : undefined;
19
- }
5
+ // Maps a Threlte canvas to its WebGLRenderer so PNG export can look up the renderer for a
6
+ // given canvas without mutating the DOM element. Populated by bind_renderer (scene/).
7
+ export const renderer_registry = new WeakMap();
8
+ // PNG DPI -> render scale relative to the 72 DPI baseline, capped at 10x. DPI floors
9
+ // at 1 (non-finite -> 72) so bad inputs can't yield 0x0 canvases or NaN pixel ratios.
10
+ export const dpi_to_scale = (png_dpi) => Math.min(Math.max(1, Number.isFinite(png_dpi) ? png_dpi : 72) / 72, 10);
20
11
  // Capture a WebGL canvas as a PNG Blob at the given DPI.
21
12
  // Temporarily adjusts renderer pixel ratio for high-res capture, then restores.
22
13
  // Returns data directly (no browser download), suitable for programmatic capture
23
14
  // in test suites, server-side rendering, or Python widget integration via anywidget.
24
15
  // DPI is converted to a resolution multiplier relative to 72 DPI baseline, capped at 10x.
25
16
  export function canvas_to_png_blob(canvas, png_dpi = 150, scene = null, camera = null) {
26
- const resolution_multiplier = Math.min(png_dpi / 72, 10);
27
- const renderer = get_canvas_renderer(canvas);
17
+ const resolution_multiplier = dpi_to_scale(png_dpi);
18
+ const renderer = renderer_registry.get(canvas);
28
19
  if (resolution_multiplier <= 1.1 || !renderer) {
29
20
  if (renderer && scene && camera)
30
21
  renderer.render(scene, camera);
@@ -168,9 +159,10 @@ export function svg_to_png_blob(svg_element, png_dpi = 150, inline_styles = [])
168
159
  if (!Number.isFinite(png_dpi) || png_dpi <= 0) {
169
160
  return Promise.reject(new Error(`Invalid PNG DPI for export`));
170
161
  }
171
- const resolution_multiplier = Math.min(png_dpi / 72, 10);
172
- const pixel_width = Math.round(width * resolution_multiplier);
173
- const pixel_height = Math.round(height * resolution_multiplier);
162
+ const resolution_multiplier = dpi_to_scale(png_dpi);
163
+ // Floor at 1px: small viewBoxes at low DPI round to 0 and make toBlob fail confusingly
164
+ const pixel_width = Math.max(1, Math.round(width * resolution_multiplier));
165
+ const pixel_height = Math.max(1, Math.round(height * resolution_multiplier));
174
166
  const canvas = document.createElement(`canvas`);
175
167
  const ctx = canvas.getContext(`2d`);
176
168
  if (!ctx)
@@ -217,6 +209,23 @@ export function export_svg_as_png(svg_element, filename, png_dpi = 150, inline_s
217
209
  .then((blob) => download(blob, filename, `image/png`))
218
210
  .catch((error) => console.error(`Error exporting PNG:`, error));
219
211
  }
212
+ // Watch a wrapper element for <canvas> insertion/removal: calls set(bool) immediately
213
+ // and on every DOM mutation. Returns a cleanup that disconnects the observer (or
214
+ // undefined when no wrapper is given). Used by export panes to enable canvas exports.
215
+ export function observe_canvas_presence(wrapper, set) {
216
+ if (!wrapper) {
217
+ set(false);
218
+ return undefined;
219
+ }
220
+ const check = () => set(Boolean(wrapper.querySelector(`canvas`)));
221
+ check();
222
+ const observer = new MutationObserver(check);
223
+ observer.observe(wrapper, { childList: true, subtree: true });
224
+ return () => observer.disconnect();
225
+ }
226
+ // Estimate VP9 video bitrate (bits/s) from pixel count and frame rate.
227
+ // VP9 needs ~0.1 bits per pixel per frame for good quality; clamped to [1, 200] Mbps.
228
+ export const estimate_video_bitrate = (pixel_count, fps) => Math.max(1_000_000, Math.min(pixel_count * fps * 0.1, 200_000_000));
220
229
  // Generate FFmpeg command for WebM to MP4 conversion
221
230
  export function get_ffmpeg_conversion_command(input_filename) {
222
231
  const output = input_filename.replace(/\.webm$/i, `.mp4`);
@@ -230,7 +239,7 @@ export async function export_trajectory_video(canvas, filename, options = {}) {
230
239
  typeof MediaRecorder === `undefined` ||
231
240
  !MediaRecorder.isTypeSupported(`video/webm;codecs=vp9`))
232
241
  throw new Error(`WebM video recording not supported in this browser`);
233
- const renderer = get_canvas_renderer(canvas);
242
+ const renderer = renderer_registry.get(canvas);
234
243
  // Store original renderer settings if changing resolution
235
244
  let orig_pixel_ratio;
236
245
  let orig_size;
@@ -242,13 +251,8 @@ export async function export_trajectory_video(canvas, filename, options = {}) {
242
251
  renderer.setSize(orig_size.width, orig_size.height, false);
243
252
  }
244
253
  // Calculate bitrate based on actual video dimensions
245
- // VP9 typically needs 0.08-0.12 bits per pixel per frame for good quality
246
- // canvas dimensions include device pixel ratio and any resolution_multiplier
247
- const pixels_per_frame = canvas.width * canvas.height;
248
- const bits_per_pixel_per_frame = 0.1; // Good quality for VP9
249
- // Clamp bitrate to reasonable bounds (1 Mbps min, 200 Mbps max)
250
- const calculated_bitrate = pixels_per_frame * fps * bits_per_pixel_per_frame;
251
- const bitrate = Math.max(1_000_000, Math.min(calculated_bitrate, 200_000_000));
254
+ // (canvas dimensions include device pixel ratio and any resolution_multiplier)
255
+ const bitrate = estimate_video_bitrate(canvas.width * canvas.height, fps);
252
256
  const stream = canvas.captureStream(0);
253
257
  const chunks = [];
254
258
  const recorder = new MediaRecorder(stream, {
@@ -2,4 +2,5 @@ export declare const to_query: (params: Record<string, string | number | undefin
2
2
  export declare function fetch_zipped<T>(url: string, { unzip }?: {
3
3
  unzip?: boolean | undefined;
4
4
  }): Promise<T | null>;
5
- export declare function download(data: string | Blob, filename: string, type: string): void;
5
+ export type DownloadData = string | Blob | ArrayBuffer | ArrayBufferView<ArrayBuffer>;
6
+ export declare function download(data: DownloadData, filename: string, type: string): void;
@@ -4,4 +4,11 @@ export interface FileDropOptions {
4
4
  on_error?: (msg: string) => void;
5
5
  set_loading?: (loading: boolean) => void;
6
6
  }
7
+ export declare const drag_over_handlers: (opts: {
8
+ allow?: () => boolean;
9
+ set_dragover: (over: boolean) => void;
10
+ }) => {
11
+ ondragover: (event: DragEvent) => void;
12
+ ondragleave: () => void;
13
+ };
7
14
  export declare const create_file_drop_handler: (opts: FileDropOptions) => ((event: DragEvent) => Promise<void>);
@@ -2,6 +2,19 @@
2
2
  import { decompress_file } from './decompress';
3
3
  import { handle_url_drop } from './url-drop';
4
4
  import { to_error } from '../utils';
5
+ // Drag-over visual-state handlers for file-drop zones; spread onto the drop target
6
+ // alongside `ondrop` from create_file_drop_handler
7
+ export const drag_over_handlers = (opts) => ({
8
+ // preventDefault on dragover marks the element as a valid drop target; dragleave
9
+ // has no default action to cancel, so it only clears the visual state
10
+ ondragover: (event) => {
11
+ event.preventDefault();
12
+ if (opts.allow && !opts.allow())
13
+ return;
14
+ opts.set_dragover(true);
15
+ },
16
+ ondragleave: () => opts.set_dragover(false),
17
+ });
5
18
  // Handles URL drops (from FilePicker), direct file drops with decompression,
6
19
  // loading state, and error reporting.
7
20
  export const create_file_drop_handler = (opts) => async (event) => {
@@ -1,3 +1,4 @@
1
+ export { default as ExportPane } from './ExportPane.svelte';
1
2
  export * from './decompress';
2
3
  export * from './export';
3
4
  export * from './fetch';
@@ -5,3 +6,4 @@ export * from './file-drop';
5
6
  export * from './is-binary';
6
7
  export type * from './types';
7
8
  export * from './url-drop';
9
+ export declare function strip_compression_extensions(filename: string): string;
package/dist/io/index.js CHANGED
@@ -1,6 +1,16 @@
1
+ import { COMPRESSION_EXTENSIONS_REGEX } from '../constants';
2
+ export { default as ExportPane } from './ExportPane.svelte';
1
3
  export * from './decompress';
2
4
  export * from './export';
3
5
  export * from './fetch';
4
6
  export * from './file-drop';
5
7
  export * from './is-binary';
6
8
  export * from './url-drop';
9
+ // Lowercase a filename and strip all trailing compression extensions (.gz, .zip, ...)
10
+ export function strip_compression_extensions(filename) {
11
+ let base_name = filename.toLowerCase();
12
+ while (COMPRESSION_EXTENSIONS_REGEX.test(base_name)) {
13
+ base_name = base_name.replace(COMPRESSION_EXTENSIONS_REGEX, ``);
14
+ }
15
+ return base_name;
16
+ }
@@ -6,3 +6,16 @@ export interface FileInfo {
6
6
  category?: string;
7
7
  category_icon?: string;
8
8
  }
9
+ export interface ExportItem {
10
+ label: string;
11
+ hint?: string;
12
+ disabled?: boolean;
13
+ on_download?: () => void;
14
+ copy_text?: () => string | null;
15
+ show_dpi?: boolean;
16
+ }
17
+ export interface ExportSection {
18
+ title?: string;
19
+ tooltip?: string;
20
+ items: ExportItem[];
21
+ }
@@ -1,10 +1,23 @@
1
1
  // Parsers for volumetric data file formats (VASP CHGCAR, Gaussian .cube)
2
- import { COMPRESSION_EXTENSIONS_REGEX, VASP_VOLUMETRIC_REGEX } from '../constants';
3
- import { ELEM_SYMBOLS } from '../labels';
2
+ import { ATOMIC_NUMBER_TO_SYMBOL } from '../composition/parse';
3
+ import { VASP_VOLUMETRIC_REGEX } from '../constants';
4
+ import { coerce_elem_symbol, FALLBACK_ELEMENTS } from '../element';
5
+ import { strip_compression_extensions } from '../io';
4
6
  import * as math from '../math';
5
7
  import { wrap_to_unit_cell } from '../structure/pbc';
8
+ import { make_site } from '../structure/site';
6
9
  // Bohr radius in Angstroms (for Gaussian .cube unit conversion)
7
10
  const BOHR_TO_ANGSTROM = 0.529177249;
11
+ // === Parse error contract ===
12
+ // parse_chgcar/parse_cube return null and record reasons here (mirrored to console.error).
13
+ // parse_volumetric_file resets per call and throws when the FILENAME identifies a volumetric
14
+ // format that fails to parse, but returns null when content doesn't look volumetric at all
15
+ // (probe semantics — callers then fall back to structure parsing).
16
+ let vol_parse_errors = [];
17
+ const vol_error = (message) => {
18
+ vol_parse_errors.push(message);
19
+ console.error(message);
20
+ };
8
21
  // === Fast number parsing utilities ===
9
22
  // Parse whitespace-separated numbers directly from a string, starting at `pos`.
10
23
  // Writes into a pre-allocated Float64Array and returns { count, end_pos }.
@@ -177,7 +190,7 @@ export function parse_chgcar(content) {
177
190
  cur = read_line(content, pos);
178
191
  const scale_factor = parseFloat(cur.line);
179
192
  if (isNaN(scale_factor)) {
180
- console.error(`Invalid scaling factor in CHGCAR`);
193
+ vol_error(`Invalid scaling factor in CHGCAR`);
181
194
  return null;
182
195
  }
183
196
  pos = cur.next;
@@ -195,7 +208,7 @@ export function parse_chgcar(content) {
195
208
  let atom_counts = [];
196
209
  cur = read_line(content, pos);
197
210
  if (pos >= content.length) {
198
- console.error(`CHGCAR: file ends before element/count lines`);
211
+ vol_error(`CHGCAR: file ends before element/count lines`);
199
212
  return null;
200
213
  }
201
214
  // Detect VASP 5+ format (has element symbols before counts)
@@ -206,19 +219,18 @@ export function parse_chgcar(content) {
206
219
  pos = cur.next;
207
220
  cur = read_line(content, pos);
208
221
  if (pos >= content.length) {
209
- console.error(`CHGCAR: file ends before atom counts line`);
222
+ vol_error(`CHGCAR: file ends before atom counts line`);
210
223
  return null;
211
224
  }
212
225
  atom_counts = cur.line.trim().split(/\s+/).map(Number);
213
226
  }
214
227
  else {
215
228
  atom_counts = cur.line.trim().split(/\s+/).map(Number);
216
- const fallback_elements = [`H`, `He`, `Li`, `Be`, `B`, `C`, `N`, `O`, `F`, `Ne`];
217
- element_symbols = atom_counts.map((_count, idx) => fallback_elements[idx % fallback_elements.length]);
229
+ element_symbols = atom_counts.map((_count, idx) => FALLBACK_ELEMENTS[idx % FALLBACK_ELEMENTS.length]);
218
230
  }
219
231
  pos = cur.next;
220
232
  if (pos >= content.length) {
221
- console.error(`CHGCAR: file ends before coordinate mode line`);
233
+ vol_error(`CHGCAR: file ends before coordinate mode line`);
222
234
  return null;
223
235
  }
224
236
  // Check for selective dynamics line
@@ -228,7 +240,7 @@ export function parse_chgcar(content) {
228
240
  cur = read_line(content, pos);
229
241
  }
230
242
  if (pos >= content.length) {
231
- console.error(`CHGCAR: file ends before coordinate mode line`);
243
+ vol_error(`CHGCAR: file ends before coordinate mode line`);
232
244
  return null;
233
245
  }
234
246
  // Coordinate mode line
@@ -242,18 +254,18 @@ export function parse_chgcar(content) {
242
254
  ({ cart_to_frac, frac_to_cart } = math.create_lattice_converters(lattice));
243
255
  }
244
256
  catch {
245
- console.error(`CHGCAR: lattice matrix is singular; cannot convert coordinates`);
257
+ vol_error(`CHGCAR: lattice matrix is singular; cannot convert coordinates`);
246
258
  return null;
247
259
  }
248
260
  const sites = [];
249
261
  let atom_idx = 0;
250
262
  for (let elem_idx = 0; elem_idx < element_symbols.length; elem_idx++) {
251
263
  const symbol = element_symbols[elem_idx].split(/[_/]/)[0];
252
- const element = (ELEM_SYMBOLS.includes(symbol) ? symbol : `H`);
264
+ const element = coerce_elem_symbol(symbol) ?? `H`;
253
265
  const count = atom_counts[elem_idx];
254
266
  for (let count_idx = 0; count_idx < count; count_idx++) {
255
267
  if (pos >= content.length) {
256
- console.error(`CHGCAR: file ends before all atom coordinates are read`);
268
+ vol_error(`CHGCAR: file ends before all atom coordinates are read`);
257
269
  return null;
258
270
  }
259
271
  cur = read_line(content, pos);
@@ -270,13 +282,7 @@ export function parse_chgcar(content) {
270
282
  const raw = cart_to_frac(xyz);
271
283
  abc = wrap_to_unit_cell(raw);
272
284
  }
273
- sites.push({
274
- species: [{ element, occu: 1, oxidation_state: 0 }],
275
- abc,
276
- xyz,
277
- label: `${element}${atom_idx + count_idx + 1}`,
278
- properties: {},
279
- });
285
+ sites.push(make_site(element, abc, xyz, `${element}${atom_idx + count_idx + 1}`));
280
286
  }
281
287
  atom_idx += count;
282
288
  }
@@ -348,7 +354,7 @@ export function parse_chgcar(content) {
348
354
  }
349
355
  }
350
356
  if (volumes.length === 0) {
351
- console.error(`No volumetric data found in CHGCAR`);
357
+ vol_error(`No volumetric data found in CHGCAR`);
352
358
  return null;
353
359
  }
354
360
  return { structure, volumes };
@@ -365,7 +371,7 @@ export function parse_cube(content, options = {}) {
365
371
  line_count++;
366
372
  }
367
373
  if (line_count < 6) {
368
- console.error(`.cube file too short`);
374
+ vol_error(`.cube file too short`);
369
375
  return null;
370
376
  }
371
377
  // Parse header (first 6 lines + atom lines)
@@ -376,7 +382,7 @@ export function parse_cube(content, options = {}) {
376
382
  // (negative n_atoms indicates orbital data with extra header line)
377
383
  const line2 = header.lines[2].split(/\s+/).map(Number);
378
384
  if (line2.length < 4 || line2.some(isNaN)) {
379
- console.error(`.cube header line 3 malformed: expected 4 numbers`);
385
+ vol_error(`.cube header line 3 malformed: expected 4 numbers`);
380
386
  return null;
381
387
  }
382
388
  const n_atoms = Math.abs(line2[0]);
@@ -390,7 +396,7 @@ export function parse_cube(content, options = {}) {
390
396
  header.lines[5].split(/\s+/).map(Number),
391
397
  ];
392
398
  if (voxel_lines.some((line) => line.length < 4 || line.some(isNaN))) {
393
- console.error(`.cube voxel lines malformed: expected 4 numbers per line`);
399
+ vol_error(`.cube voxel lines malformed: expected 4 numbers per line`);
394
400
  return null;
395
401
  }
396
402
  const n_grid = [
@@ -423,7 +429,7 @@ export function parse_cube(content, options = {}) {
423
429
  }
424
430
  catch {
425
431
  // Non-periodic system (molecule), use identity
426
- cube_cart_to_frac = (v) => [v[0], v[1], v[2]];
432
+ cube_cart_to_frac = (vec) => [vec[0], vec[1], vec[2]];
427
433
  }
428
434
  for (let atom_idx = 0; atom_idx < n_atoms; atom_idx++) {
429
435
  const cur = read_line(content, pos);
@@ -444,13 +450,7 @@ export function parse_cube(content, options = {}) {
444
450
  const xyz = math.subtract(raw_xyz, origin);
445
451
  const abc = cube_cart_to_frac(xyz);
446
452
  const element = atomic_number_to_symbol(atom_line[0]);
447
- sites.push({
448
- species: [{ element, occu: 1, oxidation_state: 0 }],
449
- abc,
450
- xyz,
451
- label: `${element}${atom_idx + 1}`,
452
- properties: {},
453
- });
453
+ sites.push(make_site(element, abc, xyz, `${element}${atom_idx + 1}`));
454
454
  }
455
455
  // Build structure
456
456
  const lattice_params = math.calc_lattice_params(lattice);
@@ -474,7 +474,7 @@ export function parse_cube(content, options = {}) {
474
474
  if (parsed_count < total_points) {
475
475
  console.warn(`.cube: expected ${total_points} data values, got ${parsed_count}`);
476
476
  if (parsed_count === 0) {
477
- console.error(`No volumetric data found in .cube file`);
477
+ vol_error(`No volumetric data found in .cube file`);
478
478
  return null;
479
479
  }
480
480
  }
@@ -499,22 +499,24 @@ export function parse_cube(content, options = {}) {
499
499
  ];
500
500
  return { structure, volumes };
501
501
  }
502
- // Convert atomic number to element symbol using ELEM_SYMBOLS (1-indexed: H=1, He=2, ...)
503
- function atomic_number_to_symbol(atomic_number) {
504
- // ELEM_SYMBOLS is 0-indexed (H at index 0), atomic numbers are 1-indexed
505
- const idx = atomic_number - 1;
506
- return idx >= 0 && idx < ELEM_SYMBOLS.length ? ELEM_SYMBOLS[idx] : `H`;
507
- }
508
- // Auto-detect and parse volumetric file format based on filename and content
502
+ // Convert atomic number to element symbol (falls back to H for unknown numbers)
503
+ const atomic_number_to_symbol = (atomic_number) => ATOMIC_NUMBER_TO_SYMBOL[atomic_number] ?? `H`;
504
+ // Auto-detect and parse volumetric file by filename + content (see parse error contract at top)
509
505
  export function parse_volumetric_file(content, filename) {
506
+ vol_parse_errors = [];
507
+ const fail = (format) => {
508
+ const detail = vol_parse_errors.length ? `: ${vol_parse_errors.join(`; `)}` : ``;
509
+ throw new Error(`Failed to parse ${format} file${filename ? ` '${filename}'` : ``}${detail}`);
510
+ };
510
511
  // Strip compression suffixes so "CHGCAR.gz" and "molecule.cube.bz2" match correctly
511
- const lower_name = (filename ?? ``).toLowerCase().replace(COMPRESSION_EXTENSIONS_REGEX, ``);
512
- // Extension-based detection
512
+ const lower_name = strip_compression_extensions(filename ?? ``);
513
+ // Extension-based detection (filename is authoritative: parse failure throws)
513
514
  if (lower_name.endsWith(`.cube`))
514
- return parse_cube(content);
515
+ return parse_cube(content) ?? fail(`.cube`);
515
516
  // VASP volumetric file detection by filename
516
- if (VASP_VOLUMETRIC_REGEX.test(lower_name))
517
- return parse_chgcar(content);
517
+ if (VASP_VOLUMETRIC_REGEX.test(lower_name)) {
518
+ return parse_chgcar(content) ?? fail(`VASP volumetric (CHGCAR-like)`);
519
+ }
518
520
  // Content-based detection (only parse first few lines, not the whole file)
519
521
  // Find enough lines for detection without splitting the entire string
520
522
  const detection_end = find_line_offset(content, 10);
package/dist/labels.js CHANGED
@@ -16,7 +16,7 @@ export const symbol_names = [
16
16
  ...new Set([...d3_symbols.symbolsFill, ...d3_symbols.symbolsStroke]),
17
17
  ]
18
18
  .map(name_for_symbol)
19
- .filter((n) => n !== null);
19
+ .filter((name) => name !== null);
20
20
  export const symbol_map = Object.fromEntries(
21
21
  // Symbol lookup from d3-shape
22
22
  symbol_names.map((name) => [name, d3_symbols[`symbol${name}`]]));