matterviz 0.3.2 → 0.3.4
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/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/element/data.js +1 -1
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,477 +1,907 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import * as
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { D3InterpolateName } from '../colors'
|
|
3
|
+
import { AXIS_COLORS, get_d3_interpolator, NEG_AXIS_COLORS } from '../colors'
|
|
4
|
+
import type { ElementSymbol } from '../element'
|
|
5
|
+
import { element_data } from '../element'
|
|
6
|
+
import Isosurface from '../isosurface/Isosurface.svelte'
|
|
7
|
+
import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
|
|
8
|
+
import { DEFAULT_ISOSURFACE_SETTINGS } from '../isosurface/types'
|
|
9
|
+
import { format_num } from '../labels'
|
|
10
|
+
import type { Vec3 } from '../math'
|
|
11
|
+
import * as math from '../math'
|
|
12
|
+
import type {
|
|
13
|
+
CameraProjection,
|
|
14
|
+
ShowBonds,
|
|
15
|
+
VectorColorMode,
|
|
16
|
+
VectorLayerConfig,
|
|
17
|
+
} from '../settings'
|
|
18
|
+
import { DEFAULTS } from '../settings'
|
|
19
|
+
import { sanitize_html } from '../sanitize'
|
|
20
|
+
import { colors } from '../state.svelte'
|
|
21
|
+
import type { AnyStructure, BondPair, MeasureMode, Site } from './'
|
|
22
|
+
import {
|
|
23
|
+
Arrow,
|
|
24
|
+
atomic_radii,
|
|
25
|
+
Cylinder,
|
|
26
|
+
get_all_site_vectors,
|
|
27
|
+
get_center_of_mass,
|
|
28
|
+
get_structure_vector_keys,
|
|
29
|
+
Lattice,
|
|
30
|
+
VECTOR_PALETTE,
|
|
31
|
+
} from './'
|
|
32
|
+
import type { AtomColorConfig } from './atom-properties'
|
|
33
|
+
import {
|
|
34
|
+
get_orig_site_idx,
|
|
35
|
+
get_property_colors,
|
|
36
|
+
} from './atom-properties'
|
|
37
|
+
import * as measure from './measure'
|
|
38
|
+
import {
|
|
39
|
+
compute_slice_geometry,
|
|
40
|
+
merge_split_partial_sites,
|
|
41
|
+
PARTIAL_OCCUPANCY_CAP_ARC,
|
|
42
|
+
} from './partial-occupancy'
|
|
43
|
+
import type { MoyoDataset } from '@spglib/moyo-wasm'
|
|
44
|
+
import { T, useThrelte } from '@threlte/core'
|
|
45
|
+
import * as extras from '@threlte/extras'
|
|
46
|
+
import { type ComponentProps, type Snippet, untrack } from 'svelte'
|
|
47
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
48
|
+
import { type Camera, Color, type Mesh, type Scene } from 'three'
|
|
49
|
+
import Bond from './Bond.svelte'
|
|
50
|
+
import type { BondingStrategy } from './bonding'
|
|
51
|
+
import { BONDING_STRATEGIES, compute_bond_transform } from './bonding'
|
|
52
|
+
import { CanvasTooltip } from './index'
|
|
53
|
+
|
|
54
|
+
type InstancedAtomGroup = {
|
|
55
|
+
element: string
|
|
56
|
+
radius: number
|
|
57
|
+
color: string
|
|
58
|
+
is_image_atom: boolean
|
|
59
|
+
atoms: (typeof atom_data)[number][]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let pulse_time = $state(0)
|
|
63
|
+
let pulse_opacity = $derived(0.15 + 0.25 * Math.sin(pulse_time * 5))
|
|
64
|
+
$effect(() => {
|
|
65
|
+
if (!selected_sites?.length && !active_sites?.length) return
|
|
66
|
+
if (typeof globalThis === `undefined`) return
|
|
67
|
+
const reduce = globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches
|
|
68
|
+
if (reduce) return
|
|
69
|
+
let frame_id = 0
|
|
32
70
|
const animate = () => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
frame_id = requestAnimationFrame(animate)
|
|
37
|
-
return () => cancelAnimationFrame(frame_id)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
pulse_time += 0.015
|
|
72
|
+
frame_id = requestAnimationFrame(animate)
|
|
73
|
+
}
|
|
74
|
+
frame_id = requestAnimationFrame(animate)
|
|
75
|
+
return () => cancelAnimationFrame(frame_id)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
let {
|
|
79
|
+
structure = undefined,
|
|
80
|
+
base_structure = undefined,
|
|
81
|
+
atom_radius = DEFAULTS.structure.atom_radius,
|
|
82
|
+
same_size_atoms = false,
|
|
83
|
+
camera_position = DEFAULTS.structure.camera_position,
|
|
84
|
+
camera_target = undefined,
|
|
85
|
+
camera_projection = DEFAULTS.structure.camera_projection,
|
|
86
|
+
rotation_damping = DEFAULTS.structure.rotation_damping,
|
|
87
|
+
max_zoom = DEFAULTS.structure.max_zoom,
|
|
88
|
+
min_zoom = DEFAULTS.structure.min_zoom,
|
|
89
|
+
rotate_speed = DEFAULTS.structure.rotate_speed,
|
|
90
|
+
zoom_speed = DEFAULTS.structure.zoom_speed,
|
|
91
|
+
pan_speed = DEFAULTS.structure.pan_speed,
|
|
92
|
+
zoom_to_cursor = DEFAULTS.structure.zoom_to_cursor,
|
|
93
|
+
show_atoms = DEFAULTS.structure.show_atoms,
|
|
94
|
+
show_bonds = DEFAULTS.structure.show_bonds,
|
|
95
|
+
show_site_labels = DEFAULTS.structure.show_site_labels,
|
|
96
|
+
show_site_indices = DEFAULTS.structure.show_site_indices,
|
|
97
|
+
site_label_size = DEFAULTS.structure.site_label_size,
|
|
98
|
+
site_label_offset = $bindable(DEFAULTS.structure.site_label_offset),
|
|
99
|
+
site_label_bg_color = `color-mix(in srgb, #000000 0%, transparent)`,
|
|
100
|
+
site_label_color = `#ffffff`,
|
|
101
|
+
site_label_padding = 3,
|
|
102
|
+
vector_configs = $bindable<Record<string, VectorLayerConfig>>({}),
|
|
103
|
+
vector_scale = DEFAULTS.structure.vector_scale,
|
|
104
|
+
vector_color = DEFAULTS.structure.vector_color,
|
|
105
|
+
vector_color_mode = DEFAULTS.structure.vector_color_mode as VectorColorMode,
|
|
106
|
+
vector_color_scale = DEFAULTS.structure.vector_color_scale,
|
|
107
|
+
vector_normalize = DEFAULTS.structure.vector_normalize,
|
|
108
|
+
vector_uniform_thickness = DEFAULTS.structure.vector_uniform_thickness,
|
|
109
|
+
vector_origin_gap = DEFAULTS.structure.vector_origin_gap,
|
|
110
|
+
vector_shaft_radius = DEFAULTS.structure.vector_shaft_radius,
|
|
111
|
+
vector_arrow_head_radius = DEFAULTS.structure.vector_arrow_head_radius,
|
|
112
|
+
vector_arrow_head_length = DEFAULTS.structure.vector_arrow_head_length,
|
|
113
|
+
gizmo = DEFAULTS.structure.show_gizmo,
|
|
114
|
+
hovered_idx = $bindable(null),
|
|
115
|
+
hovered_site = $bindable(null),
|
|
116
|
+
float_fmt = `.3~f`,
|
|
117
|
+
auto_rotate = DEFAULTS.structure.auto_rotate,
|
|
118
|
+
bond_thickness = DEFAULTS.structure.bond_thickness,
|
|
119
|
+
bond_color = DEFAULTS.structure.bond_color,
|
|
120
|
+
bonding_strategy = DEFAULTS.structure.bonding_strategy,
|
|
121
|
+
bonding_options = {},
|
|
122
|
+
fov = DEFAULTS.structure.fov,
|
|
123
|
+
initial_zoom = DEFAULTS.structure.initial_zoom,
|
|
124
|
+
ambient_light = DEFAULTS.structure.ambient_light,
|
|
125
|
+
directional_light = DEFAULTS.structure.directional_light,
|
|
126
|
+
sphere_segments = DEFAULTS.structure.sphere_segments,
|
|
127
|
+
lattice_props = {},
|
|
128
|
+
atom_label,
|
|
129
|
+
camera_is_moving = $bindable(false),
|
|
130
|
+
width = 0,
|
|
131
|
+
height = 0,
|
|
132
|
+
measure_mode = `distance`,
|
|
133
|
+
selected_sites = $bindable([]),
|
|
134
|
+
measured_sites = $bindable([]),
|
|
135
|
+
added_bonds = $bindable([]),
|
|
136
|
+
removed_bonds = $bindable([]),
|
|
137
|
+
selection_highlight_color = `#6cf0ff`,
|
|
138
|
+
// Active highlight group with different color
|
|
139
|
+
active_sites = $bindable([]),
|
|
140
|
+
active_highlight_color = `var(--struct-active-highlight-color, #2563eb)`,
|
|
141
|
+
rotation = DEFAULTS.structure.rotation,
|
|
142
|
+
scene = $bindable(),
|
|
143
|
+
camera = $bindable(),
|
|
144
|
+
orbit_controls = $bindable(),
|
|
145
|
+
rotation_target_ref = $bindable(),
|
|
146
|
+
initial_computed_zoom = $bindable(),
|
|
147
|
+
hidden_elements = $bindable(new SvelteSet()),
|
|
148
|
+
hidden_prop_vals = $bindable(new SvelteSet<number | string>()),
|
|
149
|
+
element_radius_overrides = $bindable<Partial<Record<ElementSymbol, number>>>({}),
|
|
150
|
+
site_radius_overrides = $bindable<SvelteMap<number, number>>(new SvelteMap()),
|
|
151
|
+
atom_color_config = {
|
|
152
|
+
mode: DEFAULTS.structure.atom_color_mode,
|
|
153
|
+
scale: DEFAULTS.structure.atom_color_scale as D3InterpolateName,
|
|
154
|
+
scale_type: DEFAULTS.structure.atom_color_scale_type,
|
|
155
|
+
},
|
|
156
|
+
sym_data = null,
|
|
157
|
+
// Edit-atoms mode callbacks
|
|
158
|
+
on_sites_moved,
|
|
159
|
+
on_operation_start,
|
|
160
|
+
on_add_atom,
|
|
161
|
+
add_atom_mode = $bindable(false),
|
|
162
|
+
add_element = $bindable(`C`),
|
|
163
|
+
cursor = $bindable(`default`),
|
|
164
|
+
dragging_atoms = $bindable(false),
|
|
165
|
+
volumetric_data = undefined,
|
|
166
|
+
isosurface_settings = DEFAULT_ISOSURFACE_SETTINGS,
|
|
167
|
+
}: {
|
|
168
|
+
structure?: AnyStructure
|
|
169
|
+
base_structure?: AnyStructure // The original structure without image atoms, used for property color calculation
|
|
170
|
+
atom_radius?: number // scale factor for atomic radii
|
|
171
|
+
same_size_atoms?: boolean // whether to use the same radius for all atoms. if not, the radius will be
|
|
172
|
+
// determined by the atomic radius of the element
|
|
173
|
+
camera_position?: [x: number, y: number, z: number] // initial camera position from which to render the scene
|
|
174
|
+
camera_target?: Vec3 // external orbit-controls target for pan synchronization
|
|
175
|
+
camera_projection?: CameraProjection // camera projection type
|
|
176
|
+
rotation_damping?: number // rotation damping factor (how quickly the rotation comes to rest after mouse release)
|
|
177
|
+
// zoom level of the camera
|
|
178
|
+
max_zoom?: number
|
|
179
|
+
min_zoom?: number
|
|
180
|
+
rotate_speed?: number // rotation speed. set to 0 to disable rotation.
|
|
181
|
+
zoom_speed?: number // zoom speed. set to 0 to disable zooming.
|
|
182
|
+
pan_speed?: number // pan speed. set to 0 to disable panning.
|
|
183
|
+
zoom_to_cursor?: boolean // zoom toward cursor position instead of scene center
|
|
184
|
+
show_atoms?: boolean
|
|
185
|
+
show_bonds?: ShowBonds
|
|
186
|
+
show_site_labels?: boolean
|
|
187
|
+
show_site_indices?: boolean
|
|
188
|
+
vector_configs?: Record<string, VectorLayerConfig>
|
|
189
|
+
vector_scale?: number
|
|
190
|
+
vector_color?: string
|
|
191
|
+
vector_color_mode?: VectorColorMode
|
|
192
|
+
vector_color_scale?: D3InterpolateName
|
|
193
|
+
vector_normalize?: boolean
|
|
194
|
+
vector_uniform_thickness?: boolean
|
|
195
|
+
vector_origin_gap?: number
|
|
196
|
+
vector_shaft_radius?: number
|
|
197
|
+
vector_arrow_head_radius?: number
|
|
198
|
+
vector_arrow_head_length?: number
|
|
199
|
+
gizmo?: boolean | ComponentProps<typeof extras.Gizmo>
|
|
200
|
+
hovered_idx?: number | null
|
|
201
|
+
hovered_site?: Site | null
|
|
202
|
+
float_fmt?: string
|
|
203
|
+
auto_rotate?: number
|
|
204
|
+
initial_zoom?: number
|
|
205
|
+
bond_thickness?: number
|
|
206
|
+
bond_color?: string
|
|
207
|
+
bonding_strategy?: BondingStrategy
|
|
208
|
+
bonding_options?: Record<string, unknown>
|
|
209
|
+
fov?: number
|
|
210
|
+
ambient_light?: number
|
|
211
|
+
directional_light?: number
|
|
212
|
+
sphere_segments?: number
|
|
213
|
+
lattice_props?: ComponentProps<typeof Lattice>
|
|
214
|
+
atom_label?: Snippet<[{ site: Site; site_idx: number }]>
|
|
215
|
+
site_label_size?: number
|
|
216
|
+
site_label_offset?: Vec3
|
|
217
|
+
site_label_bg_color?: string
|
|
218
|
+
site_label_color?: string
|
|
219
|
+
site_label_padding?: number
|
|
220
|
+
camera_is_moving?: boolean // used to prevent tooltip from showing while camera is moving
|
|
221
|
+
width?: number // Viewer dimensions for responsive zoom
|
|
222
|
+
height?: number
|
|
223
|
+
// measurement props
|
|
224
|
+
measure_mode?: MeasureMode
|
|
225
|
+
selected_sites?: number[]
|
|
226
|
+
measured_sites?: number[]
|
|
227
|
+
added_bonds?: [number, number][]
|
|
228
|
+
removed_bonds?: [number, number][]
|
|
229
|
+
selection_highlight_color?: string
|
|
230
|
+
// Support for active highlight group with different color
|
|
231
|
+
active_sites?: number[]
|
|
232
|
+
active_highlight_color?: string
|
|
233
|
+
rotation?: Vec3 // rotation control prop
|
|
234
|
+
// Expose scene and camera for external use (e.g. export pane)
|
|
235
|
+
scene?: Scene
|
|
236
|
+
camera?: Camera
|
|
237
|
+
orbit_controls?: ComponentProps<typeof extras.OrbitControls>[`ref`] // OrbitControls instance
|
|
238
|
+
rotation_target_ref?: Vec3 // Expose rotation target for reset
|
|
239
|
+
initial_computed_zoom?: number // Expose initial zoom for reset
|
|
240
|
+
hidden_elements?: Set<ElementSymbol>
|
|
241
|
+
hidden_prop_vals?: Set<number | string> // Track hidden property values (e.g. Wyckoff positions, coordination numbers)
|
|
242
|
+
element_radius_overrides?: Partial<Record<ElementSymbol, number>> // Per-element absolute radius in Angstroms
|
|
243
|
+
site_radius_overrides?: Map<number, number> | SvelteMap<number, number> // Per-site absolute radius in Angstroms
|
|
244
|
+
atom_color_config?: Partial<AtomColorConfig> // Atom coloring configuration
|
|
245
|
+
sym_data?: MoyoDataset | null // Symmetry data for Wyckoff coloring
|
|
246
|
+
// Edit-atoms mode callbacks and state
|
|
247
|
+
on_sites_moved?: (scene_indices: number[], delta: Vec3) => void
|
|
248
|
+
on_operation_start?: () => void
|
|
249
|
+
on_add_atom?: (xyz: Vec3, element: ElementSymbol) => void
|
|
250
|
+
add_atom_mode?: boolean // whether user is in click-to-place add-atom sub-mode
|
|
251
|
+
add_element?: ElementSymbol // element to add when clicking in add-atom mode
|
|
252
|
+
cursor?: string // cursor style for the 3D canvas
|
|
253
|
+
dragging_atoms?: boolean // true while TransformControls drag is active (skips expensive recalculations)
|
|
254
|
+
volumetric_data?: VolumetricData // Active volumetric data for isosurface rendering
|
|
255
|
+
isosurface_settings?: IsosurfaceSettings // Isosurface rendering settings
|
|
256
|
+
} = $props()
|
|
257
|
+
|
|
258
|
+
const threlte = useThrelte()
|
|
259
|
+
$effect(() => {
|
|
260
|
+
scene = threlte.scene
|
|
261
|
+
camera = threlte.camera.current
|
|
52
262
|
if (threlte.renderer) {
|
|
53
|
-
|
|
263
|
+
Object.assign(threlte.renderer.domElement, { __renderer: threlte.renderer })
|
|
54
264
|
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// Expose rotation target for external reset
|
|
268
|
+
$effect(() => {
|
|
269
|
+
rotation_target_ref = rotation_target
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Track initial computed zoom for reset
|
|
273
|
+
let stored_initial_zoom = $state<number | undefined>(undefined)
|
|
274
|
+
$effect(() => {
|
|
63
275
|
if (stored_initial_zoom === undefined && computed_zoom > 0) {
|
|
64
|
-
|
|
276
|
+
stored_initial_zoom = computed_zoom
|
|
65
277
|
}
|
|
66
|
-
initial_computed_zoom = stored_initial_zoom
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
let
|
|
70
|
-
let
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
278
|
+
initial_computed_zoom = stored_initial_zoom
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
let bond_pairs: BondPair[] = $state([])
|
|
282
|
+
let active_tooltip = $state<`atom` | `bond` | null>(null)
|
|
283
|
+
let hovered_bond_key = $state<string | null>(null)
|
|
284
|
+
|
|
285
|
+
// Cursor style for the canvas, derived from mode and hover state
|
|
286
|
+
let canvas_cursor = $derived.by(() => {
|
|
287
|
+
if (measure_mode === `edit-atoms` && add_atom_mode) return `crosshair`
|
|
75
288
|
if (hovered_idx != null) {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return `pointer`;
|
|
289
|
+
if (measure_mode === `edit-atoms`) {
|
|
290
|
+
const site = structure?.sites?.[hovered_idx]
|
|
291
|
+
if (site?.properties?.orig_site_idx != null) return `not-allowed`
|
|
292
|
+
return `pointer`
|
|
293
|
+
}
|
|
294
|
+
return `pointer`
|
|
83
295
|
}
|
|
84
|
-
return `default
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
let
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
296
|
+
return `default`
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// Desaturate a color by blending it toward gray (for ghosting image atoms in edit mode)
|
|
300
|
+
const gray = new Color(0x999999)
|
|
301
|
+
function desaturate(hex: string | undefined, amount = 0.4): string {
|
|
302
|
+
return `#${new Color(hex ?? 0x999999).lerp(gray, amount).getHexString()}`
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// === Edit-atoms mode state ===
|
|
306
|
+
let transform_object = $state<Mesh | undefined>(undefined)
|
|
307
|
+
// Plain variable — only used imperatively in TransformControls drag handlers
|
|
308
|
+
let drag_start_centroid: Vec3 | null = null
|
|
309
|
+
// Frozen centroid set on drag start. While non-null, the TransformControls mesh
|
|
310
|
+
// position stays at this fixed value so Svelte's reactive centroid updates (from
|
|
311
|
+
// PBC wrapping) don't fight TransformControls. Cleared on mouseUp so the mesh
|
|
312
|
+
// snaps to the new wrapped centroid.
|
|
313
|
+
let frozen_centroid = $state<Vec3 | null>(null)
|
|
314
|
+
|
|
315
|
+
const get_bond_key = (idx1: number, idx2: number): string =>
|
|
316
|
+
idx1 < idx2 ? `${idx1}-${idx2}` : `${idx2}-${idx1}`
|
|
317
|
+
|
|
318
|
+
// Toggle a bond between two atoms: cycles through add → remove → restore states
|
|
319
|
+
function toggle_bond(site_1: number, site_2: number) {
|
|
320
|
+
const idx_i = Math.min(site_1, site_2)
|
|
321
|
+
const idx_j = Math.max(site_1, site_2)
|
|
107
322
|
// added/removed pairs are stored sorted, so direct comparison works
|
|
108
|
-
const match = ([a, b]) => a === idx_i && b === idx_j
|
|
109
|
-
|
|
323
|
+
const match = ([a, b]: [number, number]) => a === idx_i && b === idx_j
|
|
324
|
+
|
|
325
|
+
const added_idx = added_bonds.findIndex(match)
|
|
110
326
|
if (added_idx >= 0) {
|
|
111
|
-
|
|
112
|
-
|
|
327
|
+
added_bonds = added_bonds.toSpliced(added_idx, 1)
|
|
328
|
+
return
|
|
113
329
|
}
|
|
114
|
-
|
|
330
|
+
|
|
331
|
+
const removed_idx = removed_bonds.findIndex(match)
|
|
115
332
|
if (removed_idx >= 0) {
|
|
116
|
-
|
|
117
|
-
|
|
333
|
+
removed_bonds = removed_bonds.toSpliced(removed_idx, 1)
|
|
334
|
+
return
|
|
118
335
|
}
|
|
336
|
+
|
|
119
337
|
// bond_pairs may not be sorted, so use get_bond_key for comparison
|
|
120
|
-
const key = `${idx_i}-${idx_j}
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
338
|
+
const key = `${idx_i}-${idx_j}`
|
|
339
|
+
if (
|
|
340
|
+
bond_pairs.some((bond) =>
|
|
341
|
+
get_bond_key(bond.site_idx_1, bond.site_idx_2) === key
|
|
342
|
+
)
|
|
343
|
+
) removed_bonds = [...removed_bonds, [idx_i, idx_j]]
|
|
344
|
+
else added_bonds = [...added_bonds, [idx_i, idx_j]]
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Deduplicate clicks: when a highlight sphere and the underlying atom both
|
|
348
|
+
// intercept the same native click, only the first intersection should fire.
|
|
349
|
+
// All threlte intersection events from one click share the same nativeEvent ref.
|
|
350
|
+
let last_native_event: Event | null = null
|
|
351
|
+
|
|
352
|
+
function toggle_selection(site_index: number, evt?: Event) {
|
|
353
|
+
evt?.stopPropagation?.()
|
|
354
|
+
const native_event = (evt as Event & { nativeEvent?: unknown } | undefined)
|
|
355
|
+
?.nativeEvent
|
|
134
356
|
if (native_event instanceof Event) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
last_native_event = native_event;
|
|
357
|
+
if (native_event === last_native_event) return
|
|
358
|
+
last_native_event = native_event
|
|
138
359
|
}
|
|
360
|
+
|
|
139
361
|
if (measure_mode === `edit-bonds`) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
362
|
+
// In edit-bonds mode, select atoms to add/remove bonds between them
|
|
363
|
+
const new_sites = measured_sites.includes(site_index)
|
|
364
|
+
? measured_sites.filter((idx) => idx !== site_index)
|
|
365
|
+
: [...measured_sites, site_index]
|
|
366
|
+
|
|
367
|
+
measured_sites = new_sites
|
|
368
|
+
selected_sites = new_sites
|
|
369
|
+
|
|
370
|
+
// When two atoms are selected, toggle the bond between them
|
|
371
|
+
if (measured_sites.length === 2) {
|
|
372
|
+
toggle_bond(measured_sites[0], measured_sites[1])
|
|
373
|
+
measured_sites = []
|
|
374
|
+
selected_sites = []
|
|
375
|
+
}
|
|
376
|
+
return
|
|
153
377
|
}
|
|
378
|
+
|
|
154
379
|
if (measure_mode === `edit-atoms`) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
380
|
+
// Block image atoms (detected by orig_site_idx property from PBC)
|
|
381
|
+
const site = structure?.sites?.[site_index]
|
|
382
|
+
if (site?.properties?.orig_site_idx != null) return
|
|
383
|
+
|
|
384
|
+
const is_selected = selected_sites.includes(site_index)
|
|
385
|
+
const is_shift = evt instanceof MouseEvent && evt.shiftKey
|
|
386
|
+
|
|
387
|
+
// In edit-atoms mode, selected_sites and measured_sites always stay in sync
|
|
388
|
+
let new_sites: number[]
|
|
389
|
+
if (is_shift) {
|
|
390
|
+
// Multi-select: toggle this site in/out of selection
|
|
391
|
+
new_sites = is_selected
|
|
392
|
+
? selected_sites.filter((idx) => idx !== site_index)
|
|
393
|
+
: [...selected_sites, site_index]
|
|
394
|
+
} else {
|
|
395
|
+
// Single-select: replace selection (or deselect if already selected)
|
|
396
|
+
new_sites = is_selected ? [] : [site_index]
|
|
397
|
+
}
|
|
398
|
+
selected_sites = new_sites
|
|
399
|
+
measured_sites = new_sites
|
|
400
|
+
return
|
|
176
401
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
!measured_sites.includes(site_index) &&
|
|
405
|
+
measured_sites.length >= measure.MAX_SELECTED_SITES
|
|
406
|
+
) {
|
|
407
|
+
console.warn(
|
|
408
|
+
`Selection size limit reached (${measure.MAX_SELECTED_SITES}). Deselect some sites first.`,
|
|
409
|
+
)
|
|
410
|
+
return
|
|
181
411
|
}
|
|
412
|
+
|
|
182
413
|
measured_sites = measured_sites.includes(site_index)
|
|
183
|
-
|
|
184
|
-
|
|
414
|
+
? measured_sites.filter((idx) => idx !== site_index)
|
|
415
|
+
: [...measured_sites, site_index]
|
|
185
416
|
selected_sites = selected_sites.includes(site_index)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
}
|
|
189
|
-
$effect(() => {
|
|
190
|
-
const count = structure?.sites?.length ?? 0
|
|
417
|
+
? selected_sites.filter((idx) => idx !== site_index)
|
|
418
|
+
: [...selected_sites, site_index]
|
|
419
|
+
}
|
|
420
|
+
$effect(() => {
|
|
421
|
+
const count = structure?.sites?.length ?? 0
|
|
191
422
|
if (count <= 0) {
|
|
192
|
-
|
|
193
|
-
|
|
423
|
+
measured_sites = []
|
|
424
|
+
return
|
|
194
425
|
}
|
|
195
426
|
untrack(() => {
|
|
196
|
-
|
|
197
|
-
})
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
let
|
|
209
|
-
?
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
let
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
427
|
+
measured_sites = measured_sites.filter((idx) => idx >= 0 && idx < count)
|
|
428
|
+
})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
$effect(() => {
|
|
432
|
+
cursor = canvas_cursor
|
|
433
|
+
})
|
|
434
|
+
|
|
435
|
+
extras.interactivity()
|
|
436
|
+
$effect.pre(() => {
|
|
437
|
+
hovered_site = structure?.sites?.[hovered_idx ?? -1] ?? null
|
|
438
|
+
})
|
|
439
|
+
let lattice = $derived(
|
|
440
|
+
structure && `lattice` in structure ? structure.lattice : null,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
let visual_lattice = $derived(
|
|
444
|
+
base_structure && `lattice` in base_structure ? base_structure.lattice : lattice,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
let rotation_target = $derived(
|
|
448
|
+
lattice
|
|
449
|
+
? (math.scale(math.add(...lattice.matrix), 0.5) as Vec3)
|
|
450
|
+
: structure
|
|
451
|
+
? get_center_of_mass(structure)
|
|
452
|
+
: [0, 0, 0] as Vec3,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
let structure_size = $derived(
|
|
456
|
+
lattice ? (lattice.a + lattice.b + lattice.c) / 2 : 10,
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
// Characteristic inter-atomic spacing: cube root of volume per atom.
|
|
460
|
+
// Excludes PBC image atoms (orig_site_idx) so toggling image atoms doesn't affect arrow sizing.
|
|
461
|
+
let char_atom_spacing = $derived.by(() => {
|
|
462
|
+
if (!lattice || !structure?.sites?.length) return structure_size
|
|
463
|
+
const n_real = structure.sites.filter((site) =>
|
|
464
|
+
site.properties?.orig_site_idx == null
|
|
465
|
+
).length
|
|
466
|
+
return n_real > 0 ? Math.cbrt(lattice.volume / n_real) : structure_size
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// When uniform thickness is on, convert negative (length-relative) radii to
|
|
470
|
+
// positive (absolute) values scaled by inter-atomic spacing.
|
|
471
|
+
// Already-positive (absolute) values are preserved as-is.
|
|
472
|
+
let eff_shaft_radius = $derived(
|
|
473
|
+
vector_uniform_thickness && vector_shaft_radius < 0
|
|
474
|
+
? char_atom_spacing * -vector_shaft_radius
|
|
475
|
+
: vector_shaft_radius,
|
|
476
|
+
)
|
|
477
|
+
let eff_head_radius = $derived(
|
|
478
|
+
vector_uniform_thickness && vector_arrow_head_radius < 0
|
|
479
|
+
? char_atom_spacing * -vector_arrow_head_radius
|
|
480
|
+
: vector_arrow_head_radius,
|
|
481
|
+
)
|
|
482
|
+
let eff_head_length = $derived(
|
|
483
|
+
vector_uniform_thickness && vector_arrow_head_length < 0
|
|
484
|
+
? char_atom_spacing * -vector_arrow_head_length
|
|
485
|
+
: vector_arrow_head_length,
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
// Compute dynamic camera clipping planes based on structure size
|
|
489
|
+
// This prevents z-fighting and disappearing objects when zooming in close on large supercells
|
|
490
|
+
let camera_near = $derived(Math.max(0.01, structure_size * 0.01))
|
|
491
|
+
let camera_far = $derived(Math.max(1000, structure_size * 100))
|
|
492
|
+
|
|
493
|
+
// Using $state because this is mutated in an effect based on viewport/structure size
|
|
494
|
+
let computed_zoom = $state(untrack(() => initial_zoom))
|
|
495
|
+
$effect(() => {
|
|
496
|
+
if (!(width > 0) || !(height > 0)) return
|
|
497
|
+
const structure_max_dim = Math.max(1, untrack(() => structure_size))
|
|
498
|
+
const viewer_min_dim = Math.min(width, height)
|
|
499
|
+
const scale_factor = viewer_min_dim / (structure_max_dim * 50) // 50px per unit
|
|
500
|
+
let new_zoom = initial_zoom * scale_factor
|
|
501
|
+
if (min_zoom && min_zoom > 0) new_zoom = Math.max(min_zoom, new_zoom)
|
|
502
|
+
if (max_zoom && max_zoom > 0) new_zoom = Math.min(max_zoom, new_zoom)
|
|
503
|
+
computed_zoom = new_zoom
|
|
504
|
+
})
|
|
505
|
+
|
|
506
|
+
$effect.pre(() => { // Simple initial camera auto-position: proportional to structure size and fov
|
|
234
507
|
if (camera_position.every((val) => val === 0) && structure) {
|
|
235
|
-
|
|
236
|
-
|
|
508
|
+
stored_initial_zoom = undefined
|
|
509
|
+
const distance = Math.max(1, structure_size) * (60 / fov)
|
|
510
|
+
camera_position = [distance, distance * 0.3, distance * 0.8]
|
|
237
511
|
}
|
|
238
|
-
})
|
|
239
|
-
$effect(() => {
|
|
512
|
+
})
|
|
513
|
+
$effect(() => {
|
|
240
514
|
if (structure && show_bonds !== `never`) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
515
|
+
// Determine if we should show bonds based on the setting and structure type
|
|
516
|
+
const should_show_bonds = show_bonds === `always` ||
|
|
517
|
+
(show_bonds === `crystals` && lattice) ||
|
|
518
|
+
(show_bonds === `molecules` && !lattice)
|
|
519
|
+
|
|
520
|
+
if (should_show_bonds) {
|
|
521
|
+
bond_pairs = BONDING_STRATEGIES[bonding_strategy](structure, bonding_options)
|
|
522
|
+
} else bond_pairs = []
|
|
523
|
+
} else bond_pairs = []
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
// Compute property-based colors when not using element coloring
|
|
527
|
+
// Use base_structure (original unit cell) for color calculation
|
|
528
|
+
let property_colors = $derived(
|
|
529
|
+
get_property_colors(
|
|
530
|
+
base_structure || structure,
|
|
531
|
+
atom_color_config,
|
|
532
|
+
bonding_strategy,
|
|
533
|
+
sym_data,
|
|
534
|
+
),
|
|
535
|
+
)
|
|
536
|
+
// Compute weighted average radius for a site based on species occupancies
|
|
537
|
+
// Normalizes by total occupancy so vacancy-containing sites render at full size
|
|
538
|
+
const calc_weighted_radius = (site: Site): number => {
|
|
539
|
+
const total_occu = site.species.reduce((sum, { occu }) => sum + occu, 0)
|
|
261
540
|
const weighted_sum = site.species.reduce((sum, { element, occu }) => {
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}, 0)
|
|
265
|
-
return total_occu > 0 ? weighted_sum / total_occu : 1
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const render_sites = merge_split_partial_sites(structure.sites, hidden_elements)
|
|
541
|
+
const override = element_radius_overrides?.[element as ElementSymbol]
|
|
542
|
+
return sum + occu * (override ?? atomic_radii[element] ?? 1)
|
|
543
|
+
}, 0)
|
|
544
|
+
return total_occu > 0 ? weighted_sum / total_occu : 1
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
let atom_data = $derived.by(() => {
|
|
548
|
+
if (!show_atoms || !structure?.sites) return []
|
|
549
|
+
const render_sites = merge_split_partial_sites(structure.sites, hidden_elements)
|
|
271
550
|
return render_sites.flatMap(({ site_idx, site, is_image_atom }) => {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
551
|
+
const orig_idx = get_orig_site_idx(site, site_idx)
|
|
552
|
+
|
|
553
|
+
// Skip sites with hidden property values
|
|
554
|
+
const prop_val = property_colors?.values[orig_idx]
|
|
555
|
+
if (prop_val !== undefined && hidden_prop_vals.has(prop_val)) return []
|
|
556
|
+
|
|
557
|
+
// Calculate radius: same_size > site override > element override > default
|
|
558
|
+
// All radii scale uniformly with atom_radius for consistent slider behavior
|
|
559
|
+
const base_radius = same_size_atoms
|
|
560
|
+
? 1
|
|
561
|
+
: site_radius_overrides?.get(site_idx) ?? calc_weighted_radius(site)
|
|
562
|
+
const radius = base_radius * atom_radius
|
|
563
|
+
|
|
564
|
+
// Use property color if available (e.g. coordination number, Wyckoff position)
|
|
565
|
+
// Otherwise, each species gets its own element color (important for disordered sites)
|
|
566
|
+
const site_property_color = property_colors?.colors[orig_idx]
|
|
567
|
+
|
|
568
|
+
const visible_species = site.species.filter(({ element }) =>
|
|
569
|
+
!hidden_elements.has(element)
|
|
570
|
+
)
|
|
571
|
+
const slice_geometry = compute_slice_geometry(visible_species)
|
|
572
|
+
return slice_geometry.map((slice_data) => {
|
|
573
|
+
return {
|
|
574
|
+
site_idx,
|
|
575
|
+
element: slice_data.element,
|
|
576
|
+
occupancy: slice_data.occupancy,
|
|
577
|
+
position: site.xyz,
|
|
578
|
+
radius,
|
|
579
|
+
color: site_property_color ?? colors.element?.[slice_data.element],
|
|
580
|
+
has_partial_occupancy: slice_data.occupancy < 1,
|
|
581
|
+
start_phi: slice_data.start_phi,
|
|
582
|
+
end_phi: slice_data.end_phi,
|
|
583
|
+
phi_length: slice_data.phi_length,
|
|
584
|
+
render_start_cap: slice_data.render_start_cap,
|
|
585
|
+
render_end_cap: slice_data.render_end_cap,
|
|
586
|
+
is_image_atom,
|
|
587
|
+
}
|
|
588
|
+
})
|
|
589
|
+
})
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
// Shared visibility check: site has at least one non-hidden element and
|
|
593
|
+
// its property value (if any) isn't hidden. Used by both bond and vector filtering.
|
|
594
|
+
const is_site_visible = (site_idx: number): boolean => {
|
|
595
|
+
if (!structure?.sites) return false
|
|
596
|
+
const site = structure.sites[site_idx]
|
|
597
|
+
const has_visible_element = site?.species.some(({ element }) =>
|
|
598
|
+
!hidden_elements.has(element)
|
|
599
|
+
)
|
|
600
|
+
const orig_idx = get_orig_site_idx(site, site_idx)
|
|
601
|
+
const prop_val = property_colors?.values[orig_idx]
|
|
602
|
+
const prop_visible = prop_val === undefined ||
|
|
603
|
+
!hidden_prop_vals.has(prop_val)
|
|
604
|
+
return has_visible_element && prop_visible
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
let filtered_bond_pairs = $derived.by(() => {
|
|
608
|
+
if (!structure?.sites) return bond_pairs
|
|
609
|
+
|
|
319
610
|
// Build set of removed bond keys for efficient lookup
|
|
320
|
-
const removed_keys = new Set(
|
|
611
|
+
const removed_keys = new Set(
|
|
612
|
+
removed_bonds.map(([idx_i, idx_j]) => get_bond_key(idx_i, idx_j)),
|
|
613
|
+
)
|
|
614
|
+
|
|
321
615
|
// Filter calculated bonds: exclude removed and hidden
|
|
322
616
|
const calculated = bond_pairs.filter(({ site_idx_1, site_idx_2 }) => {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
617
|
+
if (removed_keys.has(get_bond_key(site_idx_1, site_idx_2))) return false
|
|
618
|
+
return is_site_visible(site_idx_1) && is_site_visible(site_idx_2)
|
|
619
|
+
})
|
|
620
|
+
|
|
327
621
|
// Create BondPair objects for manually added bonds
|
|
328
|
-
const added = added_bonds
|
|
329
|
-
|
|
330
|
-
if (!is_site_visible(idx_i) || !is_site_visible(idx_j))
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
const
|
|
338
|
-
|
|
622
|
+
const added: BondPair[] = added_bonds
|
|
623
|
+
.map(([idx_i, idx_j]) => {
|
|
624
|
+
if (!is_site_visible(idx_i) || !is_site_visible(idx_j)) return null
|
|
625
|
+
const site1 = structure.sites[idx_i]
|
|
626
|
+
const site2 = structure.sites[idx_j]
|
|
627
|
+
if (!site1 || !site2) return null
|
|
628
|
+
|
|
629
|
+
const pos_1 = site1.xyz
|
|
630
|
+
const pos_2 = site2.xyz
|
|
631
|
+
const dist = math.euclidean_dist(pos_1, pos_2)
|
|
632
|
+
|
|
339
633
|
return {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
634
|
+
pos_1,
|
|
635
|
+
pos_2,
|
|
636
|
+
site_idx_1: idx_i,
|
|
637
|
+
site_idx_2: idx_j,
|
|
638
|
+
bond_length: dist,
|
|
639
|
+
strength: 1.0,
|
|
640
|
+
transform_matrix: compute_bond_transform(pos_1, pos_2),
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
.filter((bond): bond is BondPair => bond !== null)
|
|
644
|
+
|
|
645
|
+
return [...calculated, ...added]
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
let instanced_bond_groups = $derived.by(() => {
|
|
649
|
+
if (!structure?.sites || filtered_bond_pairs.length === 0) return []
|
|
650
|
+
|
|
355
651
|
const group = {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
652
|
+
thickness: bond_thickness,
|
|
653
|
+
ambient_light,
|
|
654
|
+
directional_light,
|
|
655
|
+
instances: [] as {
|
|
656
|
+
matrix: Float32Array
|
|
657
|
+
color_start: string
|
|
658
|
+
color_end: string
|
|
659
|
+
}[],
|
|
660
|
+
}
|
|
661
|
+
|
|
361
662
|
for (const bond_data of filtered_bond_pairs) {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
663
|
+
const site_a = structure.sites[bond_data.site_idx_1]
|
|
664
|
+
const site_b = structure.sites[bond_data.site_idx_2]
|
|
665
|
+
|
|
666
|
+
const get_majority_color = (site: typeof site_a) => {
|
|
667
|
+
if (!site?.species || site.species.length === 0) return bond_color
|
|
668
|
+
const majority_species = site.species.reduce((max, spec) =>
|
|
669
|
+
spec.occu > max.occu ? spec : max
|
|
670
|
+
)
|
|
671
|
+
return colors.element?.[majority_species.element] || bond_color
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const color_start = get_majority_color(site_a)
|
|
675
|
+
const color_end = get_majority_color(site_b)
|
|
676
|
+
const instance = { matrix: bond_data.transform_matrix, color_start, color_end }
|
|
677
|
+
group.instances.push(instance)
|
|
374
678
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
679
|
+
|
|
680
|
+
return group.instances.length > 0 ? [group] : []
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
let radius_by_site_idx = $derived.by(() => {
|
|
684
|
+
const map = new SvelteMap<number, number>()
|
|
379
685
|
for (const atom of atom_data) {
|
|
380
|
-
|
|
381
|
-
map.set(atom.site_idx, atom.radius);
|
|
686
|
+
if (!map.has(atom.site_idx)) map.set(atom.site_idx, atom.radius)
|
|
382
687
|
}
|
|
383
|
-
return map
|
|
384
|
-
})
|
|
385
|
-
|
|
386
|
-
//
|
|
387
|
-
|
|
688
|
+
return map
|
|
689
|
+
})
|
|
690
|
+
|
|
691
|
+
// Get radius for a site (for highlight fallback when site is hidden/filtered)
|
|
692
|
+
// Checks site_radius_overrides first for consistency with visible atoms
|
|
693
|
+
const get_site_radius = (site: Site, site_idx: number | null): number => {
|
|
388
694
|
const override = site_idx !== null
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site)
|
|
392
|
-
return base_radius * atom_radius
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
const
|
|
400
|
-
const
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
695
|
+
? site_radius_overrides?.get(site_idx)
|
|
696
|
+
: undefined
|
|
697
|
+
const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site)
|
|
698
|
+
return base_radius * atom_radius
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Interpolate between spin-down (#3498db blue) and spin-up (#e74c3c red)
|
|
702
|
+
// based on the z-component direction of a magnetic vector
|
|
703
|
+
function spin_direction_color(vec: Vec3): string {
|
|
704
|
+
const mag = Math.hypot(...vec)
|
|
705
|
+
const z_frac = mag > 1e-10 ? (vec[2] / mag + 1) / 2 : 0.5 // 0=down, 1=up
|
|
706
|
+
const red = Math.round(52 + (231 - 52) * z_frac)
|
|
707
|
+
const grn = Math.round(152 + (76 - 152) * z_frac)
|
|
708
|
+
const blu = Math.round(219 + (60 - 219) * z_frac)
|
|
709
|
+
return `#${red.toString(16).padStart(2, `0`)}${
|
|
710
|
+
grn.toString(16).padStart(2, `0`)
|
|
711
|
+
}${blu.toString(16).padStart(2, `0`)}`
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Build one arrow layer per visible vector key. Auto-scales the longest
|
|
715
|
+
// vector to 1.8× char_atom_spacing (cube root of volume per atom).
|
|
716
|
+
// When vector_normalize is on, effective_max is 1 so all arrows get equal length.
|
|
717
|
+
// Single active key preserves legacy coloring (element for force,
|
|
718
|
+
// spin-direction for magmom/spin). Multiple keys use flat palette colors.
|
|
719
|
+
let vector_layers = $derived.by(() => {
|
|
720
|
+
if (!structure?.sites) return []
|
|
721
|
+
const keys = get_structure_vector_keys(structure)
|
|
722
|
+
const active_keys = keys.filter((key) => vector_configs[key]?.visible !== false)
|
|
723
|
+
if (active_keys.length === 0) return []
|
|
724
|
+
|
|
725
|
+
// Build per-site lookup; skip hidden sites so they don't contribute
|
|
726
|
+
// arrows or affect autoscaling. null entries = hidden site.
|
|
727
|
+
const active_set = new Set(active_keys)
|
|
728
|
+
let max_mag = 0
|
|
729
|
+
const site_vec_maps = structure.sites.map((site, site_idx) => {
|
|
730
|
+
if (!is_site_visible(site_idx)) return null
|
|
731
|
+
const map = new SvelteMap<string, Vec3>()
|
|
732
|
+
for (const { key, vec } of get_all_site_vectors(site)) {
|
|
733
|
+
map.set(key, vec)
|
|
734
|
+
if (active_set.has(key)) {
|
|
735
|
+
max_mag = Math.max(max_mag, Math.hypot(...vec))
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return map
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
// When normalize is on, treat all magnitudes as 1 so arrows have equal length
|
|
742
|
+
const effective_max = vector_normalize ? 1 : max_mag
|
|
743
|
+
const auto_scale = effective_max > 1e-10
|
|
744
|
+
? (char_atom_spacing * 1.8) / effective_max
|
|
745
|
+
: 1
|
|
746
|
+
const is_single = active_keys.length === 1
|
|
747
|
+
const effective_global_scale = auto_scale * vector_scale
|
|
748
|
+
|
|
749
|
+
// When vector_origin_gap > 0 and multiple vectors exist at a site,
|
|
750
|
+
// arrange arrow origins on a regular polygon centered on the atom, in a
|
|
751
|
+
// plane perpendicular to the mean vector direction. The gap is a fraction
|
|
752
|
+
// of the visual atom radius (0 = center, 0.5 = halfway to surface).
|
|
753
|
+
// get_site_radius() returns the uniform scale applied to SphereGeometry(0.5),
|
|
754
|
+
// so visual_radius = get_site_radius() * 0.5.
|
|
755
|
+
const site_offsets = (vector_origin_gap > 0 && !is_single)
|
|
756
|
+
? structure.sites.map((site, site_idx) => {
|
|
757
|
+
const vec_map = site_vec_maps[site_idx]
|
|
758
|
+
if (!vec_map) return null
|
|
759
|
+
const site_keys = active_keys.filter((key) => vec_map.has(key))
|
|
760
|
+
const n_keys = site_keys.length
|
|
761
|
+
if (n_keys <= 1) return null
|
|
762
|
+
const visual_radius = get_site_radius(site, site_idx) * 0.5
|
|
763
|
+
const gap_abs = vector_origin_gap * visual_radius
|
|
764
|
+
let mean: Vec3 = [0, 0, 0]
|
|
765
|
+
for (const key of site_keys) {
|
|
766
|
+
const vec = vec_map.get(key)
|
|
767
|
+
if (vec) mean = math.add(mean, math.normalize_vec3(vec)) as Vec3
|
|
417
768
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
769
|
+
const mean_dir = math.normalize_vec3(mean, [0, 1, 0] as Vec3)
|
|
770
|
+
const [u_vec, v_vec] = math.compute_in_plane_basis(mean_dir)
|
|
771
|
+
const offsets = new SvelteMap<string, Vec3>()
|
|
772
|
+
for (const [idx, key] of site_keys.entries()) {
|
|
773
|
+
const angle = (2 * Math.PI * idx) / n_keys
|
|
774
|
+
const dx = math.scale(u_vec, gap_abs * Math.cos(angle)) as Vec3
|
|
775
|
+
const dy = math.scale(v_vec, gap_abs * Math.sin(angle)) as Vec3
|
|
776
|
+
offsets.set(key, math.add(dx, dy) as Vec3)
|
|
425
777
|
}
|
|
426
|
-
return
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
778
|
+
return offsets
|
|
779
|
+
})
|
|
780
|
+
: null
|
|
781
|
+
|
|
782
|
+
const mag_interpolator = get_d3_interpolator(vector_color_scale)
|
|
783
|
+
|
|
784
|
+
return active_keys.map((key, layer_idx) => {
|
|
785
|
+
const layer_cfg = vector_configs[key]
|
|
786
|
+
const layer_scale = effective_global_scale * (layer_cfg?.scale ?? 1.0)
|
|
787
|
+
const layer_color = layer_cfg?.color ??
|
|
788
|
+
VECTOR_PALETTE[layer_idx % VECTOR_PALETTE.length]
|
|
789
|
+
|
|
790
|
+
const arrows = structure.sites
|
|
791
|
+
.map((site, site_idx) => {
|
|
792
|
+
const vec_map = site_vec_maps[site_idx]
|
|
793
|
+
if (!vec_map) return null
|
|
794
|
+
const vec = vec_map.get(key)
|
|
795
|
+
if (!vec) return null
|
|
796
|
+
|
|
797
|
+
// Resolve color mode: explicit per-key color always wins,
|
|
798
|
+
// then multi-key uses palette, then mode-based coloring
|
|
799
|
+
let arrow_color: string
|
|
800
|
+
if (layer_cfg?.color) {
|
|
801
|
+
arrow_color = layer_cfg.color
|
|
802
|
+
} else if (!is_single) arrow_color = layer_color
|
|
803
|
+
else {
|
|
804
|
+
const effective_mode = vector_color_mode === `auto`
|
|
805
|
+
? (key.startsWith(`magmom`) || key.startsWith(`spin`)
|
|
806
|
+
? `spin_direction`
|
|
807
|
+
: `element`)
|
|
808
|
+
: vector_color_mode
|
|
809
|
+
if (effective_mode === `magnitude`) {
|
|
810
|
+
const mag = Math.hypot(...vec)
|
|
811
|
+
const norm = max_mag > 1e-10 ? mag / max_mag : 0
|
|
812
|
+
arrow_color = mag_interpolator(norm)
|
|
813
|
+
} else if (effective_mode === `spin_direction`) {
|
|
814
|
+
arrow_color = spin_direction_color(vec)
|
|
815
|
+
} else if (effective_mode === `uniform`) {
|
|
816
|
+
arrow_color = vector_color
|
|
817
|
+
} else {
|
|
818
|
+
const majority_element = site.species.length > 0
|
|
819
|
+
? site.species.reduce((max, spec) =>
|
|
820
|
+
spec.occu > max.occu ? spec : max
|
|
821
|
+
).element
|
|
822
|
+
: undefined
|
|
823
|
+
arrow_color =
|
|
824
|
+
(majority_element && colors.element?.[majority_element]) ||
|
|
825
|
+
vector_color
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const offset = site_offsets?.[site_idx]?.get(key)
|
|
830
|
+
const position = offset ? math.add(site.xyz, offset) as Vec3 : site.xyz
|
|
831
|
+
const arrow_vec = vector_normalize ? math.normalize_vec3(vec) : vec
|
|
832
|
+
|
|
833
|
+
return {
|
|
834
|
+
site_idx,
|
|
835
|
+
position,
|
|
836
|
+
vector: arrow_vec,
|
|
837
|
+
scale: layer_scale,
|
|
430
838
|
color: arrow_color,
|
|
431
|
-
|
|
839
|
+
}
|
|
840
|
+
})
|
|
841
|
+
.filter((item): item is NonNullable<typeof item> => item !== null)
|
|
842
|
+
|
|
843
|
+
return { key, arrows }
|
|
432
844
|
})
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
let instanced_atom_groups = $derived(
|
|
436
|
-
.
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
},
|
|
452
|
-
|
|
453
|
-
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
let instanced_atom_groups = $derived(
|
|
848
|
+
Object.values(
|
|
849
|
+
atom_data
|
|
850
|
+
.filter((atom) => !atom.has_partial_occupancy)
|
|
851
|
+
.reduce(
|
|
852
|
+
(groups, atom) => {
|
|
853
|
+
const { element, radius, color, is_image_atom } = atom
|
|
854
|
+
// Separate image atoms into their own groups for distinct styling in edit-atoms mode
|
|
855
|
+
const key = `${element}-${format_num(radius, `.3~`)}-${color}-${
|
|
856
|
+
is_image_atom ? `img` : `base`
|
|
857
|
+
}`
|
|
858
|
+
const bucket = groups[key] ||
|
|
859
|
+
(groups[key] = { element, radius, color, is_image_atom, atoms: [] })
|
|
860
|
+
bucket.atoms.push(atom)
|
|
861
|
+
return groups
|
|
862
|
+
},
|
|
863
|
+
{} as Record<string, InstancedAtomGroup>,
|
|
864
|
+
),
|
|
865
|
+
),
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
let unique_instanced_atoms = $derived(
|
|
869
|
+
Object.values(
|
|
870
|
+
instanced_atom_groups
|
|
871
|
+
.flatMap((group) => group.atoms)
|
|
872
|
+
.reduce((acc, atom) => {
|
|
873
|
+
acc[atom.site_idx] = atom
|
|
874
|
+
return acc
|
|
875
|
+
}, {} as Record<number, (typeof atom_data)[number]>),
|
|
876
|
+
),
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
let gizmo_props = $derived.by(() => {
|
|
880
|
+
const axis_options = Object.fromEntries(
|
|
881
|
+
[...AXIS_COLORS, ...NEG_AXIS_COLORS].map(([axis, color, hover_color]) => [
|
|
454
882
|
axis,
|
|
455
883
|
{
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
884
|
+
color,
|
|
885
|
+
labelColor: `#111`,
|
|
886
|
+
opacity: axis.startsWith(`n`) ? 0.9 : 0.8,
|
|
887
|
+
hover: {
|
|
888
|
+
color: hover_color,
|
|
889
|
+
labelColor: `#222222`,
|
|
890
|
+
opacity: axis.startsWith(`n`) ? 1 : 0.9,
|
|
891
|
+
},
|
|
464
892
|
},
|
|
465
|
-
|
|
893
|
+
]),
|
|
894
|
+
)
|
|
466
895
|
return {
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
})
|
|
474
|
-
|
|
896
|
+
background: { enabled: false },
|
|
897
|
+
className: `responsive-gizmo`,
|
|
898
|
+
...axis_options,
|
|
899
|
+
...(typeof gizmo === `boolean` ? {} : gizmo),
|
|
900
|
+
offset: { left: 5, bottom: 5 },
|
|
901
|
+
}
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
let orbit_controls_props = $derived({
|
|
475
905
|
position: [0, 0, 0],
|
|
476
906
|
enableRotate: rotate_speed > 0,
|
|
477
907
|
rotateSpeed: rotate_speed,
|
|
@@ -488,20 +918,20 @@ let orbit_controls_props = $derived({
|
|
|
488
918
|
enableDamping: Boolean(rotation_damping),
|
|
489
919
|
dampingFactor: rotation_damping,
|
|
490
920
|
onstart: () => {
|
|
491
|
-
|
|
492
|
-
|
|
921
|
+
camera_is_moving = true
|
|
922
|
+
hovered_idx = null
|
|
493
923
|
},
|
|
494
924
|
onend: () => {
|
|
495
|
-
|
|
925
|
+
camera_is_moving = false
|
|
496
926
|
},
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
const root_styles = getComputedStyle(document.documentElement)
|
|
502
|
-
const text_color = root_styles.getPropertyValue(`--text-color`).trim()
|
|
503
|
-
return text_color || `#808080
|
|
504
|
-
})
|
|
927
|
+
})
|
|
928
|
+
|
|
929
|
+
let measure_line_color = $derived.by(() => {
|
|
930
|
+
if (typeof window === `undefined`) return
|
|
931
|
+
const root_styles = getComputedStyle(document.documentElement)
|
|
932
|
+
const text_color = root_styles.getPropertyValue(`--text-color`).trim()
|
|
933
|
+
return text_color || `#808080`
|
|
934
|
+
})
|
|
505
935
|
</script>
|
|
506
936
|
|
|
507
937
|
{#snippet bond_instanced_mesh_snippet(
|
|
@@ -530,11 +960,11 @@ let measure_line_color = $derived.by(() => {
|
|
|
530
960
|
{#if site.species.length === 1}
|
|
531
961
|
{site.species[0].element}{#if show_site_indices}-{site_idx + 1}{/if}
|
|
532
962
|
{:else}
|
|
533
|
-
{@html site.species.map((spec) =>
|
|
963
|
+
{@html sanitize_html(site.species.map((spec) =>
|
|
534
964
|
`${spec.element}<sub>${
|
|
535
965
|
format_num(spec.occu, `.3~`).replace(`0.`, `.`)
|
|
536
966
|
}</sub>`
|
|
537
|
-
).join(``)}{#if show_site_indices}-{
|
|
967
|
+
).join(``))}{#if show_site_indices}-{
|
|
538
968
|
site_idx + 1
|
|
539
969
|
}{/if}
|
|
540
970
|
{/if}
|
|
@@ -588,6 +1018,7 @@ let measure_line_color = $derived.by(() => {
|
|
|
588
1018
|
{@const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom}
|
|
589
1019
|
<extras.InstancedMesh
|
|
590
1020
|
key="{element}-{format_num(radius, `.3~`)}-{color}-{is_image_atom ? `img` : `base`}-{edit_mode_image}"
|
|
1021
|
+
limit={atoms.length}
|
|
591
1022
|
range={atoms.length}
|
|
592
1023
|
frustumCulled={false}
|
|
593
1024
|
>
|
|
@@ -718,11 +1149,16 @@ let measure_line_color = $derived.by(() => {
|
|
|
718
1149
|
{/if}
|
|
719
1150
|
{/if}
|
|
720
1151
|
|
|
721
|
-
{#
|
|
722
|
-
{#each
|
|
723
|
-
<Arrow
|
|
1152
|
+
{#each vector_layers as layer (layer.key)}
|
|
1153
|
+
{#each layer.arrows as arrow (`${layer.key}-${arrow.site_idx}`)}
|
|
1154
|
+
<Arrow
|
|
1155
|
+
{...arrow}
|
|
1156
|
+
shaft_radius={eff_shaft_radius}
|
|
1157
|
+
arrow_head_radius={eff_head_radius}
|
|
1158
|
+
arrow_head_length={eff_head_length}
|
|
1159
|
+
/>
|
|
724
1160
|
{/each}
|
|
725
|
-
{/
|
|
1161
|
+
{/each}
|
|
726
1162
|
|
|
727
1163
|
<!-- Instanced bond rendering with gradient colors -->
|
|
728
1164
|
{#if instanced_bond_groups.length > 0}
|
|
@@ -891,7 +1327,7 @@ let measure_line_color = $derived.by(() => {
|
|
|
891
1327
|
{#if occu !== 1}<span class="occupancy">{
|
|
892
1328
|
format_num(occu, `.3~f`)
|
|
893
1329
|
}</span>{/if}
|
|
894
|
-
<strong>{element}{@html oxi_str}</strong>
|
|
1330
|
+
<strong>{element}{@html sanitize_html(oxi_str)}</strong>
|
|
895
1331
|
{#if element_name}<span class="elem-name">{element_name}</span>{/if}
|
|
896
1332
|
{/each}
|
|
897
1333
|
</div>
|