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
@@ -1,15 +1,30 @@
1
1
  <script lang="ts">
2
2
  // Controls panel for isosurface visualization settings (isovalue, opacity, colors, etc.)
3
- // Supports both single-isovalue mode and multi-layer mode.
3
+ // Single-isovalue mode preserves the classic UI; explicit-layers mode groups
4
+ // surfaces under their geometry-source volume and exposes cross-volume scalar
5
+ // coloring (color source, colormap, value range) per surface.
6
+ import Icon from '../Icon.svelte'
7
+ import type { D3InterpolateName } from '../colors'
4
8
  import { format_num } from '../labels'
5
9
  import { SettingsSection } from '../layout'
10
+ import type { Vec2 } from '../math'
11
+ import { ColorScaleSelect } from '../plot'
6
12
  import { tooltip } from 'svelte-multiselect/attachments'
13
+ import { auto_color_config, DEFAULT_ISO_COLORMAP, ISO_COLORMAPS } from './coloring'
14
+ import type { DisplayRange } from './sampling'
15
+ import { compare_volume_grids } from './sampling'
7
16
  import type { IsosurfaceLayer, IsosurfaceSettings, VolumetricData } from './types'
8
- import { DEFAULT_ISOSURFACE_SETTINGS, generate_layers } from './types'
17
+ import {
18
+ auto_volume_layer,
19
+ DEFAULT_ISOSURFACE_SETTINGS,
20
+ generate_layers,
21
+ materialize_layers,
22
+ remove_volume,
23
+ } from './types'
9
24
 
10
25
  let {
11
26
  settings = $bindable({ ...DEFAULT_ISOSURFACE_SETTINGS }),
12
- volumes = [],
27
+ volumes = $bindable([]),
13
28
  active_volume_idx = $bindable(0),
14
29
  }: {
15
30
  settings?: IsosurfaceSettings
@@ -31,15 +46,24 @@
31
46
 
32
47
  let slider_max = $derived(Math.max(data_range.abs_max, 0.001))
33
48
  let step = $derived(slider_max / 200)
34
- let is_multi_layer = $derived((settings.layers?.length ?? 0) > 0)
49
+ // Explicit layers mode is active whenever layers is set an empty array means
50
+ // "zero surfaces" (all removed), NOT a fallback to implicit single-surface mode
51
+ let is_multi_layer = $derived(settings.layers != null)
35
52
  let n_layers = $derived(settings.layers?.length ?? 1)
36
53
 
54
+ const vol_label = (idx: number): string => volumes[idx]?.label ?? `Volume ${idx + 1}`
55
+
56
+ // Resolve a layer's geometry volume the same way for grouping, warnings, and
57
+ // data ranges (out-of-range indices clamp so stale layers stay editable/removable)
58
+ const resolve_geo_idx = (layer: IsosurfaceLayer): number =>
59
+ Math.min(Math.max(layer.volume_idx ?? active_volume_idx, 0), volumes.length - 1)
60
+
61
+ // The layer's scalar-color-source volume, if any
62
+ const color_vol_of = (layer: IsosurfaceLayer): VolumetricData | undefined =>
63
+ layer.color_volume_idx != null ? volumes[layer.color_volume_idx] : undefined
64
+
37
65
  function set_layer_count(count: number) {
38
- if (count <= 1) {
39
- settings.layers = undefined
40
- } else {
41
- settings.layers = generate_layers(data_range, count)
42
- }
66
+ settings.layers = count <= 1 ? undefined : generate_layers(data_range, count)
43
67
  }
44
68
 
45
69
  function update_layer(idx: number, updates: Partial<IsosurfaceLayer>) {
@@ -48,8 +72,159 @@
48
72
  layer_idx === idx ? { ...layer, ...updates } : layer,
49
73
  )
50
74
  }
75
+
76
+ function remove_layer(idx: number) {
77
+ if (settings.layers)
78
+ settings.layers = settings.layers.filter((_layer, layer_idx) => layer_idx !== idx)
79
+ }
80
+
81
+ function add_surface(vol_idx: number) {
82
+ const vol = volumes[vol_idx]
83
+ if (!vol) return
84
+ const layers = materialize_layers(settings, active_volume_idx)
85
+ layers.push(auto_volume_layer(vol, vol_idx, layers.length))
86
+ settings.layers = layers
87
+ active_volume_idx = vol_idx
88
+ }
89
+
90
+ function handle_remove_volume(vol_idx: number) {
91
+ const layers = materialize_layers(settings, active_volume_idx)
92
+ const result = remove_volume(volumes, layers, vol_idx, active_volume_idx)
93
+ volumes = result.volumes
94
+ settings.layers = result.layers
95
+ // Keep the active volume pointing at the same physical volume (indices shift)
96
+ if (active_volume_idx > vol_idx) active_volume_idx -= 1
97
+ if (active_volume_idx >= result.volumes.length) active_volume_idx = 0
98
+ }
99
+
100
+ // Set (or clear) a layer's scalar-color source. The colormap is auto-picked
101
+ // from the color volume's data; color_range stays unset so the renderer fits
102
+ // it to the scalar values actually present on the surface.
103
+ function set_color_source(layer_idx: number, color_idx: number | null) {
104
+ const color_vol = color_idx === null ? undefined : volumes[color_idx]
105
+ if (color_idx !== null && !color_vol) return
106
+ update_layer(layer_idx, {
107
+ color_volume_idx: color_idx ?? undefined,
108
+ colormap: color_vol ? auto_color_config(color_vol.data_range).colormap : undefined,
109
+ color_range: undefined,
110
+ })
111
+ }
112
+
113
+ // Picking a color source in single-isovalue mode materializes explicit layers
114
+ function set_single_color_source(color_idx: number | null) {
115
+ if (color_idx === null) return
116
+ settings.layers = materialize_layers(settings, active_volume_idx)
117
+ set_color_source(0, color_idx)
118
+ }
119
+
120
+ // Update one bound of a layer's color range. An empty input resets the whole
121
+ // range to auto-fit (renderer fits it to the values sampled on the surface).
122
+ // Typing into an auto range seeds the other bound from the color volume's
123
+ // full data range as a starting point.
124
+ function update_color_range(layer_idx: number, bound: 0 | 1, raw_value: string) {
125
+ const layer = settings.layers?.[layer_idx]
126
+ if (!layer) return
127
+ if (raw_value.trim() === ``) {
128
+ update_layer(layer_idx, { color_range: undefined })
129
+ return
130
+ }
131
+ const value = Number(raw_value)
132
+ if (Number.isNaN(value)) return
133
+ const color_vol = color_vol_of(layer)
134
+ const current: Vec2 =
135
+ layer.color_range ??
136
+ (color_vol ? auto_color_config(color_vol.data_range).color_range : [0, 1])
137
+ const next: Vec2 = bound === 0 ? [value, current[1]] : [current[0], value]
138
+ update_layer(layer_idx, { color_range: next })
139
+ }
140
+
141
+ // Grid-compatibility note for a surface/color volume pair. Strictly matching
142
+ // grids sample exactly; otherwise values are resampled in shared coordinates.
143
+ function compat_warning(layer: IsosurfaceLayer): string | null {
144
+ const geo_vol = volumes[resolve_geo_idx(layer)]
145
+ const color_vol = color_vol_of(layer)
146
+ if (!geo_vol || !color_vol || geo_vol === color_vol) return null
147
+ const compat = compare_volume_grids(geo_vol, color_vol)
148
+ return compat.ok ? null : (compat.reason ?? `grids differ`)
149
+ }
150
+
151
+ // Layers grouped by geometry-source volume for the tree-style multi-layer UI
152
+ let grouped_layers = $derived.by(() => {
153
+ const groups = volumes.map((_vol, vol_idx) => ({
154
+ vol_idx,
155
+ entries: [] as { layer: IsosurfaceLayer; layer_idx: number }[],
156
+ }))
157
+ for (const [layer_idx, layer] of (settings.layers ?? []).entries()) {
158
+ const vol_idx = resolve_geo_idx(layer)
159
+ if (vol_idx >= 0) groups[vol_idx].entries.push({ layer, layer_idx })
160
+ }
161
+ return groups
162
+ })
163
+
164
+ // Halo pads geometry volumes only, so gate on volumes that actually render
165
+ // surfaces (color-source-only volumes don't count)
166
+ let any_periodic = $derived(
167
+ settings.layers
168
+ ? settings.layers.some((layer) => volumes[resolve_geo_idx(layer)]?.periodic)
169
+ : (volumes[active_volume_idx]?.periodic ?? false),
170
+ )
171
+
172
+ // Update one bound of the fractional display range. Clearing an input resets
173
+ // that bound to its default (0 or 1); a fully default range unsets display_range
174
+ // so surfaces follow the structure's integer supercell again.
175
+ function update_display_range(axis: number, bound: 0 | 1, raw_value: string) {
176
+ const range = (settings.display_range?.map((pair) => [...pair]) ?? [
177
+ [0, 1],
178
+ [0, 1],
179
+ [0, 1],
180
+ ]) as DisplayRange
181
+ const value = raw_value.trim() === `` ? bound : Number(raw_value)
182
+ if (Number.isNaN(value)) return
183
+ range[axis][bound] = value
184
+ const is_default = range.every(([lo, hi]) => lo === 0 && hi === 1)
185
+ settings.display_range = is_default ? undefined : range
186
+ }
51
187
  </script>
52
188
 
189
+ {#snippet color_source_select(
190
+ selected: number,
191
+ on_pick: (idx: number | null) => void,
192
+ tip: string,
193
+ )}
194
+ <label {@attach tooltip({ content: tip })}>
195
+ <span>Color by:</span>
196
+ <select
197
+ value={selected}
198
+ onchange={(event) => {
199
+ const idx = Number(event.currentTarget.value)
200
+ on_pick(idx < 0 ? null : idx)
201
+ }}
202
+ >
203
+ <option value={-1}>None (solid)</option>
204
+ {#each volumes as _color_vol, color_idx (color_idx)}
205
+ <option value={color_idx}>{vol_label(color_idx)}</option>
206
+ {/each}
207
+ </select>
208
+ </label>
209
+ {/snippet}
210
+
211
+ {#snippet range_bound_input(layer_idx: number, bound: 0 | 1, explicit_range?: Vec2)}
212
+ <input
213
+ type="number"
214
+ class="range-input"
215
+ step="any"
216
+ placeholder="auto"
217
+ value={explicit_range ? Number(explicit_range[bound].toPrecision(4)) : ``}
218
+ onchange={(event) => update_color_range(layer_idx, bound, event.currentTarget.value)}
219
+ aria-label={bound === 0 ? `Color range minimum` : `Color range maximum`}
220
+ {@attach tooltip({
221
+ content: `Value mapped to the colormap ${
222
+ bound === 0 ? `start` : `end`
223
+ } (empty = auto-fit to surface values)`,
224
+ })}
225
+ />
226
+ {/snippet}
227
+
53
228
  <SettingsSection
54
229
  title="Isosurface"
55
230
  current_values={{
@@ -59,14 +234,15 @@
59
234
  wireframe: settings.wireframe,
60
235
  halo: settings.halo,
61
236
  layers: n_layers,
237
+ display_range: settings.display_range?.flat().join(`,`) ?? ``,
62
238
  }}
63
239
  on_reset={() => {
64
240
  settings = { ...DEFAULT_ISOSURFACE_SETTINGS }
65
241
  }}
66
242
  >
67
- <!-- Top row: volume selector (if multi-volume) + layer count + toggles -->
243
+ <!-- Top row: volume selector (single-layer multi-volume) + layer count + toggles -->
68
244
  <div class="pane-row compact-row">
69
- {#if volumes.length > 1}
245
+ {#if !is_multi_layer && volumes.length > 1}
70
246
  <label
71
247
  {@attach tooltip({
72
248
  content: `Select which volume to display (e.g. charge vs magnetization)`,
@@ -74,27 +250,29 @@
74
250
  >
75
251
  <span>Volume:</span>
76
252
  <select bind:value={active_volume_idx}>
77
- {#each volumes as vol, idx (idx)}
78
- <option value={idx}>{vol.label ?? `Volume ${idx + 1}`}</option>
253
+ {#each volumes as _vol, idx (idx)}
254
+ <option value={idx}>{vol_label(idx)}</option>
79
255
  {/each}
80
256
  </select>
81
257
  </label>
82
258
  {/if}
83
- <label
84
- {@attach tooltip({
85
- content: `Number of isosurface shells at different density thresholds`,
86
- })}
87
- >
88
- <span>Layers:</span>
89
- <select
90
- value={n_layers}
91
- onchange={(event) => set_layer_count(Number(event.currentTarget.value))}
259
+ {#if !is_multi_layer}
260
+ <label
261
+ {@attach tooltip({
262
+ content: `Number of isosurface shells at different density thresholds`,
263
+ })}
92
264
  >
93
- {#each [1, 2, 3, 4, 5] as count (count)}
94
- <option value={count}>{count}</option>
95
- {/each}
96
- </select>
97
- </label>
265
+ <span>Layers:</span>
266
+ <select
267
+ value={n_layers}
268
+ onchange={(event) => set_layer_count(Number(event.currentTarget.value))}
269
+ >
270
+ {#each [1, 2, 3, 4, 5] as count (count)}
271
+ <option value={count}>{count}</option>
272
+ {/each}
273
+ </select>
274
+ </label>
275
+ {/if}
98
276
  <!-- Sync both settings.show_negative (single-layer fallback) and all layer entries
99
277
  so the toggle works consistently regardless of which mode is active -->
100
278
  <label
@@ -127,45 +305,160 @@
127
305
  </div>
128
306
 
129
307
  {#if is_multi_layer && settings.layers}
130
- <!-- Multi-layer controls: each row has visibility, color, isovalue slider, opacity -->
131
- {#each settings.layers as layer, idx (idx)}
132
- <div class="layer-row">
133
- <input
134
- type="checkbox"
135
- checked={layer.visible}
136
- onchange={() => update_layer(idx, { visible: !layer.visible })}
137
- />
138
- <input
139
- type="color"
140
- value={layer.color}
141
- onchange={(event) => update_layer(idx, { color: event.currentTarget.value })}
142
- />
143
- <input
144
- type="range"
145
- min={step}
146
- max={slider_max}
147
- {step}
148
- value={layer.isovalue}
149
- oninput={(event) =>
150
- update_layer(idx, {
151
- isovalue: Number(event.currentTarget.value),
152
- })}
153
- style="flex: 1; min-width: 60px"
154
- />
155
- <span class="layer-value">{format_num(layer.isovalue, `.3~g`)}</span>
156
- <input
157
- type="range"
158
- min={0.1}
159
- max={1}
160
- step={0.05}
161
- value={layer.opacity}
162
- oninput={(event) =>
163
- update_layer(idx, {
164
- opacity: Number(event.currentTarget.value),
165
- })}
166
- style="width: 50px"
167
- title="Opacity: {format_num(layer.opacity, `.2f`)}"
168
- />
308
+ <!-- Multi-layer mode: surfaces grouped under their geometry-source volume -->
309
+ {#each grouped_layers as { vol_idx, entries } (vol_idx)}
310
+ {@const vol = volumes[vol_idx]}
311
+ <div class="volume-group">
312
+ <div class="volume-header">
313
+ <span class="volume-label" title={vol_label(vol_idx)}>{vol_label(vol_idx)}</span>
314
+ <span class="volume-dims">{vol.grid_dims.join(`×`)}</span>
315
+ {#if entries.length === 0}
316
+ <span
317
+ class="volume-note"
318
+ {@attach tooltip({
319
+ content: `No surfaces this volume is still available as a color source for other surfaces`,
320
+ })}>color source only</span
321
+ >
322
+ {/if}
323
+ <button
324
+ type="button"
325
+ class="icon-btn"
326
+ onclick={() => add_surface(vol_idx)}
327
+ aria-label="Add surface for {vol_label(vol_idx)}"
328
+ {@attach tooltip({ content: `Add an isosurface from this volume` })}>+</button
329
+ >
330
+ {#if volumes.length > 1}
331
+ <button
332
+ type="button"
333
+ class="icon-btn"
334
+ onclick={() => handle_remove_volume(vol_idx)}
335
+ aria-label="Remove volume {vol_label(vol_idx)}"
336
+ {@attach tooltip({ content: `Remove this volume and its surfaces` })}>×</button
337
+ >
338
+ {/if}
339
+ </div>
340
+
341
+ {#each entries as { layer, layer_idx } (layer_idx)}
342
+ {@const layer_abs_max = Math.max(vol.data_range.abs_max, 0.001)}
343
+ {@const layer_step = layer_abs_max / 200}
344
+ {@const warning = compat_warning(layer)}
345
+ {@const color_vol = color_vol_of(layer)}
346
+ <div class="layer-row">
347
+ <input
348
+ type="checkbox"
349
+ checked={layer.visible}
350
+ onchange={() => update_layer(layer_idx, { visible: !layer.visible })}
351
+ {@attach tooltip({ content: `Toggle surface visibility` })}
352
+ />
353
+ <input
354
+ type="color"
355
+ value={layer.color}
356
+ onchange={(event) =>
357
+ update_layer(layer_idx, { color: event.currentTarget.value })}
358
+ {@attach tooltip({
359
+ content:
360
+ layer.color_volume_idx != null
361
+ ? `Fallback color (surface uses colormap)`
362
+ : `Surface color`,
363
+ })}
364
+ />
365
+ <label
366
+ class="slider-field"
367
+ {@attach tooltip({
368
+ content: `Density threshold — surface is drawn where grid values equal this`,
369
+ })}
370
+ >
371
+ <span>Iso</span>
372
+ <input
373
+ type="range"
374
+ class="isovalue-slider"
375
+ min={layer_step}
376
+ max={layer_abs_max}
377
+ step={layer_step}
378
+ value={layer.isovalue}
379
+ oninput={(event) =>
380
+ update_layer(layer_idx, { isovalue: Number(event.currentTarget.value) })}
381
+ aria-label="Isovalue"
382
+ />
383
+ <span class="layer-value">{format_num(layer.isovalue, `.3~g`)}</span>
384
+ </label>
385
+ <label class="slider-field" {@attach tooltip({ content: `Surface transparency` })}>
386
+ <span>Op</span>
387
+ <input
388
+ type="range"
389
+ class="opacity-slider"
390
+ min={0.1}
391
+ max={1}
392
+ step={0.05}
393
+ value={layer.opacity}
394
+ oninput={(event) =>
395
+ update_layer(layer_idx, { opacity: Number(event.currentTarget.value) })}
396
+ aria-label="Opacity"
397
+ />
398
+ <span class="layer-value">{format_num(layer.opacity, `.2f`)}</span>
399
+ </label>
400
+ <button
401
+ type="button"
402
+ class="icon-btn"
403
+ onclick={() => remove_layer(layer_idx)}
404
+ aria-label="Remove surface"
405
+ {@attach tooltip({ content: `Remove this surface` })}>×</button
406
+ >
407
+ </div>
408
+ <div class="color-row">
409
+ {@render color_source_select(
410
+ layer.color_volume_idx ?? -1,
411
+ (idx) => set_color_source(layer_idx, idx),
412
+ `Color surface by another volume's values`,
413
+ )}
414
+ {#if color_vol}
415
+ {@const explicit_range = layer.color_range}
416
+ {@const auto_colormap = auto_color_config(color_vol.data_range).colormap}
417
+ <ColorScaleSelect
418
+ options={[...ISO_COLORMAPS]}
419
+ value={layer.colormap ?? DEFAULT_ISO_COLORMAP}
420
+ selected={[layer.colormap ?? DEFAULT_ISO_COLORMAP]}
421
+ onadd={({ option }) =>
422
+ update_layer(layer_idx, {
423
+ colormap: option as D3InterpolateName,
424
+ })}
425
+ colorbar={{
426
+ bar_style: `height: 8px`,
427
+ title_style: `width: 4em; font-size: 1em;`,
428
+ }}
429
+ liSelectedStyle="width: 100%; margin: 0; padding: 0; background: transparent;"
430
+ aria-label="Colormap for sampled values"
431
+ {@attach tooltip({ content: `Colormap for sampled values` })}
432
+ />
433
+ <div class="color-range" aria-label="Colormap value range">
434
+ <span>Range:</span>
435
+ {@render range_bound_input(layer_idx, 0, explicit_range)}
436
+ <span aria-hidden="true">&ndash;</span>
437
+ {@render range_bound_input(layer_idx, 1, explicit_range)}
438
+ </div>
439
+ {#if explicit_range || (layer.colormap ?? DEFAULT_ISO_COLORMAP) !== auto_colormap}
440
+ {@const reset_color_label = `Reset colormap + range to auto-fit`}
441
+ <button
442
+ type="button"
443
+ class="icon-btn"
444
+ onclick={() => set_color_source(layer_idx, layer.color_volume_idx ?? null)}
445
+ aria-label={reset_color_label}
446
+ {@attach tooltip({ content: reset_color_label })}
447
+ >
448
+ <Icon icon="Reset" aria-hidden="true" style="--icon-size: 12px" />
449
+ </button>
450
+ {/if}
451
+ {#if warning}
452
+ <span
453
+ class="compat-warning"
454
+ {@attach tooltip({
455
+ content: `Grids differ (${warning}) — values are resampled by trilinear interpolation in shared coordinates`,
456
+ })}>⚠</span
457
+ >
458
+ {/if}
459
+ {/if}
460
+ </div>
461
+ {/each}
169
462
  </div>
170
463
  {/each}
171
464
  {:else}
@@ -177,7 +470,7 @@
177
470
  >
178
471
  <span>Isovalue:</span>
179
472
  <input type="range" min={step} max={slider_max} {step} bind:value={settings.isovalue} />
180
- <span class="value">{format_num(settings.isovalue, `.4~g`)}</span>
473
+ <span class="value">{format_num(settings.isovalue, `.3~g`)}</span>
181
474
  </label>
182
475
 
183
476
  <!-- Opacity + colors on one row -->
@@ -208,10 +501,17 @@
208
501
  <input type="color" bind:value={settings.negative_color} />
209
502
  </label>
210
503
  {/if}
504
+ {#if volumes.length > 1}
505
+ {@render color_source_select(
506
+ -1,
507
+ set_single_color_source,
508
+ `Color this surface by another volume's values (e.g. density by ESP)`,
509
+ )}
510
+ {/if}
211
511
  </div>
212
512
  {/if}
213
513
 
214
- {#if volumes?.[active_volume_idx]?.periodic}
514
+ {#if any_periodic}
215
515
  <label
216
516
  {@attach tooltip({
217
517
  content: `Extend isosurface beyond cell boundaries to close partial spheres (fraction of cell)`,
@@ -220,9 +520,46 @@
220
520
  Halo: {format_num(settings.halo, `.2f`)}
221
521
  <input type="range" min={0} max={0.5} step={0.01} bind:value={settings.halo} />
222
522
  </label>
523
+ <div class="pane-row compact-row display-range">
524
+ <span
525
+ {@attach tooltip({
526
+ content: `Fractional display range per lattice vector (VESTA-style): repeats periodic surfaces and clips them exactly at these bounds, e.g. -0.15 to 2.15. Empty = follow the structure supercell.`,
527
+ })}>Range:</span
528
+ >
529
+ {#each [`a`, `b`, `c`] as axis_label, axis (axis)}
530
+ <label class="range-axis">
531
+ {axis_label}
532
+ <input
533
+ type="number"
534
+ step="0.05"
535
+ placeholder="0"
536
+ value={settings.display_range ? settings.display_range[axis][0] : ``}
537
+ onchange={(event) => update_display_range(axis, 0, event.currentTarget.value)}
538
+ />
539
+ <input
540
+ type="number"
541
+ step="0.05"
542
+ placeholder="1"
543
+ value={settings.display_range ? settings.display_range[axis][1] : ``}
544
+ onchange={(event) => update_display_range(axis, 1, event.currentTarget.value)}
545
+ />
546
+ </label>
547
+ {/each}
548
+ {#if settings.display_range}
549
+ <button
550
+ type="button"
551
+ class="icon-btn"
552
+ onclick={() => (settings.display_range = undefined)}
553
+ aria-label="Reset display range"
554
+ {@attach tooltip({ content: `Follow the structure supercell again` })}
555
+ >
556
+ <Icon icon="Reset" aria-hidden="true" style="--icon-size: 12px" />
557
+ </button>
558
+ {/if}
559
+ </div>
223
560
  {/if}
224
561
 
225
- {#if volumes[active_volume_idx]}
562
+ {#if !is_multi_layer && volumes[active_volume_idx]}
226
563
  <div class="grid-info">
227
564
  {volumes[active_volume_idx].grid_dims.join(` × `)} grid &nbsp;|&nbsp; [{format_num(
228
565
  data_range.min,
@@ -233,6 +570,40 @@
233
570
  </SettingsSection>
234
571
 
235
572
  <style>
573
+ /* Shared chrome for native selects/number/color + ColorScaleSelect (--sms-*) */
574
+ :is(.compact-row, .volume-group, .display-range) {
575
+ --iso-ctrl-h: 22px;
576
+ --iso-ctrl-radius: 3px;
577
+ --iso-ctrl-border: 1px solid var(--border-color, light-dark(#b8bec8, #555));
578
+ --iso-ctrl-bg: light-dark(#fff, #1e1e1e);
579
+ --sms-min-height: var(--iso-ctrl-h);
580
+ --sms-border: var(--iso-ctrl-border);
581
+ --sms-border-radius: var(--iso-ctrl-radius);
582
+ --sms-bg: var(--iso-ctrl-bg);
583
+ font-size: 0.95em;
584
+
585
+ :is(select, input[type='number'], input[type='color']) {
586
+ box-sizing: border-box;
587
+ margin: 0; /* beat DraggablePane margins */
588
+ border: var(--iso-ctrl-border);
589
+ border-radius: var(--iso-ctrl-radius);
590
+ }
591
+ :is(select, input[type='number']) {
592
+ height: var(--iso-ctrl-h);
593
+ padding: 0 4px;
594
+ background: var(--iso-ctrl-bg);
595
+ color: inherit;
596
+ font: inherit;
597
+ line-height: 1.2;
598
+ }
599
+ input[type='color'] {
600
+ width: var(--iso-ctrl-h);
601
+ height: var(--iso-ctrl-h);
602
+ padding: 0;
603
+ cursor: pointer;
604
+ background: transparent;
605
+ }
606
+ }
236
607
  .compact-row {
237
608
  flex-wrap: wrap;
238
609
  gap: 4pt 14pt;
@@ -245,28 +616,136 @@
245
616
  opacity: 0.7;
246
617
  padding: 2px 0;
247
618
  }
248
- .layer-row {
619
+ .volume-group {
620
+ display: flex;
621
+ flex-direction: column;
622
+ gap: 3px;
623
+ min-width: 0;
624
+ padding: 3px 0;
625
+ border-top: 1px solid light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.12));
626
+ }
627
+ .volume-header {
628
+ display: flex;
629
+ flex-wrap: wrap;
630
+ align-items: center;
631
+ gap: 0.4em;
632
+ min-width: 0;
633
+ }
634
+ .volume-label {
635
+ font-weight: 600;
636
+ overflow: hidden;
637
+ text-overflow: ellipsis;
638
+ white-space: nowrap;
639
+ min-width: 0;
640
+ max-width: 14em;
641
+ }
642
+ .volume-dims,
643
+ .volume-note {
644
+ opacity: 0.6;
645
+ white-space: nowrap;
646
+ }
647
+ .volume-header .icon-btn:first-of-type {
648
+ margin-left: auto;
649
+ }
650
+ .icon-btn {
651
+ display: inline-grid;
652
+ place-items: center;
653
+ width: var(--iso-ctrl-h);
654
+ height: var(--iso-ctrl-h);
655
+ padding: 0;
656
+ border: none;
657
+ border-radius: var(--iso-ctrl-radius);
658
+ background: transparent;
659
+ color: inherit;
660
+ font-size: 0.875rem;
661
+ line-height: 1;
662
+ opacity: 0.7;
663
+ cursor: pointer;
664
+ }
665
+ .icon-btn:hover {
666
+ opacity: 1;
667
+ background: light-dark(rgba(0, 0, 0, 0.08), rgba(255, 255, 255, 0.15));
668
+ }
669
+ .layer-row,
670
+ .color-row {
249
671
  display: flex;
672
+ flex-wrap: wrap;
250
673
  align-items: center;
251
674
  gap: 0.3em;
252
- font-size: 0.85em;
253
- input[type='color'] {
254
- width: 24px;
255
- height: 20px;
256
- padding: 0;
257
- border: 1px solid var(--border-color, #ccc);
258
- border-radius: 3px;
259
- box-sizing: border-box;
260
- cursor: pointer;
261
- }
675
+ min-width: 0;
262
676
  input[type='checkbox'] {
263
677
  margin: 0;
678
+ flex-shrink: 0;
679
+ }
680
+ }
681
+ .layer-row {
682
+ .slider-field {
683
+ display: inline-flex;
684
+ align-items: center;
685
+ gap: 2px;
686
+ min-width: 0;
687
+ }
688
+ .slider-field + .slider-field {
689
+ margin-inline-start: 0.7em;
690
+ }
691
+ .slider-field:has(.isovalue-slider) {
692
+ flex: 1 1 6em;
693
+ }
694
+ .isovalue-slider {
695
+ flex: 1 1 4em;
696
+ min-width: 3em;
697
+ margin: 0;
698
+ }
699
+ .opacity-slider {
700
+ width: 40px;
701
+ min-width: 0;
702
+ margin: 0;
703
+ }
704
+ .layer-value {
705
+ font-family: monospace;
706
+ font-variant-numeric: tabular-nums;
707
+ }
708
+ }
709
+ .color-row {
710
+ padding-left: 1.4em;
711
+ label {
712
+ display: flex;
713
+ align-items: center;
714
+ gap: 4pt;
715
+ min-width: 0;
716
+ }
717
+ select {
718
+ max-width: min(9em, 100%);
719
+ }
720
+ :global(.multiselect) {
721
+ flex: 1 1 10em;
722
+ min-width: 0;
723
+ font: inherit;
724
+ color: inherit;
725
+ }
726
+ }
727
+ .range-input {
728
+ width: 4.5em;
729
+ }
730
+ .color-range {
731
+ display: flex;
732
+ flex-wrap: wrap;
733
+ align-items: center;
734
+ gap: 3px;
735
+ }
736
+ .display-range {
737
+ gap: 4pt 8pt;
738
+ .range-axis {
739
+ display: flex;
740
+ align-items: center;
741
+ gap: 3pt;
742
+ input {
743
+ width: 3.8em;
744
+ }
264
745
  }
265
746
  }
266
- .layer-value {
267
- font-family: monospace;
268
- font-size: 0.85em;
269
- min-width: 3.5em;
270
- text-align: right;
747
+ .compat-warning {
748
+ cursor: help;
749
+ color: light-dark(#b45309, #fbbf24);
271
750
  }
272
751
  </style>