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
@@ -56,12 +56,23 @@
56
56
  // Timer for distinguishing click from double-click (per-component state)
57
57
  let click_timer: ReturnType<typeof setTimeout> | null = null
58
58
  let click_timer_file: string | null = null
59
+ const file_key = (file: FileInfo): string => file.url || file.name
59
60
 
60
61
  const clear_click_timer = () => {
61
- if (click_timer) clearTimeout(click_timer)
62
+ if (click_timer !== null) clearTimeout(click_timer)
62
63
  click_timer = null
63
64
  click_timer_file = null
64
65
  }
66
+ const is_activation_key = (event: KeyboardEvent): boolean =>
67
+ event.key === `Enter` || event.key === ` `
68
+
69
+ const schedule_single_click = (file: FileInfo, event: MouseEvent) => {
70
+ click_timer_file = file_key(file)
71
+ click_timer = setTimeout(() => {
72
+ clear_click_timer()
73
+ on_click?.(file, event)
74
+ }, CLICK_DELAY)
75
+ }
65
76
 
66
77
  // Helper function to get the base file type (removing .gz extension)
67
78
  const get_base_file_type = (file: FileInfo): string => {
@@ -94,38 +105,29 @@
94
105
  )
95
106
 
96
107
  const toggle_filter = (kind: FilterKind, filter: string) => {
97
- if (kind === `category`) {
98
- active_category_filter = active_category_filter === filter ? null : filter
99
- active_type_filter = null
100
- } else {
101
- active_type_filter = active_type_filter === filter ? null : filter
102
- active_category_filter = null
103
- }
108
+ const active_filter = kind === `category` ? active_category_filter : active_type_filter
109
+ active_category_filter = kind === `category` && active_filter !== filter ? filter : null
110
+ active_type_filter = kind === `type` && active_filter !== filter ? filter : null
104
111
  }
105
112
 
106
113
  const handle_drag_start = (file: FileInfo) => (event: DragEvent) => {
107
- const file_url = file.url || file.name // Get the URL to drag (falling back to name)
108
-
109
- const payload = JSON.stringify({
110
- name: file.name,
111
- url: file_url,
112
- type: file.type || get_base_file_type(file),
113
- category: file.category,
114
- })
115
- // Set file data as JSON for applications that can handle it
116
- event.dataTransfer?.setData(`application/json`, payload)
117
-
118
- // Also set plain text as fallback for external applications
119
- event.dataTransfer?.setData(`text/plain`, file_url)
120
-
114
+ const url = file_key(file)
115
+ event.dataTransfer?.setData(
116
+ `application/json`,
117
+ JSON.stringify({
118
+ name: file.name,
119
+ url,
120
+ type: file.type || get_base_file_type(file),
121
+ category: file.category,
122
+ }),
123
+ )
124
+ event.dataTransfer?.setData(`text/plain`, url)
121
125
  on_drag_start?.(file, event)
122
126
  }
123
127
 
124
128
  // Get unique file types/categories for format/category filters
125
129
  let uniq_formats = $derived([...new Set(files.map(get_base_file_type))].sort())
126
- let uniq_categories = $derived(
127
- [...new Set(files.map(get_category_id))].filter(Boolean).sort(),
128
- )
130
+ let uniq_categories = $derived([...new Set(files.map(get_category_id))].sort())
129
131
  </script>
130
132
 
131
133
  <div class="file-picker" class:vertical={layout === `vertical`} {...rest}>
@@ -135,11 +137,8 @@
135
137
  <span
136
138
  class="legend-item"
137
139
  class:active={is_active}
138
- onclick={() => category && toggle_filter(`category`, category)}
139
- onkeydown={(evt) =>
140
- (evt.key === `Enter` || evt.key === ` `) &&
141
- category &&
142
- toggle_filter(`category`, category)}
140
+ onclick={() => toggle_filter(`category`, category)}
141
+ onkeydown={(event) => is_activation_key(event) && toggle_filter(`category`, category)}
143
142
  role="button"
144
143
  tabindex="0"
145
144
  aria-pressed={is_active}
@@ -158,8 +157,7 @@
158
157
  class="legend-item format-item"
159
158
  class:active={is_active}
160
159
  onclick={() => toggle_filter(`type`, format)}
161
- onkeydown={(evt) =>
162
- (evt.key === `Enter` || evt.key === ` `) && toggle_filter(`type`, format)}
160
+ onkeydown={(event) => is_activation_key(event) && toggle_filter(`type`, format)}
163
161
  role="button"
164
162
  tabindex="0"
165
163
  {@attach tooltip({ content: `Filter to show only ${format.toUpperCase()} files` })}
@@ -191,26 +189,17 @@
191
189
  ondragend={() => on_drag_end?.()}
192
190
  onclick={(event) => {
193
191
  clear_click_timer()
194
- if (on_dblclick) {
195
- // Delay click to allow double-click to cancel it
196
- const target_file = file.name
197
- click_timer_file = target_file
198
- click_timer = setTimeout(() => {
199
- if (click_timer_file === target_file) {
200
- clear_click_timer()
201
- on_click?.(file, event)
202
- }
203
- }, CLICK_DELAY)
204
- } else {
205
- on_click?.(file, event)
206
- }
192
+ if (on_dblclick) schedule_single_click(file, event)
193
+ else on_click?.(file, event)
207
194
  }}
208
195
  ondblclick={(event) => {
196
+ const pending = click_timer_file
209
197
  clear_click_timer()
210
- on_dblclick?.(file, event)
198
+ if (pending !== null && pending === file_key(file)) on_dblclick?.(file, event)
199
+ else schedule_single_click(file, event)
211
200
  }}
212
201
  onkeydown={(event) => {
213
- if ([`Enter`, ` `].includes(event.key)) {
202
+ if (is_activation_key(event)) {
214
203
  event.preventDefault()
215
204
  clear_click_timer()
216
205
  on_click?.(file, event)
@@ -230,7 +219,8 @@
230
219
  >
231
220
  {/if}
232
221
  <div class="file-name">
233
- {file.category ? `${file.category_icon} ` : ``}{file.label ?? file.name}
222
+ {file.category && file.category_icon ? `${file.category_icon} ` : ``}{file.label ??
223
+ file.name}
234
224
  </div>
235
225
  </div>
236
226
  {/each}
@@ -316,9 +306,9 @@
316
306
  .file-item {
317
307
  display: flex;
318
308
  align-items: center;
319
- padding: 4pt 8pt;
309
+ padding: 2pt 8pt;
320
310
  border: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.2));
321
- border-radius: 20px;
311
+ border-radius: 9px;
322
312
  cursor: grab;
323
313
  background: light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.1));
324
314
  transition: all 0.2s ease;
@@ -110,12 +110,16 @@ export function detect_provider_from_slug(slug, providers) {
110
110
  const prefix = decoded_slug.split(`-`)[0].toLowerCase();
111
111
  return providers.find((provider) => provider.id === prefix)?.id ?? ``;
112
112
  }
113
- export async function fetch_optimade_structure(structure_id, provider, providers) {
113
+ // Resolve a provider id to its versioned API base URL (throws on unknown provider)
114
+ async function get_api_base(provider, providers) {
114
115
  const provider_config = providers.find((entry) => entry.id === provider);
115
116
  if (!provider_config)
116
117
  throw new Error(`Unknown provider: ${provider}`);
117
118
  const base_url = await resolve_provider_url(provider_config.attributes.base_url);
118
- const api_base = base_url.endsWith(`/v1`) ? base_url : `${base_url}/v1`;
119
+ return base_url.endsWith(`/v1`) ? base_url : `${base_url}/v1`;
120
+ }
121
+ export async function fetch_optimade_structure(structure_id, provider, providers) {
122
+ const api_base = await get_api_base(provider, providers);
119
123
  const encoded_id = encode_structure_id(structure_id);
120
124
  const response = await fetch_with_cors_proxy(`${api_base}/structures/${encoded_id}`);
121
125
  const data = await response.json();
@@ -124,12 +128,11 @@ export async function fetch_optimade_structure(structure_id, provider, providers
124
128
  return Array.isArray(data.data) ? data.data[0] : data.data;
125
129
  }
126
130
  export async function fetch_suggested_structures(provider, providers, limit = 12) {
127
- const provider_config = providers.find((entry) => entry.id === provider);
128
- if (!provider_config)
129
- throw new Error(`Unknown provider: ${provider}`);
130
131
  try {
131
- const base_url = await resolve_provider_url(provider_config.attributes.base_url);
132
- const api_base = base_url.endsWith(`/v1`) ? base_url : `${base_url}/v1`;
132
+ // Keep get_api_base inside try: suggestions are optional, so unknown providers and
133
+ // URL-resolution failures both soft-fail to []. Note: unknown provider threw
134
+ // pre-cleanup, which left the sole caller's loading state stuck on rejection.
135
+ const api_base = await get_api_base(provider, providers);
133
136
  const response = await fetch_with_cors_proxy(`${api_base}/structures?page_limit=${limit}&page_offset=0`);
134
137
  const data = await response.json();
135
138
  return Array.isArray(data.data) ? data.data : [];
package/dist/app.css CHANGED
@@ -25,6 +25,21 @@
25
25
  Override per viewer by setting this on its wrapper. */
26
26
  --ctrl-btn-icon-size: clamp(0.7rem, 2cqmin, 0.85rem);
27
27
 
28
+ /* In-viewer overlay stack (inside one viewer container, above its WebGL
29
+ canvas): 1 viewport labels · 2 glass chips · 4–6 carousel chrome
30
+ (legend/resize-handle/pager) · 10 draggable panes · 100 dropdowns +
31
+ toasts · 1000 canvas tooltips + error banners. Components fall back to
32
+ these tiers; override per instance via their dedicated --*-z-index vars.
33
+ NOTE (WKWebView/Tauri): overlays that can overlap a WebGL canvas also
34
+ need their own compositing layer (will-change: transform) — after hiding
35
+ and re-showing the window, WebKit reattaches canvas layers above
36
+ non-composited overlays regardless of z-index. */
37
+ --z-index-viewer-label: 1;
38
+ --z-index-viewer-chip: 2;
39
+ --z-index-viewer-pane: 10;
40
+ --z-index-viewer-dropdown: 100;
41
+ --z-index-viewer-tooltip: 1000;
42
+
28
43
  /* App-wide overlay stack: controls < fullscreen/nav < dialogs < floating options.
29
44
  These must exceed Threlte HTML overlays, which use very high z-index values. */
30
45
  --z-index-overlay-controls: 100000000;
@@ -3,7 +3,7 @@
3
3
  import EmptyState from '../EmptyState.svelte'
4
4
  import { StatusMessage } from '../feedback'
5
5
  import Spinner from '../feedback/Spinner.svelte'
6
- import { create_file_drop_handler, drag_over_handlers, load_from_url } from '../io'
6
+ import * as io from '../io'
7
7
  import { type FullscreenToggleProp, toggle_fullscreen, ViewerChrome } from '../layout'
8
8
  import { sync_fullscreen } from '../layout/fullscreen.svelte'
9
9
  import type { Vec3 } from '../math'
@@ -248,20 +248,29 @@
248
248
  })
249
249
 
250
250
  // Load structure from URL or string
251
+ let data_url_load_id = 0
251
252
  $effect(() => {
252
253
  const handle_error = (err: unknown, source: string) => {
253
254
  error_msg = to_error(err).message
254
255
  on_error?.({ error_msg, filename: source })
255
256
  }
256
257
 
257
- if (data_url && !structure) {
258
+ const requested_url = data_url
259
+ if (requested_url && !structure) {
260
+ const load_id = ++data_url_load_id
258
261
  loading = true
259
262
  error_msg = undefined
260
- load_from_url(data_url, (content, filename) =>
261
- on_file_drop ? on_file_drop(content, filename) : safe_parse(content, filename),
262
- )
263
- .catch((err) => handle_error(err, data_url))
264
- .finally(() => (loading = false))
263
+ io.load_from_url(requested_url, (content, filename) => {
264
+ if (load_id !== data_url_load_id) return
265
+ return on_file_drop ? on_file_drop(content, filename) : safe_parse(content, filename)
266
+ })
267
+ .catch((err) => {
268
+ if (load_id !== data_url_load_id) return
269
+ handle_error(err, io.basename_from_url(requested_url))
270
+ })
271
+ .finally(() => {
272
+ if (load_id === data_url_load_id) loading = false
273
+ })
265
274
  } else if (structure_string && !data_url) {
266
275
  loading = true
267
276
  error_msg = undefined
@@ -275,7 +284,7 @@
275
284
  }
276
285
  })
277
286
 
278
- const handle_file_drop = create_file_drop_handler({
287
+ const handle_file_drop = io.create_file_drop_handler({
279
288
  allow: () => allow_file_drop,
280
289
  on_drop: (content, filename) => (on_file_drop || safe_parse)(content, filename),
281
290
  on_error: (msg) => {
@@ -325,7 +334,7 @@
325
334
  onmouseenter={() => (hovered = true)}
326
335
  onmouseleave={() => (hovered = false)}
327
336
  ondrop={handle_file_drop}
328
- {...drag_over_handlers({
337
+ {...io.drag_over_handlers({
329
338
  allow: () => allow_file_drop,
330
339
  set_dragover: (over) => (dragover = over),
331
340
  })}
@@ -48,7 +48,7 @@
48
48
  auto_rotate = DEFAULTS.structure.auto_rotate,
49
49
  scene = $bindable(),
50
50
  camera = $bindable(),
51
- k_path_points = [],
51
+ k_path_points: input_k_path_points = [],
52
52
  k_path_labels = [],
53
53
  hovered_k_point = null,
54
54
  hovered_qpoint_index = null,
@@ -75,6 +75,7 @@
75
75
  hover_data?: BZHoverData | null
76
76
  on_kpath_hover?: (qpoint_index: number | null) => void
77
77
  } = $props()
78
+ const k_path_points = $derived(input_k_path_points ?? [])
78
79
 
79
80
  bind_renderer((threlte_scene, threlte_camera) => {
80
81
  scene = threlte_scene
@@ -114,7 +115,7 @@
114
115
  // Band paths are densely sampled, so legit segments are tiny; a discontinuity jumps by
115
116
  // a fraction of the zone. Skip segments far longer than the median sampling step.
116
117
  const k_path_seg_cutoff = $derived.by(() => {
117
- if (!k_path_points || k_path_points.length < 3) return Infinity
118
+ if (k_path_points.length < 3) return Infinity
118
119
  const lens = k_path_points
119
120
  .slice(1)
120
121
  .map((pt, idx) => Math.hypot(...math.subtract(pt as Vec3, k_path_points[idx] as Vec3)))
@@ -290,7 +291,7 @@
290
291
  {/if}
291
292
 
292
293
  <!-- K-path visualization -->
293
- {#if k_path_points && k_path_points.length > 1}
294
+ {#if k_path_points.length > 1}
294
295
  {#each k_path_points.slice(0, -1) as from_point, idx (`${from_point}-${k_path_points[idx + 1]}#${idx}`)}
295
296
  {@const to_point = k_path_points[idx + 1]}
296
297
  {@const seg_len = Math.hypot(...math.subtract(to_point as Vec3, from_point as Vec3))}
@@ -317,32 +318,22 @@
317
318
  {/each}
318
319
  {/if}
319
320
 
320
- <!-- Symmetry point spheres at labeled k-path points -->
321
- {#if k_path_labels}
322
- {#each k_path_labels as { position, label }, idx (`sphere-${idx}`)}
323
- {#if label}
324
- <T.Mesh position={[position[0], position[1], position[2]]}>
325
- <T.SphereGeometry args={[0.015, 16, 16]} />
326
- <T.MeshStandardMaterial color="#ffcc00" metalness={0.3} roughness={0.7} />
327
- </T.Mesh>
328
- {/if}
329
- {/each}
330
- {/if}
331
-
332
- <!-- Symmetry point labels on k-path -->
333
- {#if k_path_labels}
334
- {#each k_path_labels as { position, label }, idx (`${label}-${idx}`)}
335
- {#if label}
336
- <extras.HTML center position={position.map((coord) => coord * 1.1) as Vec3}>
337
- <span
338
- style="background: rgba(0, 0, 0, 0.3); padding: 0 3px; border-radius: 2px; color: white"
339
- >
340
- {label}
341
- </span>
342
- </extras.HTML>
343
- {/if}
344
- {/each}
345
- {/if}
321
+ <!-- Symmetry point spheres + labels at labeled k-path points -->
322
+ {#each k_path_labels as { position, label }, idx (`${label}-${idx}`)}
323
+ {#if label}
324
+ <T.Mesh position={[position[0], position[1], position[2]]}>
325
+ <T.SphereGeometry args={[0.015, 16, 16]} />
326
+ <T.MeshStandardMaterial color="#ffcc00" metalness={0.3} roughness={0.7} />
327
+ </T.Mesh>
328
+ <extras.HTML center position={position.map((coord) => coord * 1.1) as Vec3}>
329
+ <span
330
+ style="background: rgba(0, 0, 0, 0.3); padding: 0 3px; border-radius: 2px; color: white"
331
+ >
332
+ {label}
333
+ </span>
334
+ </extras.HTML>
335
+ {/if}
336
+ {/each}
346
337
 
347
338
  <!-- Hovered k-point highlight -->
348
339
  {#if hovered_k_point}
@@ -3,8 +3,6 @@ import * as math from '../math';
3
3
  import { Vector3 } from 'three';
4
4
  import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js';
5
5
  const TOL = 1e-8;
6
- // Check if rotation matrix is identity
7
- const is_identity_rotation = (rot) => rot.every((row, idx) => row.every((val, jdx) => Math.abs(val - (idx === jdx ? 1 : 0)) < TOL));
8
6
  // Extract unique point group rotation matrices from space group operations.
9
7
  // Returns fractional-coordinate rotations (W matrices from spglib convention).
10
8
  // These must be converted to Cartesian k-space before use in clipping.
@@ -111,12 +109,11 @@ export function generate_bz_vertices(k_lattice, order = 1,
111
109
  // Higher values give more accurate zones but increase computation time significantly.
112
110
  // Default values: 26 (1st order), 80 (2nd order), 150 (3rd+ order)
113
111
  max_planes_by_order = { 1: 26, 2: 80, 3: 150 }) {
114
- if (order > 3)
115
- order = 3; // Performance limit
116
- const k_points = generate_k_space_grid(k_lattice, order);
112
+ const clamped_order = Math.min(order, 3);
113
+ const k_points = generate_k_space_grid(k_lattice, clamped_order);
117
114
  const center_idx = Math.floor(k_points.length / 2);
118
- // Determine max planes for this order (default to highest value for orders > 3)
119
- const max_planes = max_planes_by_order[order] ?? 150;
115
+ // Fallback for partial records passed through compute_brillouin_zone
116
+ const max_planes = max_planes_by_order[clamped_order] ?? 150;
120
117
  // Create Bragg planes (perpendicular bisectors of k-points)
121
118
  const planes = k_points
122
119
  .map((pt, idx) => {
@@ -152,12 +149,12 @@ max_planes_by_order = { 1: 26, 2: 80, 3: 150 }) {
152
149
  vertex[2] * normals[p_idx][2];
153
150
  if (dot > distances[p_idx] + TOL) {
154
151
  beyond_count++;
155
- if (beyond_count >= order)
152
+ if (beyond_count >= clamped_order)
156
153
  break;
157
154
  }
158
155
  }
159
156
  // Vertex belongs to nth BZ if it's beyond fewer than n planes
160
- if (beyond_count < order && !dedup.has_duplicate(vertex)) {
157
+ if (beyond_count < clamped_order && !dedup.has_duplicate(vertex)) {
161
158
  vertices.push(vertex);
162
159
  dedup.add(vertex);
163
160
  }
@@ -166,13 +163,10 @@ max_planes_by_order = { 1: 26, 2: 80, 3: 150 }) {
166
163
  }
167
164
  return vertices;
168
165
  }
169
- // Compute polyhedron volume via divergence theorem (sum of signed tetrahedral volumes)
166
+ // Compute polyhedron volume via divergence theorem (sum of signed tetrahedral volumes).
167
+ // Faces always come from compute_convex_hull, so each is a valid triangle.
170
168
  function compute_hull_volume(vertices, faces) {
171
- if (faces.length === 0)
172
- return 0;
173
169
  return Math.abs(faces.reduce((sum, face) => {
174
- if (face.length < 3)
175
- return sum;
176
170
  const [v0, v1, v2] = face.slice(0, 3).map((idx) => vertices[idx]);
177
171
  const area_normal = math.scale(math.cross_3d(math.subtract(v1, v0), math.subtract(v2, v0)), 0.5);
178
172
  return sum + math.dot(v0, area_normal) / 3;
@@ -255,7 +249,7 @@ max_planes_by_order = { 1: 26, 2: 80, 3: 150 }) {
255
249
  }
256
250
  const hull = compute_convex_hull(vertices, edge_sharp_angle_deg);
257
251
  return {
258
- order,
252
+ order: Math.min(order, 3),
259
253
  vertices: hull.vertices,
260
254
  faces: hull.faces,
261
255
  edges: hull.edges.map(([i1, i2]) => [hull.vertices[i1], hull.vertices[i2]]),
@@ -305,7 +299,7 @@ export function find_ibz_reference_direction(non_identity_ops) {
305
299
  // of exactly volume(BZ)/|G|. (Using a different reference point per operation — or
306
300
  // flipping individual planes — does NOT yield a fundamental domain in general.)
307
301
  export function compute_ibz_clipping_planes(point_group_ops) {
308
- const non_identity_ops = point_group_ops.filter((rot) => !is_identity_rotation(rot));
302
+ const non_identity_ops = point_group_ops.filter((rot) => !rot.every((row, idx) => row.every((val, jdx) => Math.abs(val - (idx === jdx ? 1 : 0)) < TOL)));
309
303
  if (non_identity_ops.length === 0)
310
304
  return [];
311
305
  const ref_dir = find_ibz_reference_direction(non_identity_ops);
@@ -32,12 +32,12 @@ export function polyhedron_geometry(vertices, faces) {
32
32
  geometry.computeBoundingSphere();
33
33
  return geometry;
34
34
  }
35
- // Inverse of the reciprocal lattice for Cartesian -> fractional conversion; null if the lattice is missing or singular
35
+ // Cartesian fractional via transposed row-vector inverse; null if missing/singular
36
36
  export function k_lattice_inverse(k_lattice) {
37
37
  if (!k_lattice)
38
38
  return null;
39
39
  try {
40
- return math.matrix_inverse_3x3(k_lattice);
40
+ return math.create_cart_to_frac_matrix(k_lattice);
41
41
  }
42
42
  catch {
43
43
  return null;
@@ -9,7 +9,12 @@
9
9
  import ChemPotDiagram2D from './ChemPotDiagram2D.svelte'
10
10
  import ChemPotDiagram3D from './ChemPotDiagram3D.svelte'
11
11
  import { get_ternary_combinations } from './compute'
12
- import type { ChemPotDiagramConfig, ChemPotHoverInfo, ChemPotHoverInfo3D } from './types'
12
+ import type {
13
+ AxisRangeData,
14
+ ChemPotDiagramConfig,
15
+ ChemPotHoverInfo,
16
+ ChemPotHoverInfo3D,
17
+ } from './types'
13
18
  import { CHEMPOT_DEFAULTS } from './types'
14
19
  import { to_error } from '../utils'
15
20
 
@@ -106,6 +111,16 @@
106
111
  })
107
112
  </script>
108
113
 
114
+ {#snippet axis_ranges_section(ranges: AxisRangeData[])}
115
+ <h5 id="axis-ranges">Axis ranges</h5>
116
+ {#each ranges as axis_range (axis_range.element)}
117
+ <p>
118
+ {axis_range.element}: {format_num(axis_range.min_val, `.4~g`)} to
119
+ {format_num(axis_range.max_val, `.4~g`)} eV
120
+ </p>
121
+ {/each}
122
+ {/snippet}
123
+
109
124
  <div class="chempot-diagram-wrapper">
110
125
  {#if n_display < 2}
111
126
  <div
@@ -191,13 +206,7 @@
191
206
  {#if hover_info.view === `2d`}
192
207
  <p>2D domain · Points: {hover_info.n_points}</p>
193
208
  {#if tooltip_detail_level === `detailed`}
194
- <h5 id="axis-ranges">Axis ranges</h5>
195
- {#each hover_info.axis_ranges as axis_range (axis_range.element)}
196
- <p>
197
- {axis_range.element}: {format_num(axis_range.min_val, `.4~g`)} to
198
- {format_num(axis_range.max_val, `.4~g`)} eV
199
- </p>
200
- {/each}
209
+ {@render axis_ranges_section(hover_info.axis_ranges)}
201
210
  {/if}
202
211
  {:else if is_hover_info_3d(hover_info)}
203
212
  <p>
@@ -218,13 +227,7 @@
218
227
  {/if}
219
228
  </p>
220
229
  {#if tooltip_detail_level === `detailed`}
221
- <h5 id="axis-ranges-1">Axis ranges</h5>
222
- {#each hover_info.axis_ranges as axis_range (axis_range.element)}
223
- <p>
224
- {axis_range.element}: {format_num(axis_range.min_val, `.4~g`)} to
225
- {format_num(axis_range.max_val, `.4~g`)} eV
226
- </p>
227
- {/each}
230
+ {@render axis_ranges_section(hover_info.axis_ranges)}
228
231
  <p>
229
232
  Centroid: ({hover_info.ann_loc
230
233
  .map((value) => format_num(value, `.3~g`))