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.
- package/dist/FilePicker.svelte +40 -50
- package/dist/api/optimade.js +10 -7
- package/dist/app.css +15 -0
- package/dist/brillouin/BrillouinZone.svelte +18 -9
- package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
- package/dist/brillouin/compute.js +10 -16
- package/dist/brillouin/geometry.js +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
- package/dist/chempot-diagram/async-compute.svelte.js +12 -13
- package/dist/chempot-diagram/color.d.ts +21 -1
- package/dist/chempot-diagram/color.js +70 -1
- package/dist/chempot-diagram/compute.d.ts +7 -0
- package/dist/chempot-diagram/compute.js +85 -26
- package/dist/chempot-diagram/export.js +1 -6
- package/dist/chempot-diagram/temperature.js +9 -10
- package/dist/composition/BarChart.svelte +7 -9
- package/dist/composition/Composition.svelte +3 -8
- package/dist/composition/Formula.svelte +2 -2
- package/dist/composition/FormulaFilter.svelte +27 -93
- package/dist/composition/format.js +6 -8
- package/dist/composition/parse.js +50 -105
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +6 -17
- package/dist/convex-hull/ConvexHull2D.svelte +0 -2
- package/dist/convex-hull/ConvexHull3D.svelte +19 -47
- package/dist/convex-hull/ConvexHull4D.svelte +65 -95
- package/dist/convex-hull/StructurePopup.svelte +1 -1
- package/dist/convex-hull/gas-thermodynamics.js +21 -53
- package/dist/convex-hull/helpers.js +1 -7
- package/dist/convex-hull/thermodynamics.js +19 -27
- package/dist/coordination/CoordinationBarPlot.svelte +12 -21
- package/dist/element/BohrAtom.svelte +0 -1
- package/dist/element/ElementTile.svelte +26 -44
- package/dist/element/data.d.ts +2 -0
- package/dist/element/data.js +5 -1
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -1
- package/dist/fermi-surface/FermiSurface.svelte +22 -23
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
- package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
- package/dist/fermi-surface/compute.js +22 -55
- package/dist/fermi-surface/parse.js +38 -70
- package/dist/file-viewer/JsonBrowser.svelte +1043 -0
- package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
- package/dist/file-viewer/PlotPanel.svelte +350 -0
- package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
- package/dist/file-viewer/detect.d.ts +11 -0
- package/dist/file-viewer/detect.js +381 -0
- package/dist/file-viewer/eligibility.d.ts +4 -0
- package/dist/file-viewer/eligibility.js +53 -0
- package/dist/file-viewer/host-protocol.d.ts +50 -0
- package/dist/file-viewer/host-transfer.d.ts +25 -0
- package/dist/file-viewer/host-transfer.js +42 -0
- package/dist/file-viewer/index.d.ts +5 -0
- package/dist/file-viewer/index.js +5 -0
- package/dist/file-viewer/main.d.ts +44 -0
- package/dist/file-viewer/main.js +535 -0
- package/dist/file-viewer/parse.d.ts +9 -0
- package/dist/file-viewer/parse.js +170 -0
- package/dist/file-viewer/plot-utils.d.ts +24 -0
- package/dist/file-viewer/plot-utils.js +191 -0
- package/dist/file-viewer/types.d.ts +4 -0
- package/dist/file-viewer/types.js +5 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/io/ExportPane.svelte +1 -1
- package/dist/io/decompress.d.ts +2 -0
- package/dist/io/decompress.js +3 -2
- package/dist/io/export.d.ts +7 -4
- package/dist/io/export.js +70 -18
- package/dist/io/file-drop.js +60 -35
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +28 -16
- package/dist/isosurface/Isosurface.svelte +687 -196
- package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
- package/dist/isosurface/IsosurfaceControls.svelte +564 -85
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
- package/dist/isosurface/VolumeSlice.svelte +224 -0
- package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
- package/dist/isosurface/coloring.d.ts +20 -0
- package/dist/isosurface/coloring.js +118 -0
- package/dist/isosurface/geometry-worker-types.d.ts +38 -0
- package/dist/isosurface/geometry.worker.d.ts +1 -0
- package/dist/isosurface/geometry.worker.js +59 -0
- package/dist/isosurface/grid.d.ts +10 -0
- package/dist/isosurface/grid.js +31 -0
- package/dist/isosurface/index.d.ts +4 -0
- package/dist/isosurface/index.js +4 -0
- package/dist/isosurface/profile.d.ts +12 -0
- package/dist/isosurface/profile.js +19 -0
- package/dist/isosurface/sampling.d.ts +34 -0
- package/dist/isosurface/sampling.js +408 -0
- package/dist/isosurface/slice-rendering.d.ts +13 -0
- package/dist/isosurface/slice-rendering.js +79 -0
- package/dist/isosurface/slice.d.ts +22 -2
- package/dist/isosurface/slice.js +200 -116
- package/dist/isosurface/types.d.ts +24 -1
- package/dist/isosurface/types.js +120 -8
- package/dist/json-path.d.ts +4 -0
- package/dist/json-path.js +89 -0
- package/dist/labels.js +1 -1
- package/dist/layout/InfoCard.svelte +1 -3
- package/dist/layout/SettingsSection.svelte +52 -61
- package/dist/layout/SettingsSection.svelte.d.ts +6 -7
- package/dist/layout/ViewerChrome.svelte +7 -1
- package/dist/layout/json-tree/JsonNode.svelte +14 -48
- package/dist/layout/json-tree/JsonTree.svelte +48 -95
- package/dist/layout/json-tree/JsonValue.svelte +3 -18
- package/dist/layout/json-tree/utils.d.ts +0 -3
- package/dist/layout/json-tree/utils.js +50 -179
- package/dist/marching-cubes.d.ts +8 -1
- package/dist/marching-cubes.js +115 -48
- package/dist/math.d.ts +3 -2
- package/dist/math.js +29 -38
- package/dist/overlays/ContextMenu.svelte +15 -32
- package/dist/overlays/DraggablePane.svelte +39 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
- package/dist/overlays/GlassChip.svelte +3 -1
- package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
- package/dist/phase-diagram/build-diagram.js +1 -6
- package/dist/phase-diagram/svg-to-diagram.js +9 -14
- package/dist/plot/bar/BarPlot.svelte +38 -42
- package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
- package/dist/plot/box/BoxPlot.svelte +27 -23
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +1 -1
- package/dist/plot/core/data-transform.d.ts +1 -2
- package/dist/plot/core/data-transform.js +0 -10
- package/dist/plot/core/layout.d.ts +12 -5
- package/dist/plot/core/layout.js +37 -27
- package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
- package/dist/plot/core/pan-zoom.svelte.js +34 -17
- package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -21
- package/dist/plot/core/svg.js +0 -1
- package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
- package/dist/plot/core/utils/hierarchy-chart.js +12 -0
- package/dist/plot/core/utils/hierarchy-labels.js +3 -2
- package/dist/plot/histogram/Histogram.svelte +21 -26
- package/dist/plot/sankey/sankey.js +13 -9
- package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
- package/dist/plot/scatter/ScatterPlot.svelte +67 -52
- package/dist/plot/scatter/adaptive-density.d.ts +1 -1
- package/dist/plot/scatter/adaptive-density.js +36 -20
- package/dist/plot/sunburst/Sunburst.svelte +20 -20
- package/dist/plot/treemap/Treemap.svelte +175 -60
- package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
- package/dist/plot/treemap/index.d.ts +1 -0
- package/dist/plot/treemap/labels.d.ts +38 -0
- package/dist/plot/treemap/labels.js +80 -0
- package/dist/plot/treemap/treemap.d.ts +4 -2
- package/dist/plot/treemap/treemap.js +5 -2
- package/dist/rdf/RdfPlot.svelte +17 -35
- package/dist/rdf/calc-rdf.js +59 -44
- package/dist/rdf/index.d.ts +0 -1
- package/dist/sanitize.js +84 -26
- package/dist/scene/SceneCamera.svelte +1 -1
- package/dist/scene/SceneCamera.svelte.d.ts +1 -1
- package/dist/scene/index.d.ts +1 -2
- package/dist/scene/index.js +1 -1
- package/dist/scene/{props.js → props.svelte.js} +15 -1
- package/dist/settings.js +159 -482
- package/dist/spectral/Bands.svelte +151 -158
- package/dist/spectral/BrillouinBandsDos.svelte +4 -19
- package/dist/spectral/Dos.svelte +26 -24
- package/dist/spectral/helpers.js +40 -105
- package/dist/structure/AtomLegend.svelte +8 -13
- package/dist/structure/AtomLegend.svelte.d.ts +2 -3
- package/dist/structure/CellSelect.svelte +35 -10
- package/dist/structure/Structure.svelte +177 -99
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureCarousel.svelte +10 -3
- package/dist/structure/StructureControls.svelte +8 -26
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +65 -76
- package/dist/structure/StructureScene.svelte +75 -109
- package/dist/structure/StructureScene.svelte.d.ts +3 -1
- package/dist/structure/StructureViewport.svelte +120 -55
- package/dist/structure/StructureViewport.svelte.d.ts +3 -1
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +4 -3
- package/dist/structure/bond-order-perception.js +3 -8
- package/dist/structure/bonding.d.ts +0 -1
- package/dist/structure/bonding.js +16 -23
- package/dist/structure/export.js +50 -74
- package/dist/structure/format-detect.js +5 -7
- package/dist/structure/index.js +6 -12
- package/dist/structure/parse.js +38 -100
- package/dist/structure/partial-occupancy.js +7 -4
- package/dist/structure/pbc.js +8 -10
- package/dist/structure/polyhedra.js +5 -4
- package/dist/structure/prop-groups.d.ts +85 -0
- package/dist/structure/prop-groups.js +18 -0
- package/dist/symmetry/SymmetryStats.svelte +0 -4
- package/dist/symmetry/WyckoffTable.svelte +5 -4
- package/dist/symmetry/cell-transform.js +5 -14
- package/dist/symmetry/index.js +6 -9
- package/dist/table/HeatmapTable.svelte +61 -103
- package/dist/table/ToggleMenu.svelte +166 -44
- package/dist/table/ToggleMenu.svelte.d.ts +1 -1
- package/dist/theme/embedded.d.ts +18 -0
- package/dist/theme/embedded.js +206 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +1 -2
- package/dist/theme/themes.mjs +31 -60
- package/dist/tooltip/TooltipContent.svelte +1 -1
- package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
- package/dist/tooltip/index.d.ts +8 -1
- package/dist/trajectory/Trajectory.svelte +140 -120
- package/dist/trajectory/TrajectoryError.svelte +1 -66
- package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
- package/dist/trajectory/extract.js +19 -29
- package/dist/trajectory/format-detect.d.ts +2 -0
- package/dist/trajectory/format-detect.js +7 -5
- package/dist/trajectory/frame-reader.js +3 -4
- package/dist/trajectory/helpers.js +16 -49
- package/dist/trajectory/index.js +2 -8
- package/dist/trajectory/parse/ase.js +1 -1
- package/dist/trajectory/parse/index.d.ts +4 -3
- package/dist/trajectory/parse/index.js +12 -17
- package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
- package/dist/trajectory/parse/vaspout-h5.js +1 -4
- package/dist/trajectory/parse/xyz.js +48 -2
- package/dist/trajectory/plotting.js +14 -37
- package/dist/xrd/XrdPlot.svelte +43 -68
- package/dist/xrd/calc-xrd.js +22 -58
- package/dist/xrd/parse.js +13 -22
- package/package.json +40 -8
- package/readme.md +6 -5
- package/dist/scene/types.d.ts +0 -26
- package/dist/tooltip/types.d.ts +0 -8
- package/dist/trajectory/constants.d.ts +0 -6
- package/dist/trajectory/constants.js +0 -11
- /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
- /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
|
-
|
|
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;
|