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
@@ -0,0 +1,408 @@
1
+ import { create_cart_to_frac_matrix, scale_lattice_matrix } from '../math';
2
+ import { grid_dimensions } from './grid';
3
+ import { downsample_grid, MAX_GRID_POINTS } from './types';
4
+ const safe_mod = (val, dim) => ((val % dim) + dim) % dim;
5
+ // Lower/upper voxel indices for one axis of a trilinear sample. Singleton axes
6
+ // (n===1) pin both corners to 0 so nx-2 clamping never goes negative.
7
+ const voxel_corners = (n, floor_g, periodic) => {
8
+ if (n === 1)
9
+ return [0, 0];
10
+ const lower = periodic ? safe_mod(floor_g, n) : Math.max(0, Math.min(floor_g, n - 2));
11
+ return [lower, periodic ? (lower + 1) % n : Math.min(lower + 1, n - 1)];
12
+ };
13
+ // Trilinear interpolation of a scalar 3D grid at fractional coordinates.
14
+ // Periodic grids wrap with modulo; non-periodic return 0 for out-of-bounds.
15
+ export function trilinear_interpolate(grid, fx, fy, fz, periodic) {
16
+ const nx = grid.length;
17
+ const ny = grid[0]?.length ?? 0;
18
+ const nz = grid[0]?.[0]?.length ?? 0;
19
+ if (nx === 0 || ny === 0 || nz === 0)
20
+ return 0;
21
+ // Convert fractional to grid coordinates
22
+ const gx = periodic ? fx * nx : fx * (nx - 1);
23
+ const gy = periodic ? fy * ny : fy * (ny - 1);
24
+ const gz = periodic ? fz * nz : fz * (nz - 1);
25
+ if (!periodic) {
26
+ // Out-of-bounds check for non-periodic grids
27
+ if (fx < 0 || fx > 1 || fy < 0 || fy > 1 || fz < 0 || fz > 1)
28
+ return 0;
29
+ }
30
+ const [x0, x1] = voxel_corners(nx, Math.floor(gx), periodic);
31
+ const [y0, y1] = voxel_corners(ny, Math.floor(gy), periodic);
32
+ const [z0, z1] = voxel_corners(nz, Math.floor(gz), periodic);
33
+ // deltas from clamped lower index (non-periodic x0 clamps to nx-2 so floor(gx) may != x0)
34
+ const xd = periodic ? gx - Math.floor(gx) : gx - x0;
35
+ const yd = periodic ? gy - Math.floor(gy) : gy - y0;
36
+ const zd = periodic ? gz - Math.floor(gz) : gz - z0;
37
+ // 8-point interpolation
38
+ const c000 = grid[x0][y0][z0];
39
+ const c001 = grid[x0][y0][z1];
40
+ const c010 = grid[x0][y1][z0];
41
+ const c011 = grid[x0][y1][z1];
42
+ const c100 = grid[x1][y0][z0];
43
+ const c101 = grid[x1][y0][z1];
44
+ const c110 = grid[x1][y1][z0];
45
+ const c111 = grid[x1][y1][z1];
46
+ const c00 = c000 + (c100 - c000) * xd;
47
+ const c01 = c001 + (c101 - c001) * xd;
48
+ const c10 = c010 + (c110 - c010) * xd;
49
+ const c11 = c011 + (c111 - c011) * xd;
50
+ const c0 = c00 + (c10 - c00) * yd;
51
+ const c1 = c01 + (c11 - c01) * yd;
52
+ return c0 + (c1 - c0) * zd;
53
+ }
54
+ // Small tolerance so vertices numerically on the boundary of a finite grid
55
+ // (e.g. marching-cubes output at fractional coordinate 1.0 + 1e-16) still sample.
56
+ const OOB_TOL = 1e-6;
57
+ const prepared_sampler_cache = new WeakMap();
58
+ const prepare_volume_sampler = (volume) => {
59
+ const grid_dims = grid_dimensions(volume.grid);
60
+ const signature = [
61
+ ...volume.lattice.flat(),
62
+ ...volume.origin,
63
+ ...grid_dims,
64
+ Number(volume.periodic),
65
+ ].join(`,`);
66
+ const cached = prepared_sampler_cache.get(volume);
67
+ if (cached?.grid === volume.grid && cached.signature === signature)
68
+ return cached;
69
+ const prepared = {
70
+ grid: volume.grid,
71
+ inv: create_cart_to_frac_matrix(volume.lattice),
72
+ origin: volume.origin,
73
+ periodic: volume.periodic,
74
+ grid_dims,
75
+ signature,
76
+ };
77
+ prepared_sampler_cache.set(volume, prepared);
78
+ return prepared;
79
+ };
80
+ // Create a sampler that maps absolute Cartesian positions (in the same physical
81
+ // frame as the volume's `origin`) to trilinearly-interpolated scalar values.
82
+ // Handles non-orthogonal lattices, origin offsets, and periodic wrapping, so it
83
+ // works both for strictly matching grids and for general resampling between
84
+ // volumes that share a Cartesian coordinate system.
85
+ // The hot callback uses scalar arithmetic only (no per-call array allocations)
86
+ // since it runs once per isosurface vertex.
87
+ export function create_volume_sampler(volume, options = {}) {
88
+ const { out_of_bounds = `clamp` } = options;
89
+ const { grid, inv, origin, periodic } = prepare_volume_sampler(volume);
90
+ const [ox, oy, oz] = origin;
91
+ return (position) => {
92
+ const cx = position[0] - ox;
93
+ const cy = position[1] - oy;
94
+ const cz = position[2] - oz;
95
+ let fx = inv[0][0] * cx + inv[0][1] * cy + inv[0][2] * cz;
96
+ let fy = inv[1][0] * cx + inv[1][1] * cy + inv[1][2] * cz;
97
+ let fz = inv[2][0] * cx + inv[2][1] * cy + inv[2][2] * cz;
98
+ if (periodic) {
99
+ fx = safe_mod(fx, 1);
100
+ fy = safe_mod(fy, 1);
101
+ fz = safe_mod(fz, 1);
102
+ }
103
+ else {
104
+ if (out_of_bounds === `fallback` &&
105
+ (fx < -OOB_TOL ||
106
+ fx > 1 + OOB_TOL ||
107
+ fy < -OOB_TOL ||
108
+ fy > 1 + OOB_TOL ||
109
+ fz < -OOB_TOL ||
110
+ fz > 1 + OOB_TOL))
111
+ return NaN;
112
+ // Clamp both genuine out-of-bounds ('clamp' policy = nearest edge value)
113
+ // and tiny numerical overshoot at the boundary
114
+ fx = Math.min(1, Math.max(0, fx));
115
+ fy = Math.min(1, Math.max(0, fy));
116
+ fz = Math.min(1, Math.max(0, fz));
117
+ }
118
+ return trilinear_interpolate(grid, fx, fy, fz, periodic);
119
+ };
120
+ }
121
+ // Sample a volume at many Cartesian positions (flat xyz triplets).
122
+ // Returns one scalar per position; NaN marks out-of-bounds under 'fallback' policy.
123
+ export function sample_volume_at_positions(volume, positions, options = {}) {
124
+ const { out_of_bounds = `clamp`, position_offset = [0, 0, 0] } = options;
125
+ const { grid, inv, origin, periodic, grid_dims } = prepare_volume_sampler(volume);
126
+ const [nx, ny, nz] = grid_dims;
127
+ const [offset_x, offset_y, offset_z] = position_offset;
128
+ const [origin_x, origin_y, origin_z] = origin;
129
+ const n_points = Math.floor(positions.length / 3);
130
+ const out = options.out?.length === n_points ? options.out : new Float32Array(n_points);
131
+ if (nx === 0 || ny === 0 || nz === 0) {
132
+ out.fill(0);
133
+ return out;
134
+ }
135
+ for (let idx = 0; idx < n_points; idx++) {
136
+ const position_idx = idx * 3;
137
+ const cart_x = positions[position_idx] + offset_x - origin_x;
138
+ const cart_y = positions[position_idx + 1] + offset_y - origin_y;
139
+ const cart_z = positions[position_idx + 2] + offset_z - origin_z;
140
+ let frac_x = inv[0][0] * cart_x + inv[0][1] * cart_y + inv[0][2] * cart_z;
141
+ let frac_y = inv[1][0] * cart_x + inv[1][1] * cart_y + inv[1][2] * cart_z;
142
+ let frac_z = inv[2][0] * cart_x + inv[2][1] * cart_y + inv[2][2] * cart_z;
143
+ if (periodic) {
144
+ frac_x = safe_mod(frac_x, 1);
145
+ frac_y = safe_mod(frac_y, 1);
146
+ frac_z = safe_mod(frac_z, 1);
147
+ }
148
+ else {
149
+ if (out_of_bounds === `fallback` &&
150
+ (frac_x < -OOB_TOL ||
151
+ frac_x > 1 + OOB_TOL ||
152
+ frac_y < -OOB_TOL ||
153
+ frac_y > 1 + OOB_TOL ||
154
+ frac_z < -OOB_TOL ||
155
+ frac_z > 1 + OOB_TOL)) {
156
+ out[idx] = Number.NaN;
157
+ continue;
158
+ }
159
+ frac_x = Math.min(1, Math.max(0, frac_x));
160
+ frac_y = Math.min(1, Math.max(0, frac_y));
161
+ frac_z = Math.min(1, Math.max(0, frac_z));
162
+ }
163
+ const grid_x = frac_x * (periodic ? nx : nx - 1);
164
+ const grid_y = frac_y * (periodic ? ny : ny - 1);
165
+ const grid_z = frac_z * (periodic ? nz : nz - 1);
166
+ const floor_x = Math.floor(grid_x);
167
+ const floor_y = Math.floor(grid_y);
168
+ const floor_z = Math.floor(grid_z);
169
+ const [x_lower, x_upper] = voxel_corners(nx, floor_x, periodic);
170
+ const [y_lower, y_upper] = voxel_corners(ny, floor_y, periodic);
171
+ const [z_lower, z_upper] = voxel_corners(nz, floor_z, periodic);
172
+ const x_weight = grid_x - x_lower;
173
+ const y_weight = grid_y - y_lower;
174
+ const z_weight = grid_z - z_lower;
175
+ const row_00 = grid[x_lower][y_lower];
176
+ const row_01 = grid[x_lower][y_upper];
177
+ const row_10 = grid[x_upper][y_lower];
178
+ const row_11 = grid[x_upper][y_upper];
179
+ const value_00 = row_00[z_lower] + (row_10[z_lower] - row_00[z_lower]) * x_weight;
180
+ const value_01 = row_00[z_upper] + (row_10[z_upper] - row_00[z_upper]) * x_weight;
181
+ const value_10 = row_01[z_lower] + (row_11[z_lower] - row_01[z_lower]) * x_weight;
182
+ const value_11 = row_01[z_upper] + (row_11[z_upper] - row_01[z_upper]) * x_weight;
183
+ const value_0 = value_00 + (value_10 - value_00) * y_weight;
184
+ const value_1 = value_01 + (value_11 - value_01) * y_weight;
185
+ out[idx] = value_0 + (value_1 - value_0) * z_weight;
186
+ }
187
+ return out;
188
+ }
189
+ const incompatible_grids = (reason) => ({ ok: false, reason });
190
+ const fmt_vec = (vec) => `[${vec.map((val) => Number(val.toPrecision(6))).join(`, `)}]`;
191
+ // Strictly compare two volumetric grids: identical dimensions, origins equal
192
+ // within tolerance, all voxel vectors equal within tolerance, and matching
193
+ // periodic/finite boundary modes. Cross-volume coloring via create_volume_sampler
194
+ // works for any two volumes sharing a Cartesian frame, but strictly matching
195
+ // grids guarantee the sampled field is exact (no cross-grid resampling).
196
+ export function compare_volume_grids(vol_a, vol_b, { tolerance = 1e-4 } = {}) {
197
+ const [nx_a, ny_a, nz_a] = vol_a.grid_dims;
198
+ const [nx_b, ny_b, nz_b] = vol_b.grid_dims;
199
+ if (nx_a !== nx_b || ny_a !== ny_b || nz_a !== nz_b) {
200
+ return incompatible_grids(`grid dimensions differ: ${nx_a}×${ny_a}×${nz_a} vs ${nx_b}×${ny_b}×${nz_b}`);
201
+ }
202
+ if (vol_a.periodic !== vol_b.periodic) {
203
+ return incompatible_grids(`boundary modes differ: ${vol_a.periodic ? `periodic` : `finite`} vs ${vol_b.periodic ? `periodic` : `finite`}`);
204
+ }
205
+ for (let axis = 0; axis < 3; axis++) {
206
+ const delta = Math.abs(vol_a.origin[axis] - vol_b.origin[axis]);
207
+ if (delta > tolerance) {
208
+ return incompatible_grids(`origins differ by ${delta.toPrecision(3)} Å along axis ${axis}: ${fmt_vec(vol_a.origin)} vs ${fmt_vec(vol_b.origin)}`);
209
+ }
210
+ }
211
+ // Compare voxel vectors: lattice_row / n for periodic grids (spacing 1/n),
212
+ // lattice_row / (n-1) for finite grids (spacing 1/(n-1), endpoints included).
213
+ // Dims and boundary modes already match at this point, so one divisor per
214
+ // axis suffices for both volumes.
215
+ for (let axis = 0; axis < 3; axis++) {
216
+ const divisor = Math.max(vol_a.periodic ? vol_a.grid_dims[axis] : vol_a.grid_dims[axis] - 1, 1);
217
+ for (let comp = 0; comp < 3; comp++) {
218
+ const voxel_delta = Math.abs(vol_a.lattice[axis][comp] - vol_b.lattice[axis][comp]) / divisor;
219
+ if (voxel_delta > tolerance) {
220
+ return incompatible_grids(`voxel vectors differ along axis ${axis}: ${fmt_vec(vol_a.lattice[axis])} vs ${fmt_vec(vol_b.lattice[axis])} (lattice rows)`);
221
+ }
222
+ }
223
+ }
224
+ return { ok: true };
225
+ }
226
+ // Replace invalid axes (non-finite bounds or non-positive width) with [0, 1].
227
+ // Non-periodic volumes additionally clamp to [0, 1] — a finite volume must not
228
+ // be repeated implicitly.
229
+ export function sanitize_display_range(range, periodic) {
230
+ return range.map(([lo, hi]) => {
231
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi - lo <= 1e-6)
232
+ return [0, 1];
233
+ if (periodic)
234
+ return [lo, hi];
235
+ const clamped = [Math.max(lo, 0), Math.min(hi, 1)];
236
+ return clamped[1] - clamped[0] > 1e-6 ? clamped : [0, 1];
237
+ });
238
+ }
239
+ // Resolve the finite window used for marching cubes. Periodic volumes always
240
+ // need an endpoint-inclusive window, including for an integer structure
241
+ // supercell: a periodic N-point grid spans [0, 1), while finite marching cubes
242
+ // interprets N points as spanning [0, 1]. Resampling [0, S] to N*S + 1 points
243
+ // preserves the original voxel spacing and closes the final periodic interval.
244
+ // Finite volumes only get a window when one is explicitly requested and are
245
+ // never repeated to follow the structure supercell.
246
+ export function resolve_volume_display_range(volume, { display_range, tiling = [1, 1, 1], halo = 0 } = {}) {
247
+ if (!display_range && !volume.periodic)
248
+ return null;
249
+ const range = display_range
250
+ ? sanitize_display_range(display_range, volume.periodic)
251
+ : tiling.map((factor) => [0, factor]);
252
+ if (!volume.periodic)
253
+ return range;
254
+ const padding = Math.max(0, halo);
255
+ return range.map(([lo, hi]) => [lo - padding, hi + padding]);
256
+ }
257
+ const precompute_axis_interpolation = (range, count, source_dim, periodic) => {
258
+ const intervals = periodic ? source_dim : Math.max(source_dim - 1, 1);
259
+ const fractional_step = (range[1] - range[0]) / (count - 1);
260
+ const grid_step = fractional_step * intervals;
261
+ const grid_start = range[0] * intervals;
262
+ const direct = Math.abs(grid_step - 1) < 1e-10 && Math.abs(grid_start - Math.round(grid_start)) < 1e-10;
263
+ const lower = new Int32Array(count);
264
+ const upper = new Int32Array(count);
265
+ const weight = new Float64Array(count);
266
+ for (let sample_idx = 0; sample_idx < count; sample_idx++) {
267
+ const grid_coord = grid_start + sample_idx * grid_step;
268
+ const grid_floor = Math.floor(grid_coord);
269
+ if (periodic) {
270
+ lower[sample_idx] = safe_mod(grid_floor, source_dim);
271
+ upper[sample_idx] = (lower[sample_idx] + 1) % source_dim;
272
+ weight[sample_idx] = grid_coord - grid_floor;
273
+ }
274
+ else {
275
+ lower[sample_idx] = Math.max(0, Math.min(grid_floor, source_dim - 2));
276
+ upper[sample_idx] = Math.min(lower[sample_idx] + 1, source_dim - 1);
277
+ weight[sample_idx] = grid_coord - lower[sample_idx];
278
+ }
279
+ }
280
+ return { lower, upper, weight, direct };
281
+ };
282
+ // Resample a volume over a fractional display range (VESTA-style non-integer
283
+ // supercell) into a finite grid whose bounds land exactly on the requested
284
+ // fractional coordinates, so marching cubes clips repeated surfaces precisely
285
+ // at the range instead of at integer cell boundaries. Periodic volumes wrap;
286
+ // the sample resolution follows the source voxel density, capped at max_points
287
+ // (matching the downsampling budget applied to integer-tiled grids).
288
+ // The returned volume's lattice spans the range and its origin shifts by
289
+ // range_min·lattice, keeping vertices in the same Cartesian frame as the source.
290
+ export function extract_volume_range(volume, range, max_points = MAX_GRID_POINTS) {
291
+ const [rx, ry, rz] = sanitize_display_range(range, volume.periodic);
292
+ const widths = [rx[1] - rx[0], ry[1] - ry[0], rz[1] - rz[0]];
293
+ // Sample counts follow the source voxel density (inclusive endpoints), capped
294
+ // to the point budget. The reduction loop guards against cbrt undershoot when
295
+ // the min-2 floor prevents an axis from shrinking.
296
+ let counts = widths.map((width, axis) => {
297
+ const source_intervals = volume.periodic
298
+ ? volume.grid_dims[axis]
299
+ : Math.max(volume.grid_dims[axis] - 1, 1);
300
+ return Math.max(2, Math.round(width * source_intervals) + 1);
301
+ });
302
+ const total = counts[0] * counts[1] * counts[2];
303
+ if (total > max_points) {
304
+ const shrink = Math.cbrt(max_points / total);
305
+ counts = counts.map((count) => Math.max(2, Math.floor(count * shrink)));
306
+ while (counts[0] * counts[1] * counts[2] > Math.max(max_points, 8)) {
307
+ const largest = counts.indexOf(Math.max(...counts));
308
+ counts[largest] = Math.max(2, Math.floor(counts[largest] * 0.9));
309
+ }
310
+ }
311
+ const [nx, ny, nz] = counts;
312
+ const grid = Array(nx);
313
+ const x_samples = precompute_axis_interpolation(rx, nx, volume.grid_dims[0], volume.periodic);
314
+ const y_samples = precompute_axis_interpolation(ry, ny, volume.grid_dims[1], volume.periodic);
315
+ const z_samples = precompute_axis_interpolation(rz, nz, volume.grid_dims[2], volume.periodic);
316
+ let [min_val, max_val, sum] = [Infinity, -Infinity, 0];
317
+ const direct_copy = x_samples.direct && y_samples.direct && z_samples.direct;
318
+ if (direct_copy) {
319
+ for (let x_idx = 0; x_idx < nx; x_idx++) {
320
+ const source_x = x_samples.weight[x_idx] > 0.5 ? x_samples.upper[x_idx] : x_samples.lower[x_idx];
321
+ const source_plane = volume.grid[source_x];
322
+ const plane = Array(ny);
323
+ for (let y_idx = 0; y_idx < ny; y_idx++) {
324
+ const source_y = y_samples.weight[y_idx] > 0.5 ? y_samples.upper[y_idx] : y_samples.lower[y_idx];
325
+ const source_row = source_plane[source_y];
326
+ const row = Array(nz);
327
+ for (let z_idx = 0; z_idx < nz; z_idx++) {
328
+ const source_z = z_samples.weight[z_idx] > 0.5 ? z_samples.upper[z_idx] : z_samples.lower[z_idx];
329
+ const value = source_row[source_z];
330
+ row[z_idx] = value;
331
+ if (value < min_val)
332
+ min_val = value;
333
+ if (value > max_val)
334
+ max_val = value;
335
+ sum += value;
336
+ }
337
+ plane[y_idx] = row;
338
+ }
339
+ grid[x_idx] = plane;
340
+ }
341
+ }
342
+ else {
343
+ for (let x_idx = 0; x_idx < nx; x_idx++) {
344
+ const x_lower_plane = volume.grid[x_samples.lower[x_idx]];
345
+ const x_upper_plane = volume.grid[x_samples.upper[x_idx]];
346
+ const x_weight = x_samples.weight[x_idx];
347
+ const plane = Array(ny);
348
+ for (let y_idx = 0; y_idx < ny; y_idx++) {
349
+ const y_lower = y_samples.lower[y_idx];
350
+ const y_upper = y_samples.upper[y_idx];
351
+ const y_weight = y_samples.weight[y_idx];
352
+ const row_00 = x_lower_plane[y_lower];
353
+ const row_01 = x_lower_plane[y_upper];
354
+ const row_10 = x_upper_plane[y_lower];
355
+ const row_11 = x_upper_plane[y_upper];
356
+ const row = Array(nz);
357
+ for (let z_idx = 0; z_idx < nz; z_idx++) {
358
+ const z_lower = z_samples.lower[z_idx];
359
+ const z_upper = z_samples.upper[z_idx];
360
+ const z_weight = z_samples.weight[z_idx];
361
+ const value_00 = row_00[z_lower] + (row_10[z_lower] - row_00[z_lower]) * x_weight;
362
+ const value_01 = row_00[z_upper] + (row_10[z_upper] - row_00[z_upper]) * x_weight;
363
+ const value_10 = row_01[z_lower] + (row_11[z_lower] - row_01[z_lower]) * x_weight;
364
+ const value_11 = row_01[z_upper] + (row_11[z_upper] - row_01[z_upper]) * x_weight;
365
+ const value_0 = value_00 + (value_10 - value_00) * y_weight;
366
+ const value_1 = value_01 + (value_11 - value_01) * y_weight;
367
+ const value = value_0 + (value_1 - value_0) * z_weight;
368
+ row[z_idx] = value;
369
+ if (value < min_val)
370
+ min_val = value;
371
+ if (value > max_val)
372
+ max_val = value;
373
+ sum += value;
374
+ }
375
+ plane[y_idx] = row;
376
+ }
377
+ grid[x_idx] = plane;
378
+ }
379
+ }
380
+ const [row_a, row_b, row_c] = volume.lattice;
381
+ return {
382
+ ...volume,
383
+ grid,
384
+ grid_dims: counts,
385
+ lattice: scale_lattice_matrix(volume.lattice, widths),
386
+ origin: [
387
+ volume.origin[0] + rx[0] * row_a[0] + ry[0] * row_b[0] + rz[0] * row_c[0],
388
+ volume.origin[1] + rx[0] * row_a[1] + ry[0] * row_b[1] + rz[0] * row_c[1],
389
+ volume.origin[2] + rx[0] * row_a[2] + ry[0] * row_b[2] + rz[0] * row_c[2],
390
+ ],
391
+ data_range: {
392
+ min: min_val,
393
+ max: max_val,
394
+ abs_max: Math.max(Math.abs(min_val), Math.abs(max_val)),
395
+ mean: sum / (nx * ny * nz),
396
+ },
397
+ periodic: false, // the extracted block is a finite window; endpoints included
398
+ };
399
+ }
400
+ export function prepare_geometry_grid(volume, range) {
401
+ if (range)
402
+ return extract_volume_range(volume, range);
403
+ return {
404
+ grid: downsample_grid(volume.grid, volume.grid_dims).grid,
405
+ lattice: volume.lattice,
406
+ origin: volume.origin,
407
+ };
408
+ }
@@ -0,0 +1,13 @@
1
+ import { type D3InterpolateName } from '../colors';
2
+ import type { Vec2 } from '../math';
3
+ import type { SliceResult } from './slice';
4
+ export type VolumeSliceMode = `both` | `contours` | `filled`;
5
+ /** Resolve an explicit or automatic slice color range. */
6
+ export declare function resolve_slice_color_range(slice: Pick<SliceResult, `min` | `max`>, color_range?: Vec2, symmetric?: boolean | `auto`): Vec2;
7
+ /** Convert a sampled slice to browser-sRGB RGBA pixels, preserving its exact mask. */
8
+ export declare function slice_to_rgba(slice: Pick<SliceResult, `data` | `mask` | `width` | `height`>, colormap: D3InterpolateName, color_range: Vec2, { flip_y, out }?: {
9
+ flip_y?: boolean;
10
+ out?: Uint8ClampedArray;
11
+ }): Uint8ClampedArray;
12
+ /** Resolve a contour count or explicit threshold list against a color range. */
13
+ export declare function resolve_contour_thresholds(color_range: Vec2, contour_levels: number | number[]): number[];
@@ -0,0 +1,79 @@
1
+ import { get_d3_interpolator } from '../colors';
2
+ import { rgb } from 'd3-color';
3
+ const SLICE_LUT_SIZE = 256;
4
+ const MAX_CONTOUR_LEVELS = 256;
5
+ const slice_lut_cache = new Map();
6
+ const get_slice_lut = (colormap) => {
7
+ const cached = slice_lut_cache.get(colormap);
8
+ if (cached)
9
+ return cached;
10
+ const interpolator = get_d3_interpolator(colormap);
11
+ const lut = new Uint8ClampedArray(SLICE_LUT_SIZE * 3);
12
+ for (let color_idx = 0; color_idx < SLICE_LUT_SIZE; color_idx++) {
13
+ const color = rgb(interpolator(color_idx / (SLICE_LUT_SIZE - 1)));
14
+ lut[color_idx * 3] = color.r;
15
+ lut[color_idx * 3 + 1] = color.g;
16
+ lut[color_idx * 3 + 2] = color.b;
17
+ }
18
+ slice_lut_cache.set(colormap, lut);
19
+ return lut;
20
+ };
21
+ /** Resolve an explicit or automatic slice color range. */
22
+ export function resolve_slice_color_range(slice, color_range, symmetric = `auto`) {
23
+ if (color_range)
24
+ return [...color_range];
25
+ if (symmetric === true || (symmetric === `auto` && slice.min < 0 && slice.max > 0)) {
26
+ const abs_max = Math.max(Math.abs(slice.min), Math.abs(slice.max));
27
+ return [-abs_max, abs_max];
28
+ }
29
+ return [slice.min, slice.max];
30
+ }
31
+ /** Convert a sampled slice to browser-sRGB RGBA pixels, preserving its exact mask. */
32
+ export function slice_to_rgba(slice, colormap, color_range, { flip_y = true, out } = {}) {
33
+ const pixels = out?.length === slice.data.length * 4 ? out : new Uint8ClampedArray(slice.data.length * 4);
34
+ const lut = get_slice_lut(colormap);
35
+ const [range_min, range_max] = color_range;
36
+ const span = range_max - range_min;
37
+ const inv_span = span === 0 ? 0 : 1 / span;
38
+ for (let row_idx = 0; row_idx < slice.height; row_idx++) {
39
+ const target_row = flip_y ? slice.height - 1 - row_idx : row_idx;
40
+ for (let col_idx = 0; col_idx < slice.width; col_idx++) {
41
+ const source_idx = row_idx * slice.width + col_idx;
42
+ const pixel_idx = (target_row * slice.width + col_idx) * 4;
43
+ if (!slice.mask[source_idx] || !Number.isFinite(slice.data[source_idx])) {
44
+ pixels[pixel_idx] = 0;
45
+ pixels[pixel_idx + 1] = 0;
46
+ pixels[pixel_idx + 2] = 0;
47
+ pixels[pixel_idx + 3] = 0;
48
+ continue;
49
+ }
50
+ const normalized = span === 0
51
+ ? 0.5
52
+ : Math.max(0, Math.min(1, (slice.data[source_idx] - range_min) * inv_span));
53
+ const lut_idx = Math.round(normalized * (SLICE_LUT_SIZE - 1)) * 3;
54
+ pixels[pixel_idx] = lut[lut_idx];
55
+ pixels[pixel_idx + 1] = lut[lut_idx + 1];
56
+ pixels[pixel_idx + 2] = lut[lut_idx + 2];
57
+ pixels[pixel_idx + 3] = 255;
58
+ }
59
+ }
60
+ return pixels;
61
+ }
62
+ /** Resolve a contour count or explicit threshold list against a color range. */
63
+ export function resolve_contour_thresholds(color_range, contour_levels) {
64
+ if (Array.isArray(contour_levels)) {
65
+ // Sort before truncating so the cap keeps the lowest thresholds deterministically
66
+ // regardless of input order
67
+ return contour_levels
68
+ .filter(Number.isFinite)
69
+ .sort((left, right) => left - right)
70
+ .slice(0, MAX_CONTOUR_LEVELS);
71
+ }
72
+ const count = Number.isFinite(contour_levels)
73
+ ? Math.min(MAX_CONTOUR_LEVELS, Math.max(0, Math.floor(contour_levels)))
74
+ : 0;
75
+ const [range_min, range_max] = color_range;
76
+ if (count === 0 || range_min === range_max)
77
+ return [];
78
+ return Array.from({ length: count }, (_, level_idx) => range_min + ((level_idx + 1) / (count + 1)) * (range_max - range_min)).sort((left, right) => left - right);
79
+ }
@@ -1,11 +1,31 @@
1
- import type { Vec3 } from '../math';
1
+ import type { Vec2, Vec3 } from '../math';
2
+ import { type DisplayRange } from './sampling';
2
3
  import type { VolumetricData } from './types';
4
+ export interface CartesianPlane {
5
+ point: Vec3;
6
+ normal: Vec3;
7
+ up?: Vec3;
8
+ }
9
+ export interface PlaneSliceOptions {
10
+ resolution?: number | Vec2;
11
+ max_pixels?: number;
12
+ fractional_bounds?: DisplayRange;
13
+ }
3
14
  export interface SliceResult {
4
15
  data: Float64Array;
16
+ mask: Uint8Array;
5
17
  width: number;
6
18
  height: number;
7
19
  min: number;
8
20
  max: number;
21
+ point: Vec3;
22
+ normal: Vec3;
23
+ u_axis: Vec3;
24
+ v_axis: Vec3;
25
+ u_range: Vec2;
26
+ v_range: Vec2;
27
+ polygon: Vec2[];
9
28
  }
10
- export declare function trilinear_interpolate(grid: number[][][], fx: number, fy: number, fz: number, periodic: boolean): number;
29
+ /** Sample a scalar volume on an arbitrary absolute Cartesian plane. */
30
+ export declare function sample_plane_slice(volume: VolumetricData, plane: CartesianPlane, options?: PlaneSliceOptions): SliceResult | null;
11
31
  export declare function sample_hkl_slice(volume: VolumetricData, miller_indices: Vec3, distance: number, n_points?: number): SliceResult | null;