matterviz 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/FilePicker.svelte +40 -50
  2. package/dist/api/optimade.js +10 -7
  3. package/dist/app.css +15 -0
  4. package/dist/brillouin/BrillouinZone.svelte +18 -9
  5. package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
  6. package/dist/brillouin/compute.js +10 -16
  7. package/dist/brillouin/geometry.js +2 -2
  8. package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
  9. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
  10. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
  11. package/dist/chempot-diagram/async-compute.svelte.js +12 -13
  12. package/dist/chempot-diagram/color.d.ts +21 -1
  13. package/dist/chempot-diagram/color.js +70 -1
  14. package/dist/chempot-diagram/compute.d.ts +7 -0
  15. package/dist/chempot-diagram/compute.js +85 -26
  16. package/dist/chempot-diagram/export.js +1 -6
  17. package/dist/chempot-diagram/temperature.js +9 -10
  18. package/dist/composition/BarChart.svelte +7 -9
  19. package/dist/composition/Composition.svelte +3 -8
  20. package/dist/composition/Formula.svelte +2 -2
  21. package/dist/composition/FormulaFilter.svelte +27 -93
  22. package/dist/composition/format.js +6 -8
  23. package/dist/composition/parse.js +50 -105
  24. package/dist/constants.d.ts +0 -1
  25. package/dist/constants.js +6 -17
  26. package/dist/convex-hull/ConvexHull2D.svelte +0 -2
  27. package/dist/convex-hull/ConvexHull3D.svelte +19 -47
  28. package/dist/convex-hull/ConvexHull4D.svelte +65 -95
  29. package/dist/convex-hull/StructurePopup.svelte +1 -1
  30. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  31. package/dist/convex-hull/helpers.js +1 -7
  32. package/dist/convex-hull/thermodynamics.js +19 -27
  33. package/dist/coordination/CoordinationBarPlot.svelte +12 -21
  34. package/dist/element/BohrAtom.svelte +0 -1
  35. package/dist/element/ElementTile.svelte +26 -44
  36. package/dist/element/data.d.ts +2 -0
  37. package/dist/element/data.js +5 -1
  38. package/dist/element/index.d.ts +1 -1
  39. package/dist/element/index.js +1 -1
  40. package/dist/fermi-surface/FermiSurface.svelte +22 -23
  41. package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
  42. package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
  43. package/dist/fermi-surface/compute.js +22 -55
  44. package/dist/fermi-surface/parse.js +38 -70
  45. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  46. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  47. package/dist/file-viewer/PlotPanel.svelte +350 -0
  48. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  49. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  50. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  51. package/dist/file-viewer/detect.d.ts +11 -0
  52. package/dist/file-viewer/detect.js +381 -0
  53. package/dist/file-viewer/eligibility.d.ts +4 -0
  54. package/dist/file-viewer/eligibility.js +53 -0
  55. package/dist/file-viewer/host-protocol.d.ts +50 -0
  56. package/dist/file-viewer/host-transfer.d.ts +25 -0
  57. package/dist/file-viewer/host-transfer.js +42 -0
  58. package/dist/file-viewer/index.d.ts +5 -0
  59. package/dist/file-viewer/index.js +5 -0
  60. package/dist/file-viewer/main.d.ts +44 -0
  61. package/dist/file-viewer/main.js +535 -0
  62. package/dist/file-viewer/parse.d.ts +9 -0
  63. package/dist/file-viewer/parse.js +170 -0
  64. package/dist/file-viewer/plot-utils.d.ts +24 -0
  65. package/dist/file-viewer/plot-utils.js +191 -0
  66. package/dist/file-viewer/types.d.ts +4 -0
  67. package/dist/file-viewer/types.js +5 -0
  68. package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
  69. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.js +1 -1
  72. package/dist/io/ExportPane.svelte +1 -1
  73. package/dist/io/decompress.d.ts +2 -0
  74. package/dist/io/decompress.js +3 -2
  75. package/dist/io/export.d.ts +7 -4
  76. package/dist/io/export.js +70 -18
  77. package/dist/io/file-drop.js +60 -35
  78. package/dist/io/url-drop.d.ts +2 -0
  79. package/dist/io/url-drop.js +28 -16
  80. package/dist/isosurface/Isosurface.svelte +687 -196
  81. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  82. package/dist/isosurface/IsosurfaceControls.svelte +564 -85
  83. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  84. package/dist/isosurface/VolumeSlice.svelte +224 -0
  85. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  86. package/dist/isosurface/coloring.d.ts +20 -0
  87. package/dist/isosurface/coloring.js +118 -0
  88. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  89. package/dist/isosurface/geometry.worker.d.ts +1 -0
  90. package/dist/isosurface/geometry.worker.js +59 -0
  91. package/dist/isosurface/grid.d.ts +10 -0
  92. package/dist/isosurface/grid.js +31 -0
  93. package/dist/isosurface/index.d.ts +4 -0
  94. package/dist/isosurface/index.js +4 -0
  95. package/dist/isosurface/profile.d.ts +12 -0
  96. package/dist/isosurface/profile.js +19 -0
  97. package/dist/isosurface/sampling.d.ts +34 -0
  98. package/dist/isosurface/sampling.js +408 -0
  99. package/dist/isosurface/slice-rendering.d.ts +13 -0
  100. package/dist/isosurface/slice-rendering.js +79 -0
  101. package/dist/isosurface/slice.d.ts +22 -2
  102. package/dist/isosurface/slice.js +200 -116
  103. package/dist/isosurface/types.d.ts +24 -1
  104. package/dist/isosurface/types.js +120 -8
  105. package/dist/json-path.d.ts +4 -0
  106. package/dist/json-path.js +89 -0
  107. package/dist/labels.js +1 -1
  108. package/dist/layout/InfoCard.svelte +1 -3
  109. package/dist/layout/SettingsSection.svelte +52 -61
  110. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  111. package/dist/layout/ViewerChrome.svelte +7 -1
  112. package/dist/layout/json-tree/JsonNode.svelte +14 -48
  113. package/dist/layout/json-tree/JsonTree.svelte +48 -95
  114. package/dist/layout/json-tree/JsonValue.svelte +3 -18
  115. package/dist/layout/json-tree/utils.d.ts +0 -3
  116. package/dist/layout/json-tree/utils.js +50 -179
  117. package/dist/marching-cubes.d.ts +8 -1
  118. package/dist/marching-cubes.js +115 -48
  119. package/dist/math.d.ts +3 -2
  120. package/dist/math.js +29 -38
  121. package/dist/overlays/ContextMenu.svelte +15 -32
  122. package/dist/overlays/DraggablePane.svelte +39 -58
  123. package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
  124. package/dist/overlays/GlassChip.svelte +3 -1
  125. package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
  126. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
  127. package/dist/phase-diagram/build-diagram.js +1 -6
  128. package/dist/phase-diagram/svg-to-diagram.js +9 -14
  129. package/dist/plot/bar/BarPlot.svelte +38 -42
  130. package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
  131. package/dist/plot/box/BoxPlot.svelte +27 -23
  132. package/dist/plot/core/auto-place.d.ts +1 -1
  133. package/dist/plot/core/auto-place.js +1 -1
  134. package/dist/plot/core/data-transform.d.ts +1 -2
  135. package/dist/plot/core/data-transform.js +0 -10
  136. package/dist/plot/core/layout.d.ts +12 -5
  137. package/dist/plot/core/layout.js +37 -27
  138. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  139. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  140. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  141. package/dist/plot/core/placed-tween.svelte.js +68 -21
  142. package/dist/plot/core/svg.js +0 -1
  143. package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
  144. package/dist/plot/core/utils/hierarchy-chart.js +12 -0
  145. package/dist/plot/core/utils/hierarchy-labels.js +3 -2
  146. package/dist/plot/histogram/Histogram.svelte +21 -26
  147. package/dist/plot/sankey/sankey.js +13 -9
  148. package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
  149. package/dist/plot/scatter/ScatterPlot.svelte +67 -52
  150. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  151. package/dist/plot/scatter/adaptive-density.js +36 -20
  152. package/dist/plot/sunburst/Sunburst.svelte +20 -20
  153. package/dist/plot/treemap/Treemap.svelte +175 -60
  154. package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
  155. package/dist/plot/treemap/index.d.ts +1 -0
  156. package/dist/plot/treemap/labels.d.ts +38 -0
  157. package/dist/plot/treemap/labels.js +80 -0
  158. package/dist/plot/treemap/treemap.d.ts +4 -2
  159. package/dist/plot/treemap/treemap.js +5 -2
  160. package/dist/rdf/RdfPlot.svelte +17 -35
  161. package/dist/rdf/calc-rdf.js +59 -44
  162. package/dist/rdf/index.d.ts +0 -1
  163. package/dist/sanitize.js +84 -26
  164. package/dist/scene/SceneCamera.svelte +1 -1
  165. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  166. package/dist/scene/index.d.ts +1 -2
  167. package/dist/scene/index.js +1 -1
  168. package/dist/scene/{props.js → props.svelte.js} +15 -1
  169. package/dist/settings.js +159 -482
  170. package/dist/spectral/Bands.svelte +151 -158
  171. package/dist/spectral/BrillouinBandsDos.svelte +4 -19
  172. package/dist/spectral/Dos.svelte +26 -24
  173. package/dist/spectral/helpers.js +40 -105
  174. package/dist/structure/AtomLegend.svelte +8 -13
  175. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  176. package/dist/structure/CellSelect.svelte +35 -10
  177. package/dist/structure/Structure.svelte +177 -99
  178. package/dist/structure/Structure.svelte.d.ts +1 -1
  179. package/dist/structure/StructureCarousel.svelte +10 -3
  180. package/dist/structure/StructureControls.svelte +8 -26
  181. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  182. package/dist/structure/StructureInfoPane.svelte +65 -76
  183. package/dist/structure/StructureScene.svelte +75 -109
  184. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  185. package/dist/structure/StructureViewport.svelte +120 -55
  186. package/dist/structure/StructureViewport.svelte.d.ts +3 -1
  187. package/dist/structure/atom-properties.d.ts +1 -1
  188. package/dist/structure/atom-properties.js +4 -3
  189. package/dist/structure/bond-order-perception.js +3 -8
  190. package/dist/structure/bonding.d.ts +0 -1
  191. package/dist/structure/bonding.js +16 -23
  192. package/dist/structure/export.js +50 -74
  193. package/dist/structure/format-detect.js +5 -7
  194. package/dist/structure/index.js +6 -12
  195. package/dist/structure/parse.js +38 -100
  196. package/dist/structure/partial-occupancy.js +7 -4
  197. package/dist/structure/pbc.js +8 -10
  198. package/dist/structure/polyhedra.js +5 -4
  199. package/dist/structure/prop-groups.d.ts +85 -0
  200. package/dist/structure/prop-groups.js +18 -0
  201. package/dist/symmetry/SymmetryStats.svelte +0 -4
  202. package/dist/symmetry/WyckoffTable.svelte +5 -4
  203. package/dist/symmetry/cell-transform.js +5 -14
  204. package/dist/symmetry/index.js +6 -9
  205. package/dist/table/HeatmapTable.svelte +61 -103
  206. package/dist/table/ToggleMenu.svelte +166 -44
  207. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  208. package/dist/theme/embedded.d.ts +18 -0
  209. package/dist/theme/embedded.js +206 -0
  210. package/dist/theme/index.d.ts +4 -0
  211. package/dist/theme/index.js +1 -2
  212. package/dist/theme/themes.mjs +31 -60
  213. package/dist/tooltip/TooltipContent.svelte +1 -1
  214. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  215. package/dist/tooltip/index.d.ts +8 -1
  216. package/dist/trajectory/Trajectory.svelte +140 -120
  217. package/dist/trajectory/TrajectoryError.svelte +1 -66
  218. package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
  219. package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
  220. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  221. package/dist/trajectory/extract.js +19 -29
  222. package/dist/trajectory/format-detect.d.ts +2 -0
  223. package/dist/trajectory/format-detect.js +7 -5
  224. package/dist/trajectory/frame-reader.js +3 -4
  225. package/dist/trajectory/helpers.js +16 -49
  226. package/dist/trajectory/index.js +2 -8
  227. package/dist/trajectory/parse/ase.js +1 -1
  228. package/dist/trajectory/parse/index.d.ts +4 -3
  229. package/dist/trajectory/parse/index.js +12 -17
  230. package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
  231. package/dist/trajectory/parse/vaspout-h5.js +1 -4
  232. package/dist/trajectory/parse/xyz.js +48 -2
  233. package/dist/trajectory/plotting.js +14 -37
  234. package/dist/xrd/XrdPlot.svelte +43 -68
  235. package/dist/xrd/calc-xrd.js +22 -58
  236. package/dist/xrd/parse.js +13 -22
  237. package/package.json +40 -8
  238. package/readme.md +6 -5
  239. package/dist/scene/types.d.ts +0 -26
  240. package/dist/tooltip/types.d.ts +0 -8
  241. package/dist/trajectory/constants.d.ts +0 -6
  242. package/dist/trajectory/constants.js +0 -11
  243. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  244. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -6,14 +6,17 @@
6
6
  import { StatusMessage } from '../feedback'
7
7
  import Spinner from '../feedback/Spinner.svelte'
8
8
  import Icon from '../Icon.svelte'
9
- import { create_file_drop_handler, drag_over_handlers, load_from_url } from '../io'
9
+ import * as io from '../io'
10
10
  import { forward_window_keydown, handle_and_prevent } from '../keyboard'
11
11
  import { parse_volumetric_file } from '../isosurface/parse'
12
12
  import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
13
13
  import {
14
14
  auto_isosurface_settings,
15
15
  DEFAULT_ISOSURFACE_SETTINGS,
16
- tile_volumetric_data,
16
+ label_file_volumes,
17
+ lattices_match,
18
+ materialize_layers,
19
+ merge_imported_volumes,
17
20
  } from '../isosurface/types'
18
21
  import { type FullscreenToggleProp, toggle_fullscreen, ViewerChrome } from '../layout'
19
22
  import { sync_fullscreen } from '../layout/fullscreen.svelte'
@@ -302,23 +305,39 @@
302
305
  // completions so a newer data_url (or an externally-supplied structure, via the cleanup)
303
306
  // can't be clobbered by a slow earlier fetch.
304
307
  let data_url_load_id = 0
308
+ let loaded_data_url: string | undefined
309
+ let url_owned_structure: AnyStructure | undefined
310
+
305
311
  $effect(() => {
306
- if (!data_url || structure) return
312
+ const requested_url = data_url
313
+ const current_structure = structure
314
+ // Host on_file_drop owns the structure; don't treat it as caller-owned cancel.
315
+ const caller_owns_structure = Boolean(
316
+ !on_file_drop && current_structure && current_structure !== url_owned_structure,
317
+ )
318
+ if (!requested_url || caller_owns_structure) {
319
+ loaded_data_url = undefined
320
+ url_owned_structure = undefined
321
+ return
322
+ }
323
+ if (loaded_data_url === requested_url) return
324
+
307
325
  const load_id = ++data_url_load_id
308
326
  const is_current = () => load_id === data_url_load_id
309
327
  loading = true
310
328
  error_msg = undefined
311
329
 
312
- load_from_url(data_url, (content, filename) => {
330
+ io.load_from_url(requested_url, (content, filename) => {
313
331
  if (!is_current()) return // stale response
314
- if (on_file_drop) on_file_drop(content, filename)
315
- else {
332
+ if (on_file_drop) {
333
+ on_file_drop(content, filename)
334
+ loaded_data_url = requested_url
335
+ } else {
316
336
  // Parse structure internally when no handler provided
317
337
  try {
318
- const text_content =
319
- content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
320
- const parsed = parse_file_content(text_content, filename)
321
- emit_file_load_event(parsed, filename, content)
338
+ parse_and_emit_file(content, filename)
339
+ url_owned_structure = structure
340
+ loaded_data_url = requested_url
322
341
  } catch (error) {
323
342
  error_msg = `Failed to parse structure: ${to_error(error).message}`
324
343
  on_error?.({ error_msg, filename })
@@ -329,7 +348,7 @@
329
348
  if (!is_current()) return
330
349
  console.error(`Failed to load structure from URL:`, error)
331
350
  error_msg = `Failed to load structure: ${error.message}`
332
- on_error?.({ error_msg, filename: data_url })
351
+ on_error?.({ error_msg, filename: io.basename_from_url(requested_url) })
333
352
  })
334
353
  .finally(() => {
335
354
  if (is_current()) loading = false
@@ -680,6 +699,14 @@
680
699
  // Flag set before internal edits (undo/redo/delete/add/move) to distinguish
681
700
  // them from external structure changes (file load, trajectory step, etc.)
682
701
  let is_internal_edit = false
702
+ // Claim URL ownership before regular effects so internal edits aren't treated as caller-owned.
703
+ $effect.pre(() => {
704
+ void structure
705
+ if (is_internal_edit && loaded_data_url && loaded_data_url === data_url && structure) {
706
+ url_owned_structure = structure
707
+ }
708
+ })
709
+
683
710
  // Add-atom sub-mode state (bound to StructureScene)
684
711
  let add_atom_mode = $state(false)
685
712
  let add_element = $state<ElementSymbol>(`C` as ElementSymbol)
@@ -830,7 +857,9 @@
830
857
  $effect(() => {
831
858
  viewer_active = hovered || focused
832
859
  })
833
- let scene_gizmo = $derived(viewer_active && (scene_props.gizmo ?? scene_props.show_gizmo))
860
+ // Keep the gizmo mounted whenever enabled — toggling via `{#if gizmo}` on hover remounts
861
+ // OrbitControls/Gizmo and resets camera rotation. Visibility is CSS-gated by `gizmo-visible`.
862
+ let scene_gizmo = $derived(scene_props.gizmo ?? scene_props.show_gizmo)
834
863
  let active_scene_sites = $derived([
835
864
  ...new SvelteSet([...(scene_props.active_sites ?? []), ...highlighted_sites]),
836
865
  ])
@@ -898,16 +927,16 @@
898
927
  })
899
928
  })
900
929
 
901
- // Tile volumetric data to match supercell when active.
902
- // Gate on !supercell_loading so the tiled volume and supercell structure update
903
- // in the same frame (large supercells defer structure via setTimeout).
904
- let supercell_volume = $derived.by(() => {
905
- const vol = volumetric_data?.[active_volume_idx]
906
- if (!vol || !has_supercell || supercell_loading) return vol
930
+ // Supercell tiling factors for isosurface geometry (tiling itself happens in
931
+ // the Isosurface component so color sampling can use the untiled full-res
932
+ // volumes). Gate on !supercell_loading so tiled surfaces and the supercell
933
+ // structure update in the same frame (large supercells defer via setTimeout).
934
+ let volume_scaling = $derived.by((): Vec3 => {
935
+ if (!has_supercell || supercell_loading) return [1, 1, 1]
907
936
  try {
908
- return tile_volumetric_data(vol, parse_supercell_scaling(supercell_scaling))
937
+ return parse_supercell_scaling(supercell_scaling)
909
938
  } catch {
910
- return vol
939
+ return [1, 1, 1]
911
940
  }
912
941
  })
913
942
 
@@ -1034,7 +1063,9 @@
1034
1063
  scene_props,
1035
1064
  gizmo: scene_gizmo,
1036
1065
  lattice_props,
1037
- volumetric_data: supercell_volume,
1066
+ volumetric_data,
1067
+ volume_scaling,
1068
+ active_volume_idx,
1038
1069
  isosurface_settings,
1039
1070
  bond_edits_enabled,
1040
1071
  bond_edit_order,
@@ -1100,18 +1131,65 @@
1100
1131
  total_atoms: loaded_structure.sites?.length || 0,
1101
1132
  })
1102
1133
 
1134
+ // The currently loaded structure's lattice matrix (undefined for molecules/none)
1135
+ const current_lattice_matrix = () =>
1136
+ structure && `lattice` in structure ? structure.lattice.matrix : undefined
1137
+ const shares_current_lattice = (matrix: Parameters<typeof lattices_match>[1]): boolean =>
1138
+ lattices_match(current_lattice_matrix(), matrix)
1139
+
1103
1140
  // Try to parse content as a volumetric file, setting both structure and volumetric data.
1104
1141
  // Delegates format detection entirely to parse_volumetric_file (filename + content sniffing).
1142
+ // When the file describes the same cell as already-loaded volumes, its volumes are
1143
+ // APPENDED (or replaced in place on re-import of the same source file) so multiple
1144
+ // fields — e.g. density + ESP — coexist and can cross-color each other's isosurfaces.
1145
+ // A file with a different lattice replaces the current structure and volumes.
1105
1146
  // Returns the parsed structure on success, or null if the file isn't a volumetric format.
1106
1147
  function try_parse_volumetric(text_content: string, filename: string): AnyStructure | null {
1107
1148
  const vol_result = parse_volumetric_file(text_content, filename)
1108
1149
  if (!vol_result) return null
1150
+
1151
+ const incoming = label_file_volumes(vol_result.volumes, filename)
1152
+ const same_cell = shares_current_lattice(vol_result.structure.lattice?.matrix)
1153
+ const added_toast = (count: number) =>
1154
+ `Added ${count} volume${count > 1 ? `s` : ``} from ${filename}`
1155
+
1156
+ if (same_cell && structure) {
1157
+ // Same cell: keep the structure and camera, only update volumes
1158
+ if (volumetric_data?.length) {
1159
+ // Materialize the implicit single surface into explicit layers so existing
1160
+ // surfaces survive the transition to multi-volume mode
1161
+ const merged = merge_imported_volumes(
1162
+ volumetric_data,
1163
+ materialize_layers(isosurface_settings, active_volume_idx),
1164
+ incoming,
1165
+ active_volume_idx,
1166
+ )
1167
+ volumetric_data = merged.volumes
1168
+ isosurface_settings = { ...isosurface_settings, layers: merged.layers }
1169
+ active_volume_idx = merged.first_touched_idx
1170
+ show_toast(
1171
+ merged.n_added > 0
1172
+ ? added_toast(merged.n_added)
1173
+ : `Reloaded volumes from ${filename}`,
1174
+ )
1175
+ } else if (incoming[0]) {
1176
+ // First volumetric file for this structure
1177
+ volumetric_data = incoming
1178
+ isosurface_settings = auto_isosurface_settings(incoming[0].data_range)
1179
+ active_volume_idx = 0
1180
+ show_toast(added_toast(incoming.length))
1181
+ }
1182
+ return structure
1183
+ }
1184
+
1185
+ // Replace: new system (or nothing loaded yet)
1186
+ clear_camera_state()
1109
1187
  // parse_volumetric_file extracts structure from file header;
1110
1188
  // parsers set pbc so the lattice conforms to Crystal's LatticeType
1111
1189
  structure = vol_result.structure as AnyStructure
1112
- volumetric_data = vol_result.volumes
1190
+ volumetric_data = incoming
1113
1191
  // Auto-compute reasonable isosurface settings from data range
1114
- const vol = vol_result.volumes[0]
1192
+ const vol = incoming[0]
1115
1193
  if (vol) {
1116
1194
  isosurface_settings = auto_isosurface_settings(vol.data_range)
1117
1195
  active_volume_idx = 0
@@ -1122,30 +1200,36 @@
1122
1200
  // Parse file content, trying volumetric format first then falling back to plain structure.
1123
1201
  // Returns the parsed structure on success, throws on failure.
1124
1202
  function parse_file_content(text_content: string, filename: string): AnyStructure {
1125
- clear_camera_state()
1126
1203
  const vol_struct = try_parse_volumetric(text_content, filename)
1127
1204
  if (vol_struct) return vol_struct
1128
- // Clear stale volumetric data when loading a non-volumetric file
1129
- volumetric_data = []
1130
1205
  const parsed = parse_any_structure(text_content, filename)
1131
1206
  if (!parsed) throw new Error(`Failed to parse structure from ${filename}`)
1207
+ // Keep loaded volumes and camera when the new structure describes the same
1208
+ // cell (e.g. a mixed batch drop of CHGCAR + POSCAR, in either order);
1209
+ // clear both for a genuinely new system
1210
+ const same_cell = shares_current_lattice(
1211
+ `lattice` in parsed ? parsed.lattice?.matrix : undefined,
1212
+ )
1213
+ if (!same_cell) {
1214
+ clear_camera_state()
1215
+ volumetric_data = []
1216
+ }
1132
1217
  structure = parsed
1133
1218
  return parsed
1134
1219
  }
1135
1220
 
1136
- const handle_file_drop = create_file_drop_handler({
1221
+ function parse_and_emit_file(content: string | ArrayBuffer, filename: string): void {
1222
+ const text = content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
1223
+ emit_file_load_event(parse_file_content(text, filename), filename, content)
1224
+ }
1225
+
1226
+ const handle_file_drop = io.create_file_drop_handler({
1137
1227
  allow: () => allow_file_drop,
1228
+ // Parse errors propagate so multi-file batches aggregate all failures into
1229
+ // one message instead of the last error overwriting earlier ones
1138
1230
  on_drop: (content, filename) => {
1139
1231
  if (on_file_drop) return on_file_drop(content, filename)
1140
- try {
1141
- const text_content =
1142
- content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
1143
- const parsed = parse_file_content(text_content, filename)
1144
- emit_file_load_event(parsed, filename, content)
1145
- } catch (err) {
1146
- error_msg = `Failed to parse structure: ${to_error(err).message}`
1147
- on_error?.({ error_msg, filename })
1148
- }
1232
+ parse_and_emit_file(content, filename)
1149
1233
  },
1150
1234
  on_error: (msg) => {
1151
1235
  error_msg = msg
@@ -1470,13 +1554,14 @@
1470
1554
 
1471
1555
  // Only set background override when background_color is explicitly provided
1472
1556
  $effect(() => {
1473
- if (typeof window !== `undefined` && wrapper && background_color) {
1557
+ if (!wrapper) return
1558
+ if (background_color) {
1474
1559
  // Convert opacity (0-1) to hex alpha value (00-FF)
1475
1560
  const alpha_hex = Math.round(background_opacity * 255)
1476
1561
  .toString(16)
1477
1562
  .padStart(2, `0`)
1478
1563
  wrapper.style.setProperty(`--struct-bg-override`, `${background_color}${alpha_hex}`)
1479
- } else if (typeof window !== `undefined` && wrapper) {
1564
+ } else {
1480
1565
  // Remove override to use theme system
1481
1566
  wrapper.style.removeProperty(`--struct-bg-override`)
1482
1567
  }
@@ -1500,7 +1585,7 @@
1500
1585
  <div
1501
1586
  class:dragover
1502
1587
  class:active={info_pane_open || controls_open || export_pane_open}
1503
- class:gizmo-visible={Boolean(scene_gizmo)}
1588
+ class:gizmo-visible={viewer_active && Boolean(scene_gizmo)}
1504
1589
  class:multi-view={multi_view}
1505
1590
  role="application"
1506
1591
  tabindex="0"
@@ -1517,7 +1602,7 @@
1517
1602
  }
1518
1603
  }}
1519
1604
  ondrop={handle_file_drop}
1520
- {...drag_over_handlers({
1605
+ {...io.drag_over_handlers({
1521
1606
  allow: () => allow_file_drop,
1522
1607
  set_dragover: (over) => (dragover = over),
1523
1608
  })}
@@ -1555,7 +1640,7 @@
1555
1640
  fullscreen_btn_style="padding: 0 3px"
1556
1641
  {wrapper}
1557
1642
  before={reset_camera_btn}
1558
- style="--viewer-buttons-gap: 4pt; --viewer-buttons-btn-padding: 1px 6px; --viewer-buttons-align: stretch"
1643
+ style="--viewer-buttons-gap: 4pt; --viewer-buttons-btn-padding: 1px 6px; --viewer-buttons-align: stretch; --viewer-buttons-hover-bg: transparent; --viewer-buttons-hover-color: light-dark(#000, #fff)"
1559
1644
  >
1560
1645
  {#if controls_config.visible(`multi-view`)}
1561
1646
  <button
@@ -1639,36 +1724,43 @@
1639
1724
  {/if}
1640
1725
  </div>
1641
1726
 
1642
- <!-- Undo/redo buttons (only in edit-atoms mode) -->
1643
- {#if measure_mode === `edit-atoms`}
1727
+ {#snippet undo_redo_snippet(
1728
+ buttons: {
1729
+ icon: `Undo` | `Redo`
1730
+ title: string
1731
+ stack: unknown[]
1732
+ action: () => void
1733
+ }[],
1734
+ )}
1644
1735
  <div class="undo-redo-container">
1645
- <button
1646
- type="button"
1647
- aria-label="Undo (Cmd/Ctrl+Z)"
1648
- disabled={undo_stack.length === 0}
1649
- onclick={undo}
1650
- title="Undo (Cmd/Ctrl+Z)"
1651
- class="undo-redo-btn"
1652
- >
1653
- <Icon icon="Undo" />
1654
- {#if undo_stack.length > 0}
1655
- <span class="history-count">{undo_stack.length}</span>
1656
- {/if}
1657
- </button>
1658
- <button
1659
- type="button"
1660
- aria-label="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1661
- disabled={redo_stack.length === 0}
1662
- onclick={redo}
1663
- title="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1664
- class="undo-redo-btn"
1665
- >
1666
- <Icon icon="Redo" />
1667
- {#if redo_stack.length > 0}
1668
- <span class="history-count">{redo_stack.length}</span>
1669
- {/if}
1670
- </button>
1736
+ {#each buttons as { icon, title, stack, action } (icon)}
1737
+ <button
1738
+ type="button"
1739
+ aria-label={title}
1740
+ disabled={stack.length === 0}
1741
+ onclick={action}
1742
+ {title}
1743
+ class="undo-redo-btn"
1744
+ >
1745
+ <Icon {icon} />
1746
+ {#if stack.length > 0}
1747
+ <span class="history-count">{stack.length}</span>
1748
+ {/if}
1749
+ </button>
1750
+ {/each}
1671
1751
  </div>
1752
+ {/snippet}
1753
+
1754
+ {#if measure_mode === `edit-atoms`}
1755
+ {@render undo_redo_snippet([
1756
+ { icon: `Undo`, title: `Undo (Cmd/Ctrl+Z)`, stack: undo_stack, action: undo },
1757
+ {
1758
+ icon: `Redo`,
1759
+ title: `Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)`,
1760
+ stack: redo_stack,
1761
+ action: redo,
1762
+ },
1763
+ ])}
1672
1764
  {/if}
1673
1765
 
1674
1766
  {#if measure_mode === `edit-bonds`}
@@ -1697,34 +1789,20 @@
1697
1789
  {/each}
1698
1790
  </div>
1699
1791
  </div>
1700
- <div class="undo-redo-container">
1701
- <button
1702
- type="button"
1703
- aria-label="Undo bond edit (Cmd/Ctrl+Z)"
1704
- disabled={bond_undo_stack.length === 0}
1705
- onclick={undo_bond_edit}
1706
- title="Undo bond edit (Cmd/Ctrl+Z)"
1707
- class="undo-redo-btn"
1708
- >
1709
- <Icon icon="Undo" />
1710
- {#if bond_undo_stack.length > 0}
1711
- <span class="history-count">{bond_undo_stack.length}</span>
1712
- {/if}
1713
- </button>
1714
- <button
1715
- type="button"
1716
- aria-label="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1717
- disabled={bond_redo_stack.length === 0}
1718
- onclick={redo_bond_edit}
1719
- title="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1720
- class="undo-redo-btn"
1721
- >
1722
- <Icon icon="Redo" />
1723
- {#if bond_redo_stack.length > 0}
1724
- <span class="history-count">{bond_redo_stack.length}</span>
1725
- {/if}
1726
- </button>
1727
- </div>
1792
+ {@render undo_redo_snippet([
1793
+ {
1794
+ icon: `Undo`,
1795
+ title: `Undo bond edit (Cmd/Ctrl+Z)`,
1796
+ stack: bond_undo_stack,
1797
+ action: undo_bond_edit,
1798
+ },
1799
+ {
1800
+ icon: `Redo`,
1801
+ title: `Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)`,
1802
+ stack: bond_redo_stack,
1803
+ action: redo_bond_edit,
1804
+ },
1805
+ ])}
1728
1806
  {/if}
1729
1807
 
1730
1808
  <!-- Add-atom element input (shown when add_atom_mode is active) -->
@@ -2098,7 +2176,7 @@
2098
2176
  gap: 1rem;
2099
2177
  max-width: min(90%, 400px);
2100
2178
  font-size: 0.9rem;
2101
- z-index: 1000;
2179
+ z-index: var(--z-index-viewer-tooltip, 1000);
2102
2180
  }
2103
2181
  .symmetry-error span {
2104
2182
  flex: 1;
@@ -2125,7 +2203,7 @@
2125
2203
  padding: 0.4rem 0.8rem;
2126
2204
  border-radius: var(--border-radius, 3pt);
2127
2205
  font-size: 0.8rem;
2128
- z-index: 100;
2206
+ z-index: var(--z-index-viewer-dropdown, 100);
2129
2207
  pointer-events: none;
2130
2208
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
2131
2209
  animation: toast-fade 2s ease-in-out;
@@ -76,6 +76,6 @@ type $$ComponentProps = {
76
76
  on_camera_reset?: EventHandler;
77
77
  on_bonds_change?: (bonds: StructureBond[] | undefined) => void;
78
78
  } & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`> & Omit<HTMLAttributes<HTMLDivElement>, `children`>;
79
- declare const Structure: import("svelte").Component<$$ComponentProps, {}, "height" | "width" | "dragover" | "color_scheme" | "fullscreen" | "wrapper" | "hovered" | "controls_open" | "structure" | "background_color" | "background_opacity" | "show_image_atoms" | "sym_data" | "lattice_props" | "measure_mode" | "selected_sites" | "measured_sites" | "bond_edit_mode" | "bond_edit_order" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "site_radius_overrides" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "hovered_site_idx" | "bonds" | "element_mapping" | "supercell_scaling" | "cell_type" | "loading" | "active_volume_idx" | "png_dpi" | "highlighted_sites" | "info_pane_open" | "multi_view" | "enable_measure_mode" | "error_msg" | "performance_mode" | "displayed_structure" | "symmetry_settings">;
79
+ declare const Structure: import("svelte").Component<$$ComponentProps, {}, "height" | "width" | "dragover" | "color_scheme" | "fullscreen" | "wrapper" | "hovered" | "controls_open" | "structure" | "active_volume_idx" | "background_color" | "background_opacity" | "show_image_atoms" | "sym_data" | "lattice_props" | "measure_mode" | "selected_sites" | "measured_sites" | "bond_edit_mode" | "bond_edit_order" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "site_radius_overrides" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "hovered_site_idx" | "bonds" | "element_mapping" | "supercell_scaling" | "cell_type" | "loading" | "png_dpi" | "highlighted_sites" | "info_pane_open" | "multi_view" | "enable_measure_mode" | "error_msg" | "performance_mode" | "displayed_structure" | "symmetry_settings">;
80
80
  type Structure = ReturnType<typeof Structure>;
81
81
  export default Structure;
@@ -531,6 +531,8 @@
531
531
  .structure-carousel-pager {
532
532
  position: absolute;
533
533
  z-index: 6;
534
+ /* own compositing layer, or WKWebView paints the canvas over this (see app.css) */
535
+ will-change: transform;
534
536
  inset-block-end: 12px;
535
537
  inset-inline-start: 50%;
536
538
  display: flex;
@@ -549,17 +551,22 @@
549
551
  /* hosted in a panel title bar instead of floating over the cards */
550
552
  .structure-carousel-pager.portaled {
551
553
  position: static;
552
- padding: 1px 4px;
554
+ gap: 1px;
555
+ padding: 0 2px;
553
556
  background: transparent;
554
557
  box-shadow: none;
555
558
  transform: none;
556
559
  }
560
+ .structure-carousel-pager.portaled button {
561
+ inline-size: 18px;
562
+ block-size: 18px;
563
+ }
557
564
  .structure-carousel-pager button {
558
565
  display: grid;
559
566
  place-items: center;
560
567
  inline-size: 22px;
561
568
  block-size: 22px;
562
- padding: 0;
569
+ padding: 0 0 2px;
563
570
  border: 0;
564
571
  border-radius: 50%;
565
572
  background: transparent;
@@ -570,7 +577,7 @@
570
577
  line-height: 1;
571
578
  }
572
579
  .structure-carousel-pager button:hover:not(:disabled) {
573
- background: color-mix(in srgb, currentColor 12%, transparent);
580
+ color: var(--active-color, #6ea8ff);
574
581
  }
575
582
  .structure-carousel-pager button:disabled {
576
583
  opacity: 0.3;
@@ -2,6 +2,7 @@
2
2
  import type { PaneProps, PaneToggleProps } from '../overlays'
3
3
  import type { ColorSchemeName, D3InterpolateName } from '../colors'
4
4
  import { AXIS_COLORS, ELEMENT_COLOR_SCHEMES } from '../colors'
5
+ import Spinner from '../feedback/Spinner.svelte'
5
6
  import IsosurfaceControls from '../isosurface/IsosurfaceControls.svelte'
6
7
  import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
7
8
  import { format_num } from '../labels'
@@ -286,17 +287,11 @@
286
287
  scene_props.rotation = [...scene_props.rotation]
287
288
  }
288
289
 
289
- // Helper function to get example set of colors from an element color scheme
290
+ // Sample colors for common elements, used to preview an element color scheme
290
291
  function get_representative_colors(scheme_name: string): string[] {
291
292
  const scheme = ELEMENT_COLOR_SCHEMES[scheme_name as ColorSchemeName]
292
293
  if (!scheme) return []
293
-
294
- // Get colors for common elements: H, C, N, O, Fe, Ca, Si, Al
295
- const sample_elements = [`H`, `C`, `N`, `O`, `Fe`, `Ca`, `Si`, `Al`]
296
- return sample_elements
297
- .slice(0, 4) // Take first 4
298
- .map((el) => scheme[el] || scheme.H || `#cccccc`)
299
- .filter(Boolean)
294
+ return [`H`, `C`, `N`, `O`].map((elem) => scheme[elem] || scheme.H || `#cccccc`)
300
295
  }
301
296
  </script>
302
297
 
@@ -318,7 +313,7 @@
318
313
  {#if volumetric_data?.length && isosurface_settings}
319
314
  <IsosurfaceControls
320
315
  bind:settings={isosurface_settings}
321
- volumes={volumetric_data}
316
+ bind:volumes={volumetric_data}
322
317
  bind:active_volume_idx
323
318
  />
324
319
  {/if}
@@ -923,15 +918,10 @@
923
918
  />
924
919
  </label>
925
920
  {#if supercell_loading}
926
- <div
927
- style="display: flex; align-items: center; gap: 8px; font-size: 0.85em; color: var(--accent-color); margin-top: 4pt"
928
- >
929
- <span
930
- class="spinner-icon"
931
- style="display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite"
932
- ></span>
933
- <span>Generating supercell...</span>
934
- </div>
921
+ <Spinner
922
+ text="Generating supercell..."
923
+ style="--spinner-size: 12px; --spinner-border-width: 2px; --spinner-margin: 4pt 0 0; font-size: 0.85em; color: var(--accent-color)"
924
+ />
935
925
  {/if}
936
926
 
937
927
  {#if !supercell_input_valid}
@@ -1235,12 +1225,4 @@
1235
1225
  flex: 1;
1236
1226
  min-width: 40px;
1237
1227
  }
1238
- @keyframes spin {
1239
- from {
1240
- transform: rotate(0deg);
1241
- }
1242
- to {
1243
- transform: rotate(360deg);
1244
- }
1245
- }
1246
1228
  </style>
@@ -28,6 +28,6 @@ type $$ComponentProps = Omit<ComponentProps<typeof DraggablePane>, `children`> &
28
28
  pane_props?: PaneProps;
29
29
  toggle_props?: PaneToggleProps;
30
30
  };
31
- declare const StructureControls: import("svelte").Component<$$ComponentProps, {}, "color_scheme" | "controls_open" | "background_color" | "background_opacity" | "show_image_atoms" | "lattice_props" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "supercell_scaling" | "cell_type" | "active_volume_idx" | "supercell_loading">;
31
+ declare const StructureControls: import("svelte").Component<$$ComponentProps, {}, "color_scheme" | "controls_open" | "active_volume_idx" | "background_color" | "background_opacity" | "show_image_atoms" | "lattice_props" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "supercell_scaling" | "cell_type" | "supercell_loading">;
32
32
  type StructureControls = ReturnType<typeof StructureControls>;
33
33
  export default StructureControls;