matterviz 0.3.2 → 0.3.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/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/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,911 +1,1176 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import
|
|
25
|
-
import
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { ColorSchemeName } from '../colors'
|
|
3
|
+
import { ELEMENT_COLOR_SCHEMES } from '../colors'
|
|
4
|
+
import type { ShowControlsProp } from '../controls'
|
|
5
|
+
import { normalize_show_controls } from '../controls'
|
|
6
|
+
import type { ElementSymbol } from '../element'
|
|
7
|
+
import { StatusMessage } from '../feedback'
|
|
8
|
+
import Spinner from '../feedback/Spinner.svelte'
|
|
9
|
+
import Icon from '../Icon.svelte'
|
|
10
|
+
import { create_file_drop_handler, load_from_url } from '../io'
|
|
11
|
+
import { parse_volumetric_file } from '../isosurface/parse'
|
|
12
|
+
import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
|
|
13
|
+
import {
|
|
14
|
+
auto_isosurface_settings,
|
|
15
|
+
DEFAULT_ISOSURFACE_SETTINGS,
|
|
16
|
+
tile_volumetric_data,
|
|
17
|
+
} from '../isosurface/types'
|
|
18
|
+
import { ELEM_SYMBOLS } from '../labels'
|
|
19
|
+
import { set_fullscreen_bg, toggle_fullscreen } from '../layout'
|
|
20
|
+
import type { Vec3 } from '../math'
|
|
21
|
+
import { create_cart_to_frac, create_frac_to_cart } from '../math'
|
|
22
|
+
import { DEFAULTS } from '../settings'
|
|
23
|
+
import { sanitize_html } from '../sanitize'
|
|
24
|
+
import { colors } from '../state.svelte'
|
|
25
|
+
import type { AnyStructure, Crystal, MeasureMode } from './'
|
|
26
|
+
import {
|
|
27
|
+
default_vector_configs,
|
|
28
|
+
get_element_counts,
|
|
29
|
+
get_pbc_image_sites,
|
|
30
|
+
get_structure_vector_keys,
|
|
31
|
+
} from './'
|
|
32
|
+
import { wrap_to_unit_cell } from './pbc'
|
|
33
|
+
import {
|
|
34
|
+
is_valid_supercell_input,
|
|
35
|
+
make_supercell,
|
|
36
|
+
parse_supercell_scaling,
|
|
37
|
+
} from './supercell'
|
|
38
|
+
import type { CellType, SymmetrySettings } from '../symmetry'
|
|
39
|
+
import * as symmetry from '../symmetry'
|
|
40
|
+
import { transform_cell } from '../symmetry'
|
|
41
|
+
import type { MoyoDataset } from '@spglib/moyo-wasm'
|
|
42
|
+
import { Canvas } from '@threlte/core'
|
|
43
|
+
import type { ComponentProps, Snippet } from 'svelte'
|
|
44
|
+
import { untrack } from 'svelte'
|
|
45
|
+
import { click_outside, tooltip } from 'svelte-multiselect'
|
|
46
|
+
import type { HTMLAttributes } from 'svelte/elements'
|
|
47
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
48
|
+
import type { Camera, OrthographicCamera, Scene } from 'three'
|
|
49
|
+
import type { AtomColorConfig } from './atom-properties'
|
|
50
|
+
import { get_property_colors } from './atom-properties'
|
|
51
|
+
import AtomLegend from './AtomLegend.svelte'
|
|
52
|
+
import CellSelect from './CellSelect.svelte'
|
|
53
|
+
import type { StructureHandlerData } from './index'
|
|
54
|
+
import { MAX_SELECTED_SITES } from './measure'
|
|
55
|
+
import { normalize_fractional_coords, parse_any_structure } from './parse'
|
|
56
|
+
import StructureControls from './StructureControls.svelte'
|
|
57
|
+
import StructureExportPane from './StructureExportPane.svelte'
|
|
58
|
+
import StructureInfoPane from './StructureInfoPane.svelte'
|
|
59
|
+
import StructureScene from './StructureScene.svelte'
|
|
60
|
+
|
|
61
|
+
// Type alias for event handlers to reduce verbosity
|
|
62
|
+
type EventHandler = (data: StructureHandlerData) => void
|
|
63
|
+
|
|
64
|
+
// Local reactive state for scene and lattice props. Deeply reactive so nested mutations propagate.
|
|
65
|
+
// Deep-clone to prevent mutations from leaking to global defaults across component instances.
|
|
66
|
+
let scene_props = $state(
|
|
67
|
+
structuredClone(DEFAULTS.structure) as typeof DEFAULTS.structure & {
|
|
68
|
+
camera_target?: Vec3
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
let lattice_props = $state({
|
|
36
72
|
cell_edge_opacity: DEFAULTS.structure.cell_edge_opacity,
|
|
37
73
|
cell_surface_opacity: DEFAULTS.structure.cell_surface_opacity,
|
|
38
74
|
cell_edge_color: DEFAULTS.structure.cell_edge_color,
|
|
39
75
|
cell_surface_color: DEFAULTS.structure.cell_surface_color,
|
|
40
76
|
cell_edge_width: DEFAULTS.structure.cell_edge_width,
|
|
41
77
|
show_cell_vectors: DEFAULTS.structure.show_cell_vectors,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
$
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
let {
|
|
81
|
+
structure = $bindable(),
|
|
82
|
+
scene_props: scene_props_in = $bindable(),
|
|
83
|
+
lattice_props: lattice_props_in = $bindable(),
|
|
84
|
+
controls_open = $bindable(false),
|
|
85
|
+
info_pane_open = $bindable(false),
|
|
86
|
+
enable_measure_mode = $bindable(true),
|
|
87
|
+
measure_mode = $bindable<MeasureMode>(`distance`),
|
|
88
|
+
background_color = $bindable(),
|
|
89
|
+
background_opacity = $bindable(0.1),
|
|
90
|
+
show_controls,
|
|
91
|
+
fullscreen = $bindable(false),
|
|
92
|
+
wrapper = $bindable(),
|
|
93
|
+
width = $bindable(0),
|
|
94
|
+
height = $bindable(0),
|
|
95
|
+
reset_text = `Reset camera (or double-click)`,
|
|
96
|
+
color_scheme = $bindable(`Vesta`),
|
|
97
|
+
atom_color_config = $bindable({
|
|
98
|
+
mode: DEFAULTS.structure.atom_color_mode,
|
|
99
|
+
scale: DEFAULTS.structure.atom_color_scale,
|
|
100
|
+
scale_type: DEFAULTS.structure.atom_color_scale_type,
|
|
101
|
+
}),
|
|
102
|
+
hovered = $bindable(false),
|
|
103
|
+
dragover = $bindable(false),
|
|
104
|
+
allow_file_drop = true,
|
|
105
|
+
enable_info_pane = true,
|
|
106
|
+
png_dpi = $bindable(150),
|
|
107
|
+
show_image_atoms = $bindable(true),
|
|
108
|
+
supercell_scaling = $bindable(`1x1x1`),
|
|
109
|
+
fullscreen_toggle = DEFAULTS.structure.fullscreen_toggle,
|
|
110
|
+
bottom_left,
|
|
111
|
+
data_url,
|
|
112
|
+
structure_string,
|
|
113
|
+
on_file_drop,
|
|
114
|
+
spinner_props = {},
|
|
115
|
+
loading = $bindable(false),
|
|
116
|
+
error_msg = $bindable(),
|
|
117
|
+
performance_mode = $bindable(`quality`),
|
|
118
|
+
// expose selected site indices for external control/highlighting
|
|
119
|
+
selected_sites = $bindable([]),
|
|
120
|
+
// expose measured site indices for overlays/labels
|
|
121
|
+
measured_sites = $bindable([]),
|
|
122
|
+
// expose the displayed structure (with image atoms and supercell) for external use
|
|
123
|
+
displayed_structure = $bindable(),
|
|
124
|
+
// Track hidden elements across component lifecycle
|
|
125
|
+
hidden_elements = $bindable(new SvelteSet<ElementSymbol>()),
|
|
126
|
+
// Track hidden property values (e.g. Wyckoff positions, coordination numbers)
|
|
127
|
+
hidden_prop_vals = $bindable(new SvelteSet<number | string>()),
|
|
128
|
+
// Per-element radius overrides (absolute values in Angstroms)
|
|
129
|
+
element_radius_overrides = $bindable<Partial<Record<ElementSymbol, number>>>({}),
|
|
130
|
+
// Per-site radius overrides (absolute values in Angstroms)
|
|
131
|
+
site_radius_overrides = $bindable<SvelteMap<number, number>>(new SvelteMap()),
|
|
132
|
+
// Symmetry analysis data (bindable for external access)
|
|
133
|
+
sym_data = $bindable(null),
|
|
134
|
+
// Symmetry analysis settings (bindable for external control)
|
|
135
|
+
symmetry_settings = $bindable(symmetry.default_sym_settings),
|
|
136
|
+
// Map element symbols to different elements (e.g. {'H': 'Na', 'He': 'Cl'})
|
|
137
|
+
// Useful for LAMMPS files where atom types are mapped to H, He, Li by default
|
|
138
|
+
element_mapping = $bindable(),
|
|
139
|
+
// Cell type: original, conventional, or primitive (requires symmetry analysis)
|
|
140
|
+
cell_type = $bindable(`original`),
|
|
141
|
+
// Volumetric data for isosurface rendering (parsed from CHGCAR or .cube files)
|
|
142
|
+
volumetric_data = $bindable<VolumetricData[]>(),
|
|
143
|
+
// Isosurface rendering settings
|
|
144
|
+
isosurface_settings = $bindable<IsosurfaceSettings>({
|
|
145
|
+
...DEFAULT_ISOSURFACE_SETTINGS,
|
|
146
|
+
}),
|
|
147
|
+
// Active volume index when multiple volumes are present
|
|
148
|
+
active_volume_idx = $bindable(0),
|
|
149
|
+
children,
|
|
150
|
+
top_right_controls,
|
|
151
|
+
on_file_load,
|
|
152
|
+
on_error,
|
|
153
|
+
on_fullscreen_change,
|
|
154
|
+
on_camera_move,
|
|
155
|
+
on_camera_reset,
|
|
156
|
+
...rest
|
|
157
|
+
}:
|
|
158
|
+
& {
|
|
159
|
+
structure?: AnyStructure
|
|
160
|
+
scene_props?: ComponentProps<typeof StructureScene>
|
|
161
|
+
/**
|
|
162
|
+
* Controls visibility configuration.
|
|
163
|
+
* - 'always': controls always visible
|
|
164
|
+
* - 'hover': controls visible on component hover (default)
|
|
165
|
+
* - 'never': controls never visible
|
|
166
|
+
* - object: { mode, hidden, style } for fine-grained control
|
|
167
|
+
*
|
|
168
|
+
* Control names: 'reset-camera', 'fullscreen', 'measure-mode', 'info-pane', 'export-pane', 'controls'
|
|
169
|
+
*/
|
|
170
|
+
show_controls?: ShowControlsProp
|
|
171
|
+
fullscreen?: boolean
|
|
172
|
+
// bindable width of the canvas
|
|
173
|
+
width?: number
|
|
174
|
+
// bindable height of the canvas
|
|
175
|
+
height?: number
|
|
176
|
+
// Canvas wrapper element (for export pane)
|
|
177
|
+
wrapper?: HTMLDivElement
|
|
178
|
+
// PNG export DPI setting
|
|
179
|
+
png_dpi?: number
|
|
180
|
+
reset_text?: string
|
|
181
|
+
hovered?: boolean
|
|
182
|
+
dragover?: boolean
|
|
183
|
+
allow_file_drop?: boolean
|
|
184
|
+
enable_info_pane?: boolean
|
|
185
|
+
enable_measure_mode?: boolean
|
|
186
|
+
measure_mode?: MeasureMode
|
|
187
|
+
info_pane_open?: boolean
|
|
188
|
+
fullscreen_toggle?: Snippet<[{ fullscreen: boolean }]> | boolean
|
|
189
|
+
bottom_left?: Snippet<[{ structure?: AnyStructure }]>
|
|
190
|
+
top_right_controls?: Snippet // Additional controls to render at the end of the control buttons row
|
|
191
|
+
data_url?: string // URL to load structure from (alternative to providing structure directly)
|
|
192
|
+
// Generic callback for when files are dropped - receives raw content and filename
|
|
193
|
+
on_file_drop?: (content: string | ArrayBuffer, filename: string) => void
|
|
194
|
+
// spinner props (passed to Spinner component)
|
|
195
|
+
spinner_props?: ComponentProps<typeof Spinner>
|
|
196
|
+
loading?: boolean
|
|
197
|
+
error_msg?: string
|
|
198
|
+
// Performance mode: 'quality' (default) or 'speed' for large structures
|
|
199
|
+
performance_mode?: `quality` | `speed`
|
|
200
|
+
// allow parent components to control highlighted/selected site indices
|
|
201
|
+
selected_sites?: number[]
|
|
202
|
+
// explicit measured sites for distance/angle overlays
|
|
203
|
+
measured_sites?: number[]
|
|
204
|
+
// expose the displayed structure (with image atoms and/or supercell) for external use
|
|
205
|
+
displayed_structure?: AnyStructure
|
|
206
|
+
// Track which elements are hidden (bindable across frames in trajectories)
|
|
207
|
+
hidden_elements?: Set<ElementSymbol>
|
|
208
|
+
// Track which property values are hidden (e.g. Wyckoff positions, coordination numbers)
|
|
209
|
+
hidden_prop_vals?: Set<number | string>
|
|
210
|
+
// Per-element radius overrides (absolute values in Angstroms)
|
|
211
|
+
element_radius_overrides?: Partial<Record<ElementSymbol, number>>
|
|
212
|
+
// Per-site radius overrides (absolute values in Angstroms)
|
|
213
|
+
// Accepts Map or SvelteMap for flexibility with external callers
|
|
214
|
+
site_radius_overrides?: Map<number, number> | SvelteMap<number, number>
|
|
215
|
+
// Symmetry analysis data (bindable for external access)
|
|
216
|
+
sym_data?: MoyoDataset | null
|
|
217
|
+
// Symmetry analysis settings (bindable for external control)
|
|
218
|
+
symmetry_settings?: Partial<SymmetrySettings>
|
|
219
|
+
// Map element symbols to different elements (e.g. {'H': 'Na', 'He': 'Cl'})
|
|
220
|
+
element_mapping?: Partial<Record<ElementSymbol, ElementSymbol>>
|
|
221
|
+
// Cell type: original, conventional, or primitive (requires symmetry analysis)
|
|
222
|
+
cell_type?: CellType
|
|
223
|
+
// Volumetric data for isosurface rendering (parsed from CHGCAR or .cube files)
|
|
224
|
+
volumetric_data?: VolumetricData[]
|
|
225
|
+
// Isosurface rendering settings
|
|
226
|
+
isosurface_settings?: IsosurfaceSettings
|
|
227
|
+
// Active volume index when multiple volumes are present
|
|
228
|
+
active_volume_idx?: number
|
|
229
|
+
// structure content as string (alternative to providing structure directly or via data_url)
|
|
230
|
+
structure_string?: string
|
|
231
|
+
// Atom coloring configuration
|
|
232
|
+
atom_color_config?: Partial<AtomColorConfig>
|
|
233
|
+
children?: Snippet<[{ structure?: AnyStructure; fullscreen: boolean }]>
|
|
234
|
+
on_file_load?: EventHandler
|
|
235
|
+
on_error?: EventHandler
|
|
236
|
+
on_fullscreen_change?: EventHandler
|
|
237
|
+
on_camera_move?: EventHandler
|
|
238
|
+
on_camera_reset?: EventHandler
|
|
239
|
+
}
|
|
240
|
+
& Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`>
|
|
241
|
+
& Omit<HTMLAttributes<HTMLDivElement>, `children`> = $props()
|
|
242
|
+
|
|
243
|
+
// Initialize models from incoming props; mutations come from UI controls; we mirror into local dicts (NOTE only doing shallow merge)
|
|
244
|
+
$effect.pre(() => {
|
|
81
245
|
if (scene_props_in && typeof scene_props_in === `object`) {
|
|
82
|
-
|
|
246
|
+
Object.assign(scene_props, scene_props_in)
|
|
83
247
|
}
|
|
84
248
|
if (lattice_props_in && typeof lattice_props_in === `object`) {
|
|
85
|
-
|
|
249
|
+
Object.assign(lattice_props, lattice_props_in)
|
|
86
250
|
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Load structure from URL when data_url is provided
|
|
254
|
+
$effect(() => {
|
|
90
255
|
if (data_url && !structure) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
else {
|
|
97
|
-
// Parse structure internally when no handler provided
|
|
98
|
-
try {
|
|
99
|
-
const text_content = content instanceof ArrayBuffer
|
|
100
|
-
? new TextDecoder().decode(content)
|
|
101
|
-
: content;
|
|
102
|
-
const parsed = parse_file_content(text_content, filename);
|
|
103
|
-
emit_file_load_event(parsed, filename, content);
|
|
104
|
-
}
|
|
105
|
-
catch (error) {
|
|
106
|
-
error_msg = `Failed to parse structure: ${error instanceof Error ? error.message : String(error)}`;
|
|
107
|
-
on_error?.({ error_msg, filename });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
})
|
|
111
|
-
.then(() => loading = false)
|
|
112
|
-
.catch((error) => {
|
|
113
|
-
console.error(`Failed to load structure from URL:`, error);
|
|
114
|
-
error_msg = `Failed to load structure: ${error.message}`;
|
|
115
|
-
loading = false;
|
|
116
|
-
on_error?.({ error_msg, filename: data_url });
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
});
|
|
120
|
-
$effect(() => {
|
|
121
|
-
if (!structure_string || data_url)
|
|
122
|
-
return;
|
|
123
|
-
loading = true;
|
|
124
|
-
error_msg = undefined;
|
|
125
|
-
try {
|
|
126
|
-
const parsed = parse_any_structure(structure_string, `string`);
|
|
127
|
-
if (parsed) {
|
|
128
|
-
structure = parsed;
|
|
129
|
-
untrack(() => emit_file_load_event(parsed, `string`, structure_string));
|
|
130
|
-
}
|
|
256
|
+
loading = true
|
|
257
|
+
error_msg = undefined
|
|
258
|
+
|
|
259
|
+
load_from_url(data_url, (content, filename) => {
|
|
260
|
+
if (on_file_drop) on_file_drop(content, filename)
|
|
131
261
|
else {
|
|
132
|
-
|
|
262
|
+
// Parse structure internally when no handler provided
|
|
263
|
+
try {
|
|
264
|
+
const text_content = content instanceof ArrayBuffer
|
|
265
|
+
? new TextDecoder().decode(content)
|
|
266
|
+
: content
|
|
267
|
+
const parsed = parse_file_content(text_content, filename)
|
|
268
|
+
emit_file_load_event(parsed, filename, content)
|
|
269
|
+
} catch (error) {
|
|
270
|
+
error_msg = `Failed to parse structure: ${
|
|
271
|
+
error instanceof Error ? error.message : String(error)
|
|
272
|
+
}`
|
|
273
|
+
on_error?.({ error_msg, filename })
|
|
274
|
+
}
|
|
133
275
|
}
|
|
276
|
+
})
|
|
277
|
+
.then(() => loading = false)
|
|
278
|
+
.catch((error: Error) => {
|
|
279
|
+
console.error(`Failed to load structure from URL:`, error)
|
|
280
|
+
error_msg = `Failed to load structure: ${error.message}`
|
|
281
|
+
loading = false
|
|
282
|
+
on_error?.({ error_msg, filename: data_url })
|
|
283
|
+
})
|
|
134
284
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
$effect(() => { // Parse structure from string when structure_string is provided
|
|
288
|
+
if (!structure_string || data_url) return
|
|
289
|
+
loading = true
|
|
290
|
+
error_msg = undefined
|
|
291
|
+
clear_camera_state()
|
|
292
|
+
try {
|
|
293
|
+
const parsed = parse_any_structure(structure_string, `string`)
|
|
294
|
+
if (parsed) {
|
|
295
|
+
structure = parsed
|
|
296
|
+
untrack(() => emit_file_load_event(parsed, `string`, structure_string))
|
|
297
|
+
} else {
|
|
298
|
+
throw new Error(`Failed to parse structure from string`)
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
error_msg = `Failed to parse structure from string: ${
|
|
302
|
+
err instanceof Error ? err.message : String(err)
|
|
303
|
+
}`
|
|
304
|
+
untrack(() => on_error?.({ error_msg, filename: `string` }))
|
|
305
|
+
} finally {
|
|
306
|
+
loading = false
|
|
141
307
|
}
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// Auto-populate vector_configs when structure has vector data (force, magmom, spin, etc.)
|
|
311
|
+
// Skip if configs were externally provided. Clear auto-generated configs on structure change.
|
|
312
|
+
let vectors_auto_populated_for: AnyStructure | undefined = undefined
|
|
313
|
+
let last_auto_configs: Record<string, unknown> | undefined = undefined
|
|
314
|
+
|
|
315
|
+
$effect(() => {
|
|
316
|
+
if (!structure?.sites || structure === vectors_auto_populated_for) return
|
|
317
|
+
const keys = get_structure_vector_keys(structure)
|
|
318
|
+
// Clear auto-generated configs from previous structure; preserve externally-modified ones
|
|
319
|
+
const existing = scene_props.vector_configs
|
|
320
|
+
if (last_auto_configs && existing === last_auto_configs) {
|
|
321
|
+
scene_props.vector_configs = {}
|
|
322
|
+
last_auto_configs = undefined
|
|
323
|
+
} else if (existing && Object.keys(existing).length > 0) {
|
|
324
|
+
vectors_auto_populated_for = structure
|
|
325
|
+
return
|
|
157
326
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
327
|
+
vectors_auto_populated_for = structure
|
|
328
|
+
if (keys.length === 0) return
|
|
329
|
+
const configs = default_vector_configs(keys)
|
|
330
|
+
scene_props.vector_configs = configs
|
|
331
|
+
// Read back the proxied reference — Svelte 5 $state wraps objects in
|
|
332
|
+
// proxies, so `scene_props.vector_configs !== configs`. Storing the proxy
|
|
333
|
+
// lets the identity check above detect unmodified auto-configs.
|
|
334
|
+
// See https://svelte.dev/e/state_proxy_equality_mismatch
|
|
335
|
+
last_auto_configs = scene_props.vector_configs
|
|
336
|
+
scene_props.vector_scale ??= DEFAULTS.structure.vector_scale
|
|
337
|
+
scene_props.vector_color ??= DEFAULTS.structure.vector_color
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Optimize scene props for performance based on structure size and mode
|
|
341
|
+
$effect(() => {
|
|
161
342
|
if (structure?.sites && performance_mode === `speed`) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
343
|
+
const site_count = structure.sites.length
|
|
344
|
+
const current_sphere_segments = scene_props.sphere_segments || 20
|
|
345
|
+
|
|
346
|
+
// Reduce sphere segments for large structures in speed mode
|
|
347
|
+
if (site_count > 200) {
|
|
348
|
+
scene_props.sphere_segments = Math.min(current_sphere_segments, 12)
|
|
349
|
+
}
|
|
168
350
|
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
$effect(() => {
|
|
354
|
+
colors.element = ELEMENT_COLOR_SCHEMES[color_scheme as ColorSchemeName]
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
// Compute property-based colors for legend display
|
|
358
|
+
let property_colors = $derived(
|
|
359
|
+
get_property_colors(
|
|
360
|
+
structure,
|
|
361
|
+
atom_color_config,
|
|
362
|
+
scene_props.bonding_strategy,
|
|
363
|
+
sym_data,
|
|
364
|
+
),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
let symmetry_run_id = 0
|
|
368
|
+
let symmetry_error = $state<string>()
|
|
369
|
+
let last_symmetry_structure_ref: AnyStructure | null = null
|
|
370
|
+
|
|
371
|
+
// Trigger symmetry analysis when structure is loaded or settings change.
|
|
372
|
+
// Skip during atom drags — symmetry doesn't change from moving atoms,
|
|
373
|
+
// and WASM analysis on every drag frame causes severe frame drops.
|
|
374
|
+
$effect(() => {
|
|
375
|
+
if (dragging_atoms) return
|
|
184
376
|
if (!structure || !(`lattice` in structure)) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
377
|
+
untrack(() => {
|
|
378
|
+
sym_data = null
|
|
379
|
+
symmetry_error = undefined
|
|
380
|
+
})
|
|
381
|
+
last_symmetry_structure_ref = null
|
|
382
|
+
return
|
|
191
383
|
}
|
|
192
|
-
|
|
193
|
-
const
|
|
384
|
+
|
|
385
|
+
const current_structure = structure
|
|
386
|
+
const structure_changed = current_structure !== last_symmetry_structure_ref
|
|
194
387
|
if (structure_changed) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
388
|
+
untrack(() => {
|
|
389
|
+
sym_data = null
|
|
390
|
+
symmetry_error = undefined
|
|
391
|
+
})
|
|
392
|
+
last_symmetry_structure_ref = current_structure
|
|
393
|
+
} else {
|
|
394
|
+
// Keep previous symmetry data while recomputing so bound consumers
|
|
395
|
+
// (e.g. SymmetryStats inputs) do not unmount and lose focus.
|
|
396
|
+
untrack(() => symmetry_error = undefined)
|
|
200
397
|
}
|
|
201
|
-
|
|
202
|
-
// Keep previous symmetry data while recomputing so bound consumers
|
|
203
|
-
// (e.g. SymmetryStats inputs) do not unmount and lose focus.
|
|
204
|
-
untrack(() => symmetry_error = undefined);
|
|
205
|
-
}
|
|
206
|
-
const run_id = ++symmetry_run_id;
|
|
398
|
+
const run_id = ++symmetry_run_id
|
|
207
399
|
// Destructure symmetry_settings to ensure Svelte tracks changes to symprec and algo
|
|
208
400
|
// (reading just the object reference isn't sufficient for fine-grained reactivity)
|
|
209
|
-
const { symprec, algo } = symmetry_settings ?? symmetry.default_sym_settings
|
|
210
|
-
const current_settings = { symprec, algo }
|
|
401
|
+
const { symprec, algo } = symmetry_settings ?? symmetry.default_sym_settings
|
|
402
|
+
const current_settings = { symprec, algo }
|
|
403
|
+
// Skip symmetry auto-analysis in unit tests; happy-dom can't fetch WASM assets
|
|
404
|
+
if (typeof process !== `undefined` && process.env?.VITEST) return
|
|
405
|
+
|
|
211
406
|
symmetry.ensure_moyo_wasm_ready()
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
407
|
+
.then(() =>
|
|
408
|
+
run_id === symmetry_run_id
|
|
409
|
+
? symmetry.analyze_structure_symmetry(current_structure, current_settings)
|
|
410
|
+
: null
|
|
411
|
+
)
|
|
412
|
+
.then((data) => {
|
|
216
413
|
if (data && run_id === symmetry_run_id) {
|
|
217
|
-
|
|
414
|
+
untrack(() => sym_data = data)
|
|
218
415
|
}
|
|
219
|
-
|
|
220
|
-
|
|
416
|
+
})
|
|
417
|
+
.catch((err) => {
|
|
221
418
|
if (run_id === symmetry_run_id) {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
419
|
+
untrack(() => sym_data = null)
|
|
420
|
+
symmetry_error = `Symmetry analysis failed: ${err?.message || err}`
|
|
421
|
+
console.error(`Symmetry analysis failed:`, err)
|
|
225
422
|
}
|
|
226
|
-
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
let
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
let
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
//
|
|
242
|
-
|
|
243
|
-
let
|
|
244
|
-
|
|
245
|
-
let
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
let
|
|
249
|
-
let
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
423
|
+
})
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
let measure_menu_open = $state(false)
|
|
427
|
+
let export_pane_open = $state(false)
|
|
428
|
+
|
|
429
|
+
// Bond customization state
|
|
430
|
+
let added_bonds = $state<[number, number][]>([])
|
|
431
|
+
let removed_bonds = $state<[number, number][]>([])
|
|
432
|
+
|
|
433
|
+
// === Edit-atoms mode state ===
|
|
434
|
+
let dragging_atoms = $state(false)
|
|
435
|
+
let undo_stack = $state<AnyStructure[]>([])
|
|
436
|
+
let redo_stack = $state<AnyStructure[]>([])
|
|
437
|
+
const MAX_HISTORY = 20
|
|
438
|
+
// Flag set before internal edits (undo/redo/delete/add/move) to distinguish
|
|
439
|
+
// them from external structure changes (file load, trajectory step, etc.)
|
|
440
|
+
let is_internal_edit = false
|
|
441
|
+
// Add-atom sub-mode state (bound to StructureScene)
|
|
442
|
+
let add_atom_mode = $state(false)
|
|
443
|
+
let add_element = $state<ElementSymbol>(`C` as ElementSymbol)
|
|
444
|
+
let canvas_cursor = $state(`default`)
|
|
445
|
+
let change_element_mode = $state(false)
|
|
446
|
+
let change_element_value = $state(``)
|
|
447
|
+
// Ephemeral toast message for edit operations
|
|
448
|
+
let toast_msg = $state<string | null>(null)
|
|
449
|
+
let toast_timer: ReturnType<typeof setTimeout> | undefined
|
|
450
|
+
function show_toast(msg: string, duration_ms = 2000) {
|
|
451
|
+
clearTimeout(toast_timer)
|
|
452
|
+
toast_msg = msg
|
|
453
|
+
toast_timer = setTimeout(() => (toast_msg = null), duration_ms)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Normalize and validate element symbol (e.g. "fe" → "Fe", "Xx" → null)
|
|
457
|
+
function normalize_element(input: string): ElementSymbol | null {
|
|
257
458
|
const normalized = (input.charAt(0).toUpperCase() +
|
|
258
|
-
|
|
259
|
-
return ELEM_SYMBOLS.includes(normalized) ? normalized : null
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
459
|
+
input.slice(1).toLowerCase()) as ElementSymbol
|
|
460
|
+
return ELEM_SYMBOLS.includes(normalized) ? normalized : null
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function clear_selection() {
|
|
464
|
+
selected_sites = []
|
|
465
|
+
measured_sites = []
|
|
466
|
+
dragging_atoms = false
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function push_undo() {
|
|
470
|
+
if (!structure) return
|
|
269
471
|
if (undo_stack.length >= MAX_HISTORY) {
|
|
270
|
-
|
|
472
|
+
undo_stack.splice(0, undo_stack.length - MAX_HISTORY + 1)
|
|
271
473
|
}
|
|
272
|
-
undo_stack.push($state.snapshot(structure))
|
|
273
|
-
redo_stack.length = 0
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const restored = source.pop()
|
|
280
|
-
if (!restored)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const undo = () => apply_history(undo_stack, redo_stack)
|
|
288
|
-
const redo = () => apply_history(redo_stack, undo_stack)
|
|
289
|
-
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
$effect
|
|
474
|
+
undo_stack.push($state.snapshot(structure) as AnyStructure)
|
|
475
|
+
redo_stack.length = 0
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Shared undo/redo: pop from `source`, push current state onto `target`
|
|
479
|
+
function apply_history(source: AnyStructure[], target: AnyStructure[]) {
|
|
480
|
+
if (source.length === 0 || !structure) return
|
|
481
|
+
const restored = source.pop()
|
|
482
|
+
if (!restored) return
|
|
483
|
+
is_internal_edit = true
|
|
484
|
+
target.push($state.snapshot(structure) as AnyStructure)
|
|
485
|
+
structure = restored
|
|
486
|
+
clear_selection()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const undo = () => apply_history(undo_stack, redo_stack)
|
|
490
|
+
const redo = () => apply_history(redo_stack, undo_stack)
|
|
491
|
+
|
|
492
|
+
// Clear undo/redo stacks when structure changes externally (file load, etc.)
|
|
493
|
+
// Internal edits set is_internal_edit=true before modifying structure.
|
|
494
|
+
// This $effect runs after microtask, so the flag is still set from the edit.
|
|
495
|
+
$effect(() => {
|
|
293
496
|
// Track structure to re-run when it changes
|
|
294
|
-
void structure
|
|
497
|
+
void structure
|
|
295
498
|
if (is_internal_edit) {
|
|
296
|
-
|
|
297
|
-
|
|
499
|
+
is_internal_edit = false
|
|
500
|
+
return
|
|
298
501
|
}
|
|
299
502
|
// External change — clear history and stale edit-atoms state
|
|
300
503
|
untrack(() => {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
void measure_mode; // track reactively
|
|
504
|
+
if (undo_stack.length > 0 || redo_stack.length > 0) {
|
|
505
|
+
undo_stack = []
|
|
506
|
+
redo_stack = []
|
|
507
|
+
}
|
|
508
|
+
if (measure_mode === `edit-atoms`) {
|
|
509
|
+
if (selected_sites.length > 0 || measured_sites.length > 0) clear_selection()
|
|
510
|
+
if (site_radius_overrides?.size > 0) site_radius_overrides.clear()
|
|
511
|
+
}
|
|
512
|
+
})
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
// Clear selection when switching measure/edit mode so stale state doesn't carry over
|
|
516
|
+
let mode_first_run = true
|
|
517
|
+
$effect(() => {
|
|
518
|
+
void measure_mode // track reactively
|
|
317
519
|
if (mode_first_run) {
|
|
318
|
-
|
|
319
|
-
|
|
520
|
+
mode_first_run = false
|
|
521
|
+
return
|
|
320
522
|
}
|
|
321
523
|
untrack(() => {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
// Auto-bake cell type transform and clear stale state when entering edit-atoms mode
|
|
327
|
-
$effect(() => {
|
|
328
|
-
if (measure_mode !== `edit-atoms`)
|
|
329
|
-
return;
|
|
524
|
+
if (selected_sites.length > 0 || measured_sites.length > 0) clear_selection()
|
|
525
|
+
})
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
// Auto-bake cell type transform and clear stale state when entering edit-atoms mode
|
|
529
|
+
$effect(() => {
|
|
530
|
+
if (measure_mode !== `edit-atoms`) return
|
|
330
531
|
untrack(() => {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
})
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
532
|
+
// Clear bond edits from edit-bonds mode to avoid stale state
|
|
533
|
+
if (added_bonds.length > 0 || removed_bonds.length > 0) {
|
|
534
|
+
added_bonds = []
|
|
535
|
+
removed_bonds = []
|
|
536
|
+
}
|
|
537
|
+
if (cell_type !== `original` && cell_transformed_structure && structure) {
|
|
538
|
+
// Bake the transformed cell: push original to undo, replace structure
|
|
539
|
+
is_internal_edit = true
|
|
540
|
+
push_undo()
|
|
541
|
+
structure = $state.snapshot(cell_transformed_structure) as AnyStructure
|
|
542
|
+
cell_type = `original`
|
|
543
|
+
}
|
|
544
|
+
})
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
let controls_config = $derived(normalize_show_controls(show_controls))
|
|
548
|
+
|
|
549
|
+
// Normalize structure coordinates: wrap fractional coords to [0,1) and recompute Cartesian
|
|
550
|
+
// This ensures atoms are rendered inside the unit cell regardless of data source
|
|
551
|
+
let normalized_structure = $derived.by(() => {
|
|
552
|
+
if (!structure || !(`lattice` in structure)) return structure
|
|
553
|
+
return normalize_fractional_coords(structure) as AnyStructure
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
// Apply cell type transformation (original, conventional, or primitive)
|
|
557
|
+
// This must happen BEFORE supercell transformation
|
|
558
|
+
let cell_transformed_structure = $derived.by(() => {
|
|
559
|
+
if (
|
|
560
|
+
!normalized_structure || !(`lattice` in normalized_structure) ||
|
|
561
|
+
cell_type === `original`
|
|
562
|
+
) {
|
|
563
|
+
return normalized_structure
|
|
359
564
|
}
|
|
360
565
|
// Cell type transformation requires symmetry data
|
|
361
566
|
if (!sym_data) {
|
|
362
|
-
|
|
567
|
+
return normalized_structure
|
|
363
568
|
}
|
|
364
569
|
try {
|
|
365
|
-
|
|
570
|
+
return transform_cell(normalized_structure as Crystal, cell_type, sym_data)
|
|
571
|
+
} catch (error) {
|
|
572
|
+
console.error(`Failed to transform cell to ${cell_type}:`, error)
|
|
573
|
+
return normalized_structure
|
|
366
574
|
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
// Create supercell if needed (uses cell_transformed_structure as base)
|
|
578
|
+
let supercell_structure = $state(structure)
|
|
579
|
+
let supercell_loading = $state(false)
|
|
580
|
+
let has_supercell = $derived(
|
|
581
|
+
!!supercell_scaling && ![``, `1x1x1`, `1`].includes(supercell_scaling),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
// Tile volumetric data to match supercell when active.
|
|
585
|
+
// Gate on !supercell_loading so the tiled volume and supercell structure update
|
|
586
|
+
// in the same frame (large supercells defer structure via setTimeout).
|
|
587
|
+
let supercell_volume = $derived.by(() => {
|
|
588
|
+
const vol = volumetric_data?.[active_volume_idx]
|
|
589
|
+
if (!vol || !has_supercell || supercell_loading) return vol
|
|
590
|
+
try {
|
|
591
|
+
return tile_volumetric_data(vol, parse_supercell_scaling(supercell_scaling))
|
|
592
|
+
} catch {
|
|
593
|
+
return vol
|
|
370
594
|
}
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
let
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
const base_structure = cell_transformed_structure;
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
let supercell_timeout: ReturnType<typeof setTimeout> | undefined
|
|
598
|
+
$effect(() => {
|
|
599
|
+
const base_structure = cell_transformed_structure
|
|
600
|
+
clearTimeout(supercell_timeout)
|
|
378
601
|
if (!base_structure || !(`lattice` in base_structure) || !has_supercell) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
if (base_structure && `lattice` in base_structure) {
|
|
401
|
-
supercell_structure = make_supercell(base_structure, supercell_scaling);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
catch (error) {
|
|
405
|
-
console.error(`Failed to create supercell:`, error);
|
|
406
|
-
supercell_structure = base_structure;
|
|
407
|
-
}
|
|
408
|
-
finally {
|
|
409
|
-
supercell_loading = false;
|
|
410
|
-
}
|
|
411
|
-
}, 10);
|
|
412
|
-
}
|
|
413
|
-
else {
|
|
602
|
+
supercell_structure = base_structure
|
|
603
|
+
supercell_loading = false
|
|
604
|
+
} else if (!is_valid_supercell_input(supercell_scaling)) {
|
|
605
|
+
supercell_structure = base_structure
|
|
606
|
+
supercell_loading = false
|
|
607
|
+
} else {
|
|
608
|
+
// For large supercells, show loading state and use async generation
|
|
609
|
+
const sites_count = base_structure.sites?.length || 0
|
|
610
|
+
const [nx_str, ny_str, nz_str] = supercell_scaling.split(/[x×]/)
|
|
611
|
+
const scaling_mult = (parseInt(nx_str) || 1) * (parseInt(ny_str) || 1) *
|
|
612
|
+
(parseInt(nz_str) || 1)
|
|
613
|
+
const estimated_sites = sites_count * scaling_mult
|
|
614
|
+
|
|
615
|
+
// Show spinner for supercells with >1000 estimated sites or scaling >8
|
|
616
|
+
const show_loading = estimated_sites > 1000 || scaling_mult > 8
|
|
617
|
+
|
|
618
|
+
if (show_loading) {
|
|
619
|
+
supercell_loading = true
|
|
620
|
+
// Use setTimeout to allow UI to update before heavy computation
|
|
621
|
+
supercell_timeout = setTimeout(() => {
|
|
622
|
+
try {
|
|
414
623
|
if (base_structure && `lattice` in base_structure) {
|
|
415
|
-
|
|
624
|
+
supercell_structure = make_supercell(
|
|
625
|
+
base_structure as Crystal,
|
|
626
|
+
supercell_scaling,
|
|
627
|
+
)
|
|
416
628
|
}
|
|
417
|
-
|
|
629
|
+
} catch (error) {
|
|
630
|
+
console.error(`Failed to create supercell:`, error)
|
|
631
|
+
supercell_structure = base_structure
|
|
632
|
+
} finally {
|
|
633
|
+
supercell_loading = false
|
|
634
|
+
}
|
|
635
|
+
}, 10)
|
|
636
|
+
} else {
|
|
637
|
+
if (base_structure && `lattice` in base_structure) {
|
|
638
|
+
supercell_structure = make_supercell(
|
|
639
|
+
base_structure as Crystal,
|
|
640
|
+
supercell_scaling,
|
|
641
|
+
)
|
|
418
642
|
}
|
|
643
|
+
supercell_loading = false
|
|
644
|
+
}
|
|
419
645
|
}
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
//
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
[supercell_scaling, show_image_atoms, structure, cell_type];
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
// Clear selections, site overrides, and stale camera target when transformations
|
|
649
|
+
// change site indices (skip first run to preserve parent-provided selections)
|
|
650
|
+
let first_run = true
|
|
651
|
+
$effect(() => {
|
|
652
|
+
void [supercell_scaling, show_image_atoms, structure, cell_type] // track reactively
|
|
428
653
|
if (first_run) {
|
|
429
|
-
|
|
430
|
-
|
|
654
|
+
first_run = false
|
|
655
|
+
return
|
|
431
656
|
}
|
|
432
657
|
untrack(() => {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
// Apply element mapping then image atoms to the supercell structure.
|
|
445
|
-
// Skip get_pbc_image_sites during atom drags — the vector math + doubled site
|
|
446
|
-
// count causes frame drops. Image atoms reappear instantly on drag release.
|
|
447
|
-
$effect(() => {
|
|
448
|
-
let struct = supercell_structure
|
|
658
|
+
// In edit-atoms mode, structure changes are intentional user edits
|
|
659
|
+
// (move/add/delete) — preserve the selection so TransformControls stays active
|
|
660
|
+
if (measure_mode === `edit-atoms`) return
|
|
661
|
+
if (selected_sites.length > 0 || measured_sites.length > 0) clear_selection()
|
|
662
|
+
// Clear site radius overrides since site indices are no longer valid
|
|
663
|
+
if (site_radius_overrides?.size > 0) site_radius_overrides.clear()
|
|
664
|
+
// Clear stale camera target so orbit controls re-center on the new cell
|
|
665
|
+
scene_props.camera_target = undefined
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
// Apply element mapping then image atoms to the supercell structure.
|
|
670
|
+
// Skip get_pbc_image_sites during atom drags — the vector math + doubled site
|
|
671
|
+
// count causes frame drops. Image atoms reappear instantly on drag release.
|
|
672
|
+
$effect(() => {
|
|
673
|
+
let struct = supercell_structure
|
|
449
674
|
if (struct && element_mapping && Object.keys(element_mapping).length > 0) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
675
|
+
const mapping = element_mapping // capture for TypeScript narrowing
|
|
676
|
+
struct = {
|
|
677
|
+
...struct,
|
|
678
|
+
sites: struct.sites.map((site) => ({
|
|
679
|
+
...site,
|
|
680
|
+
species: site.species.map((sp) => ({
|
|
681
|
+
...sp,
|
|
682
|
+
element: mapping[sp.element as ElementSymbol] ?? sp.element,
|
|
683
|
+
})),
|
|
684
|
+
label: mapping[site.label as ElementSymbol] ?? site.label,
|
|
685
|
+
})),
|
|
686
|
+
}
|
|
462
687
|
}
|
|
463
688
|
displayed_structure =
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
})
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
let
|
|
472
|
-
let
|
|
473
|
-
let
|
|
474
|
-
let
|
|
475
|
-
let
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
$
|
|
689
|
+
!dragging_atoms && show_image_atoms && struct && `lattice` in struct &&
|
|
690
|
+
struct.lattice
|
|
691
|
+
? get_pbc_image_sites(struct)
|
|
692
|
+
: struct
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
// Track if camera has ever been moved from initial position
|
|
696
|
+
let camera_has_moved = $state(false)
|
|
697
|
+
let camera_is_moving = $state(false)
|
|
698
|
+
let scene = $state<Scene | undefined>(undefined)
|
|
699
|
+
let camera = $state<Camera | undefined>(undefined)
|
|
700
|
+
let orbit_controls = $state<
|
|
701
|
+
ComponentProps<typeof StructureScene>[`orbit_controls`]
|
|
702
|
+
>(undefined)
|
|
703
|
+
let rotation_target_ref = $state<Vec3 | undefined>(undefined)
|
|
704
|
+
let initial_computed_zoom = $state<number | undefined>(undefined)
|
|
705
|
+
|
|
706
|
+
// Mutual exclusion: opening one pane closes others
|
|
707
|
+
$effect(() => {
|
|
479
708
|
if (info_pane_open) {
|
|
480
|
-
|
|
709
|
+
untrack(() => [controls_open, export_pane_open] = [false, false])
|
|
481
710
|
}
|
|
482
|
-
})
|
|
483
|
-
$effect(() => {
|
|
711
|
+
})
|
|
712
|
+
$effect(() => {
|
|
484
713
|
if (controls_open) {
|
|
485
|
-
|
|
714
|
+
untrack(() => [info_pane_open, export_pane_open] = [false, false])
|
|
486
715
|
}
|
|
487
|
-
})
|
|
488
|
-
$effect(() => {
|
|
716
|
+
})
|
|
717
|
+
$effect(() => {
|
|
489
718
|
if (export_pane_open) {
|
|
490
|
-
|
|
719
|
+
untrack(() => [info_pane_open, controls_open] = [false, false])
|
|
491
720
|
}
|
|
492
|
-
})
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
// Reset tracking when structure changes
|
|
724
|
+
$effect(() => {
|
|
725
|
+
if (structure) camera_has_moved = false
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
// Clear stale camera target and position so StructureScene uses the new
|
|
729
|
+
// structure's rotation_target (unit cell center) and auto-positions the camera.
|
|
730
|
+
function clear_camera_state() {
|
|
731
|
+
scene_props.camera_target = undefined
|
|
732
|
+
scene_props.camera_position = [0, 0, 0]
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const read_orbit_target = (): Vec3 | undefined => {
|
|
736
|
+
if (!orbit_controls?.target) return
|
|
737
|
+
const { x, y, z } = orbit_controls.target
|
|
738
|
+
return [x, y, z]
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const read_camera_position = (): Vec3 | undefined =>
|
|
742
|
+
camera
|
|
743
|
+
? [camera.position.x, camera.position.y, camera.position.z]
|
|
744
|
+
: scene_props.camera_position
|
|
745
|
+
|
|
746
|
+
// Emit debounced camera updates while controls are active.
|
|
747
|
+
$effect(() => {
|
|
748
|
+
if (!camera_is_moving) return
|
|
749
|
+
camera_has_moved = true
|
|
750
|
+
|
|
512
751
|
const emit_camera_move = () => {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
emit_camera_move()
|
|
527
|
-
const emit_interval = setInterval(emit_camera_move, 200)
|
|
528
|
-
return () => clearInterval(emit_interval)
|
|
529
|
-
})
|
|
530
|
-
|
|
752
|
+
const camera_position = read_camera_position()
|
|
753
|
+
if (camera_position === undefined) return
|
|
754
|
+
const camera_target = read_orbit_target()
|
|
755
|
+
scene_props.camera_position = camera_position
|
|
756
|
+
scene_props.camera_target = camera_target
|
|
757
|
+
on_camera_move?.({
|
|
758
|
+
structure,
|
|
759
|
+
camera_has_moved,
|
|
760
|
+
camera_position,
|
|
761
|
+
camera_target,
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
emit_camera_move()
|
|
766
|
+
const emit_interval = setInterval(emit_camera_move, 200)
|
|
767
|
+
return () => clearInterval(emit_interval)
|
|
768
|
+
})
|
|
769
|
+
|
|
770
|
+
function reset_camera() {
|
|
531
771
|
// Reset camera position to trigger automatic positioning.
|
|
532
|
-
scene_props.camera_position = [0, 0, 0]
|
|
533
|
-
scene_props.camera_target = rotation_target_ref
|
|
534
|
-
camera_has_moved = false
|
|
535
|
-
|
|
536
|
-
let
|
|
772
|
+
scene_props.camera_position = [0, 0, 0]
|
|
773
|
+
scene_props.camera_target = rotation_target_ref
|
|
774
|
+
camera_has_moved = false
|
|
775
|
+
|
|
776
|
+
let camera_position: Vec3 = [0, 0, 0]
|
|
777
|
+
let camera_target: Vec3 | undefined = rotation_target_ref
|
|
778
|
+
|
|
537
779
|
// Reset pan/zoom and ensure controls target returns to structure center.
|
|
538
780
|
if (orbit_controls && camera) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
781
|
+
if (
|
|
782
|
+
`reset` in orbit_controls &&
|
|
783
|
+
typeof orbit_controls.reset === `function`
|
|
784
|
+
) orbit_controls.reset()
|
|
785
|
+
if (orbit_controls.target && rotation_target_ref) {
|
|
786
|
+
const [target_x, target_y, target_z] = rotation_target_ref
|
|
787
|
+
orbit_controls.target.set(target_x, target_y, target_z)
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Reset zoom for orthographic camera
|
|
791
|
+
if (`zoom` in camera && initial_computed_zoom !== undefined) {
|
|
792
|
+
const ortho_camera = camera as OrthographicCamera
|
|
793
|
+
ortho_camera.zoom = initial_computed_zoom
|
|
794
|
+
ortho_camera.updateProjectionMatrix()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Call update to apply changes immediately
|
|
798
|
+
if (typeof orbit_controls.update === `function`) orbit_controls.update()
|
|
799
|
+
camera_position = read_camera_position() ?? camera_position
|
|
800
|
+
camera_target = read_orbit_target()
|
|
557
801
|
}
|
|
558
|
-
|
|
559
|
-
scene_props.
|
|
560
|
-
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
802
|
+
|
|
803
|
+
scene_props.camera_position = camera_position
|
|
804
|
+
scene_props.camera_target = camera_target
|
|
805
|
+
on_camera_reset?.({ structure, camera_has_moved, camera_position, camera_target })
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
const emit_file_load_event = (
|
|
809
|
+
structure: AnyStructure,
|
|
810
|
+
filename: string,
|
|
811
|
+
content: string | ArrayBuffer,
|
|
812
|
+
) =>
|
|
813
|
+
on_file_load?.({
|
|
814
|
+
structure: structure,
|
|
815
|
+
filename,
|
|
816
|
+
file_size: typeof content === `string`
|
|
566
817
|
? new Blob([content]).size
|
|
567
818
|
: content.byteLength,
|
|
568
|
-
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
//
|
|
572
|
-
//
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
819
|
+
total_atoms: structure.sites?.length || 0,
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
// Try to parse content as a volumetric file, setting both structure and volumetric data.
|
|
823
|
+
// Delegates format detection entirely to parse_volumetric_file (filename + content sniffing).
|
|
824
|
+
// Returns the parsed structure on success, or null if the file isn't a volumetric format.
|
|
825
|
+
function try_parse_volumetric(
|
|
826
|
+
text_content: string,
|
|
827
|
+
filename: string,
|
|
828
|
+
): AnyStructure | null {
|
|
829
|
+
const vol_result = parse_volumetric_file(text_content, filename)
|
|
830
|
+
if (!vol_result) return null
|
|
577
831
|
// parse_volumetric_file extracts structure from file header;
|
|
578
832
|
// parsers set pbc so the lattice conforms to Crystal's LatticeType
|
|
579
|
-
structure = vol_result.structure
|
|
580
|
-
volumetric_data = vol_result.volumes
|
|
833
|
+
structure = vol_result.structure as AnyStructure
|
|
834
|
+
volumetric_data = vol_result.volumes
|
|
581
835
|
// Auto-compute reasonable isosurface settings from data range
|
|
582
|
-
const vol = vol_result.volumes[0]
|
|
836
|
+
const vol = vol_result.volumes[0]
|
|
583
837
|
if (vol) {
|
|
584
|
-
|
|
585
|
-
|
|
838
|
+
isosurface_settings = auto_isosurface_settings(vol.data_range)
|
|
839
|
+
active_volume_idx = 0
|
|
586
840
|
}
|
|
587
|
-
return structure
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
//
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
841
|
+
return structure
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Parse file content, trying volumetric format first then falling back to plain structure.
|
|
845
|
+
// Returns the parsed structure on success, throws on failure.
|
|
846
|
+
function parse_file_content(text_content: string, filename: string): AnyStructure {
|
|
847
|
+
clear_camera_state()
|
|
848
|
+
const vol_struct = try_parse_volumetric(text_content, filename)
|
|
849
|
+
if (vol_struct) return vol_struct
|
|
595
850
|
// Clear stale volumetric data when loading a non-volumetric file
|
|
596
|
-
volumetric_data = []
|
|
597
|
-
const parsed = parse_any_structure(text_content, filename)
|
|
598
|
-
if (!parsed)
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
const handle_file_drop = create_file_drop_handler({
|
|
851
|
+
volumetric_data = []
|
|
852
|
+
const parsed = parse_any_structure(text_content, filename)
|
|
853
|
+
if (!parsed) throw new Error(`Failed to parse structure from ${filename}`)
|
|
854
|
+
structure = parsed
|
|
855
|
+
return parsed
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
const handle_file_drop = create_file_drop_handler({
|
|
604
859
|
allow: () => allow_file_drop,
|
|
605
860
|
on_drop: (content, filename) => {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
861
|
+
if (on_file_drop) return on_file_drop(content, filename)
|
|
862
|
+
try {
|
|
863
|
+
const text_content = content instanceof ArrayBuffer
|
|
864
|
+
? new TextDecoder().decode(content)
|
|
865
|
+
: content
|
|
866
|
+
const parsed = parse_file_content(text_content, filename)
|
|
867
|
+
emit_file_load_event(parsed, filename, content)
|
|
868
|
+
} catch (err) {
|
|
869
|
+
error_msg = `Failed to parse structure: ${
|
|
870
|
+
err instanceof Error ? err.message : String(err)
|
|
871
|
+
}`
|
|
872
|
+
on_error?.({ error_msg, filename })
|
|
873
|
+
}
|
|
619
874
|
},
|
|
620
875
|
on_error: (msg) => {
|
|
621
|
-
|
|
622
|
-
|
|
876
|
+
error_msg = msg
|
|
877
|
+
on_error?.({ error_msg: msg })
|
|
623
878
|
},
|
|
624
879
|
set_loading: (val) => {
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
[error_msg, dragover] = [undefined, false];
|
|
880
|
+
loading = val
|
|
881
|
+
if (val) [error_msg, dragover] = [undefined, false]
|
|
628
882
|
},
|
|
629
|
-
})
|
|
630
|
-
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
function handle_keydown(event: KeyboardEvent) {
|
|
631
886
|
// Don't handle shortcuts if user is typing in an input field
|
|
632
|
-
const target = event.target
|
|
887
|
+
const target = event.target as HTMLElement
|
|
633
888
|
const is_input_focused = target.tagName === `INPUT` ||
|
|
634
|
-
|
|
889
|
+
target.tagName === `TEXTAREA`
|
|
890
|
+
|
|
635
891
|
// Allow Escape to cancel add-atom mode even when the element input is focused
|
|
636
892
|
if (event.key === `Escape` && measure_mode === `edit-atoms` && add_atom_mode) {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
893
|
+
event.preventDefault()
|
|
894
|
+
add_atom_mode = false
|
|
895
|
+
return
|
|
640
896
|
}
|
|
641
|
-
|
|
642
|
-
|
|
897
|
+
|
|
898
|
+
if (is_input_focused) return
|
|
899
|
+
|
|
643
900
|
// Edit-atoms mode shortcuts (including undo/redo)
|
|
644
901
|
if (measure_mode === `edit-atoms`) {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
902
|
+
// Undo/redo shortcuts (Ctrl/Cmd + Z/Y) — only active in edit-atoms mode
|
|
903
|
+
if (event.ctrlKey || event.metaKey) {
|
|
904
|
+
const key = event.key.toLowerCase()
|
|
905
|
+
if (key === `z` && !event.shiftKey) {
|
|
906
|
+
event.preventDefault()
|
|
907
|
+
undo()
|
|
908
|
+
show_toast(`Undo (${undo_stack.length} left)`)
|
|
909
|
+
return
|
|
910
|
+
} else if (key === `y` || (key === `z` && event.shiftKey)) {
|
|
911
|
+
event.preventDefault()
|
|
912
|
+
redo()
|
|
913
|
+
show_toast(`Redo (${redo_stack.length} left)`)
|
|
914
|
+
return
|
|
660
915
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
const key = event.key.toLowerCase();
|
|
684
|
-
const plain = !event.ctrlKey && !event.metaKey && !event.altKey;
|
|
685
|
-
if (key === `a` && plain) {
|
|
686
|
-
// Enter add-atom sub-mode (plain 'a' only, not Ctrl+A/Cmd+A/Alt+A)
|
|
687
|
-
event.preventDefault();
|
|
688
|
-
add_atom_mode = !add_atom_mode;
|
|
689
|
-
return;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
if (event.key === `Delete` || event.key === `Backspace`) {
|
|
919
|
+
// Delete selected atoms
|
|
920
|
+
if (selected_sites.length > 0 && structure?.sites) {
|
|
921
|
+
event.preventDefault()
|
|
922
|
+
is_internal_edit = true
|
|
923
|
+
push_undo()
|
|
924
|
+
const to_delete = scene_to_structure_indices(selected_sites, true)
|
|
925
|
+
const n_deleted = to_delete.size
|
|
926
|
+
clear_selection()
|
|
927
|
+
structure = {
|
|
928
|
+
...structure,
|
|
929
|
+
sites: structure.sites.filter((_, idx) => !to_delete.has(idx)),
|
|
930
|
+
}
|
|
931
|
+
// Clear per-site overrides since indices shifted after deletion
|
|
932
|
+
if (site_radius_overrides?.size > 0) site_radius_overrides.clear()
|
|
933
|
+
added_bonds = []
|
|
934
|
+
removed_bonds = []
|
|
935
|
+
show_toast(`Deleted ${n_deleted} site${n_deleted > 1 ? `s` : ``}`)
|
|
690
936
|
}
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
937
|
+
return
|
|
938
|
+
}
|
|
939
|
+
const key = event.key.toLowerCase()
|
|
940
|
+
const plain = !event.ctrlKey && !event.metaKey && !event.altKey
|
|
941
|
+
|
|
942
|
+
if (key === `a` && plain) {
|
|
943
|
+
// Enter add-atom sub-mode (plain 'a' only, not Ctrl+A/Cmd+A/Alt+A)
|
|
944
|
+
event.preventDefault()
|
|
945
|
+
add_atom_mode = !add_atom_mode
|
|
946
|
+
return
|
|
947
|
+
}
|
|
948
|
+
// Change element of selected atoms
|
|
949
|
+
if (key === `e` && plain && selected_sites.length > 0) {
|
|
950
|
+
event.preventDefault()
|
|
951
|
+
change_element_mode = !change_element_mode
|
|
952
|
+
return
|
|
953
|
+
}
|
|
954
|
+
// Duplicate selected atoms at a small offset
|
|
955
|
+
if (
|
|
956
|
+
key === `d` && (event.ctrlKey || event.metaKey) &&
|
|
957
|
+
selected_sites.length > 0 && structure?.sites
|
|
958
|
+
) {
|
|
959
|
+
event.preventDefault()
|
|
960
|
+
is_internal_edit = true
|
|
961
|
+
push_undo()
|
|
962
|
+
const orig_indices = scene_to_structure_indices(selected_sites)
|
|
963
|
+
const cart_to_frac = get_cart_to_frac()
|
|
964
|
+
const new_sites = structure.sites
|
|
965
|
+
.filter((_, idx) => orig_indices.has(idx))
|
|
966
|
+
.map((site) => {
|
|
967
|
+
const new_xyz: Vec3 = [
|
|
968
|
+
site.xyz[0] + 0.5,
|
|
969
|
+
site.xyz[1] + 0.5,
|
|
970
|
+
site.xyz[2] + 0.5,
|
|
971
|
+
]
|
|
972
|
+
return {
|
|
973
|
+
...site,
|
|
974
|
+
xyz: new_xyz,
|
|
975
|
+
abc: cart_to_frac?.(new_xyz) ?? new_xyz,
|
|
976
|
+
properties: { ...site.properties },
|
|
977
|
+
}
|
|
978
|
+
})
|
|
979
|
+
const base_idx = structure.sites.length
|
|
980
|
+
structure = {
|
|
981
|
+
...structure,
|
|
982
|
+
sites: [...structure.sites, ...new_sites],
|
|
696
983
|
}
|
|
697
|
-
//
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
site.xyz[2] + 0.5,
|
|
712
|
-
];
|
|
713
|
-
return {
|
|
714
|
-
...site,
|
|
715
|
-
xyz: new_xyz,
|
|
716
|
-
abc: cart_to_frac?.(new_xyz) ?? new_xyz,
|
|
717
|
-
properties: { ...site.properties },
|
|
718
|
-
};
|
|
719
|
-
});
|
|
720
|
-
const base_idx = structure.sites.length;
|
|
721
|
-
structure = {
|
|
722
|
-
...structure,
|
|
723
|
-
sites: [...structure.sites, ...new_sites],
|
|
724
|
-
};
|
|
725
|
-
// Select the newly duplicated atoms
|
|
726
|
-
selected_sites = new_sites.map((_, idx) => base_idx + idx);
|
|
727
|
-
measured_sites = [...selected_sites];
|
|
728
|
-
show_toast(`Duplicated ${new_sites.length} site${new_sites.length > 1 ? `s` : ``}`);
|
|
729
|
-
return;
|
|
984
|
+
// Select the newly duplicated atoms
|
|
985
|
+
selected_sites = new_sites.map((_, idx) => base_idx + idx)
|
|
986
|
+
measured_sites = [...selected_sites]
|
|
987
|
+
show_toast(
|
|
988
|
+
`Duplicated ${new_sites.length} site${new_sites.length > 1 ? `s` : ``}`,
|
|
989
|
+
)
|
|
990
|
+
return
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// add_atom_mode Escape is already handled above (before is_input_focused guard)
|
|
994
|
+
if (event.key === `Escape`) {
|
|
995
|
+
if (change_element_mode) {
|
|
996
|
+
change_element_mode = false
|
|
997
|
+
return
|
|
730
998
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
change_element_mode = false;
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (selected_sites.length > 0) {
|
|
738
|
-
clear_selection();
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
999
|
+
if (selected_sites.length > 0) {
|
|
1000
|
+
clear_selection()
|
|
1001
|
+
return
|
|
741
1002
|
}
|
|
1003
|
+
}
|
|
742
1004
|
}
|
|
1005
|
+
|
|
743
1006
|
// Interface shortcuts (require Ctrl/Cmd modifier to avoid accidental triggers)
|
|
744
|
-
const has_modifier = event.ctrlKey || event.metaKey
|
|
1007
|
+
const has_modifier = event.ctrlKey || event.metaKey
|
|
745
1008
|
if (event.key === `f` && has_modifier && fullscreen_toggle) {
|
|
746
|
-
|
|
747
|
-
|
|
1009
|
+
event.preventDefault()
|
|
1010
|
+
toggle_fullscreen(wrapper)
|
|
1011
|
+
} else if (event.key === `i` && has_modifier && enable_info_pane) {
|
|
1012
|
+
event.preventDefault()
|
|
1013
|
+
info_pane_open = !info_pane_open
|
|
1014
|
+
} else if (event.key === `Escape`) {
|
|
1015
|
+
// Prioritize closing panes, then exit edit modes, then exit fullscreen
|
|
1016
|
+
if (info_pane_open) info_pane_open = false
|
|
1017
|
+
else if (controls_open) controls_open = false
|
|
1018
|
+
else if (export_pane_open) export_pane_open = false
|
|
1019
|
+
else if (measure_mode === `edit-bonds` || measure_mode === `edit-atoms`) {
|
|
1020
|
+
measure_mode = `distance`
|
|
1021
|
+
}
|
|
748
1022
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
else if (measure_mode === `edit-bonds` || measure_mode === `edit-atoms`) {
|
|
762
|
-
measure_mode = `distance`;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
// === Edit-atoms mode helpers ===
|
|
767
|
-
// Map scene indices (into displayed_structure) back to raw structure indices.
|
|
768
|
-
// Handles supercell atoms via orig_unit_cell_idx property.
|
|
769
|
-
// skip_image_atoms: when true, image atoms (PBC ghosts) are excluded from the result.
|
|
770
|
-
function scene_to_structure_indices(scene_indices, skip_image_atoms = false) {
|
|
771
|
-
const result = new SvelteSet();
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// === Edit-atoms mode helpers ===
|
|
1026
|
+
|
|
1027
|
+
// Map scene indices (into displayed_structure) back to raw structure indices.
|
|
1028
|
+
// Handles supercell atoms via orig_unit_cell_idx property.
|
|
1029
|
+
// skip_image_atoms: when true, image atoms (PBC ghosts) are excluded from the result.
|
|
1030
|
+
function scene_to_structure_indices(
|
|
1031
|
+
scene_indices: number[],
|
|
1032
|
+
skip_image_atoms = false,
|
|
1033
|
+
): SvelteSet<number> {
|
|
1034
|
+
const result = new SvelteSet<number>()
|
|
772
1035
|
for (const scene_idx of scene_indices) {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
result.add(scene_idx);
|
|
788
|
-
}
|
|
1036
|
+
const displayed_site = displayed_structure?.sites?.[scene_idx]
|
|
1037
|
+
if (!displayed_site) continue
|
|
1038
|
+
if (skip_image_atoms && displayed_site.properties?.orig_site_idx != null) {
|
|
1039
|
+
continue
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (has_supercell && displayed_site.properties?.orig_unit_cell_idx != null) {
|
|
1043
|
+
result.add(displayed_site.properties.orig_unit_cell_idx as number)
|
|
1044
|
+
} else if (displayed_site.properties?.orig_site_idx != null) {
|
|
1045
|
+
// Image atom (PBC ghost) — map back to its original site index
|
|
1046
|
+
result.add(displayed_site.properties.orig_site_idx as number)
|
|
1047
|
+
} else {
|
|
1048
|
+
result.add(scene_idx)
|
|
1049
|
+
}
|
|
789
1050
|
}
|
|
790
|
-
return result
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1051
|
+
return result
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
// Try to create a Cartesian→fractional converter for the current structure's lattice
|
|
1055
|
+
function get_cart_to_frac(): ((xyz: Vec3) => Vec3) | undefined {
|
|
1056
|
+
if (!structure || !(`lattice` in structure)) return undefined
|
|
796
1057
|
try {
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
return undefined;
|
|
1058
|
+
return create_cart_to_frac((structure as Crystal).lattice.matrix)
|
|
1059
|
+
} catch {
|
|
1060
|
+
console.warn(`Failed to compute lattice inverse for fractional coordinates`)
|
|
1061
|
+
return undefined
|
|
802
1062
|
}
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
is_internal_edit = true
|
|
810
|
-
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// Handle atom moves from TransformControls. Applies Cartesian delta and wraps
|
|
1066
|
+
// fractional coords inline so normalize_fractional_coords hits its fast path.
|
|
1067
|
+
function handle_sites_moved(scene_indices: number[], delta: Vec3) {
|
|
1068
|
+
if (!structure?.sites) return
|
|
1069
|
+
is_internal_edit = true
|
|
1070
|
+
|
|
1071
|
+
const orig_indices = scene_to_structure_indices(scene_indices)
|
|
811
1072
|
// For crystals, wrap to [0,1) inline so normalize_fractional_coords fast-paths.
|
|
812
1073
|
// For molecules (no lattice), just apply the Cartesian delta directly.
|
|
813
1074
|
const lattice = `lattice` in structure
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
const cart_to_frac = lattice ? create_cart_to_frac(lattice) : null
|
|
817
|
-
const frac_to_cart = lattice ? create_frac_to_cart(lattice) : null
|
|
1075
|
+
? (structure as Crystal).lattice.matrix
|
|
1076
|
+
: null
|
|
1077
|
+
const cart_to_frac = lattice ? create_cart_to_frac(lattice) : null
|
|
1078
|
+
const frac_to_cart = lattice ? create_frac_to_cart(lattice) : null
|
|
818
1079
|
structure = {
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
// Change element symbol of selected atoms
|
|
837
|
-
function handle_change_element(new_element) {
|
|
838
|
-
if (!structure?.sites || selected_sites.length === 0)
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
push_undo();
|
|
845
|
-
const orig_indices = scene_to_structure_indices(selected_sites);
|
|
1080
|
+
...structure,
|
|
1081
|
+
sites: structure.sites.map((site, idx) => {
|
|
1082
|
+
if (!orig_indices.has(idx)) return site
|
|
1083
|
+
const new_xyz: Vec3 = [
|
|
1084
|
+
site.xyz[0] + delta[0],
|
|
1085
|
+
site.xyz[1] + delta[1],
|
|
1086
|
+
site.xyz[2] + delta[2],
|
|
1087
|
+
]
|
|
1088
|
+
if (!cart_to_frac || !frac_to_cart) {
|
|
1089
|
+
return { ...site, xyz: new_xyz, abc: new_xyz }
|
|
1090
|
+
}
|
|
1091
|
+
const wrapped_abc = wrap_to_unit_cell(cart_to_frac(new_xyz))
|
|
1092
|
+
return { ...site, xyz: frac_to_cart(wrapped_abc), abc: wrapped_abc }
|
|
1093
|
+
}),
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// Change element symbol of selected atoms
|
|
1098
|
+
function handle_change_element(new_element: string) {
|
|
1099
|
+
if (!structure?.sites || selected_sites.length === 0) return
|
|
1100
|
+
const elem = normalize_element(new_element)
|
|
1101
|
+
if (!elem) return
|
|
1102
|
+
is_internal_edit = true
|
|
1103
|
+
push_undo()
|
|
1104
|
+
const orig_indices = scene_to_structure_indices(selected_sites)
|
|
846
1105
|
structure = {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1106
|
+
...structure,
|
|
1107
|
+
sites: structure.sites.map((site, idx) => {
|
|
1108
|
+
if (!orig_indices.has(idx)) return site
|
|
1109
|
+
return {
|
|
1110
|
+
...site,
|
|
1111
|
+
species: [{ element: elem, occu: 1, oxidation_state: 0 }],
|
|
1112
|
+
label: elem,
|
|
1113
|
+
}
|
|
1114
|
+
}),
|
|
1115
|
+
}
|
|
1116
|
+
change_element_mode = false
|
|
1117
|
+
change_element_value = ``
|
|
1118
|
+
show_toast(
|
|
1119
|
+
`Changed ${orig_indices.size} site${
|
|
1120
|
+
orig_indices.size > 1 ? `s` : ``
|
|
1121
|
+
} to ${elem}`,
|
|
1122
|
+
)
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Handle add-atom from StructureScene click-to-place
|
|
1126
|
+
function handle_add_atom(xyz: Vec3, element: ElementSymbol) {
|
|
1127
|
+
if (!structure) return
|
|
1128
|
+
const elem = normalize_element(element)
|
|
867
1129
|
if (!elem) {
|
|
868
|
-
|
|
1130
|
+
return console.warn(`Invalid element symbol "${element}", ignoring add-atom`)
|
|
869
1131
|
}
|
|
870
|
-
is_internal_edit = true
|
|
871
|
-
push_undo()
|
|
1132
|
+
is_internal_edit = true
|
|
1133
|
+
push_undo()
|
|
872
1134
|
structure = {
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
};
|
|
882
|
-
show_toast(`Added ${elem} at (${xyz.map((c) => c.toFixed(2)).join(`, `)})`);
|
|
883
|
-
}
|
|
884
|
-
// Only set background override when background_color is explicitly provided
|
|
885
|
-
$effect(() => {
|
|
886
|
-
if (typeof window !== `undefined` && wrapper && background_color) {
|
|
887
|
-
// Convert opacity (0-1) to hex alpha value (00-FF)
|
|
888
|
-
const alpha_hex = Math.round(background_opacity * 255)
|
|
889
|
-
.toString(16)
|
|
890
|
-
.padStart(2, `0`);
|
|
891
|
-
wrapper.style.setProperty(`--struct-bg-override`, `${background_color}${alpha_hex}`);
|
|
1135
|
+
...structure,
|
|
1136
|
+
sites: [...structure.sites, {
|
|
1137
|
+
species: [{ element: elem, occu: 1, oxidation_state: 0 }],
|
|
1138
|
+
xyz,
|
|
1139
|
+
abc: get_cart_to_frac()?.(xyz) ?? xyz,
|
|
1140
|
+
label: elem,
|
|
1141
|
+
properties: {},
|
|
1142
|
+
}],
|
|
892
1143
|
}
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1144
|
+
show_toast(`Added ${elem} at (${xyz.map((c) => c.toFixed(2)).join(`, `)})`)
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Only set background override when background_color is explicitly provided
|
|
1148
|
+
$effect(() => {
|
|
1149
|
+
if (typeof window !== `undefined` && wrapper && background_color) {
|
|
1150
|
+
// Convert opacity (0-1) to hex alpha value (00-FF)
|
|
1151
|
+
const alpha_hex = Math.round(background_opacity * 255)
|
|
1152
|
+
.toString(16)
|
|
1153
|
+
.padStart(2, `0`)
|
|
1154
|
+
wrapper.style.setProperty(
|
|
1155
|
+
`--struct-bg-override`,
|
|
1156
|
+
`${background_color}${alpha_hex}`,
|
|
1157
|
+
)
|
|
1158
|
+
} else if (typeof window !== `undefined` && wrapper) {
|
|
1159
|
+
// Remove override to use theme system
|
|
1160
|
+
wrapper.style.removeProperty(`--struct-bg-override`)
|
|
896
1161
|
}
|
|
897
|
-
})
|
|
898
|
-
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
$effect(() => { // fullscreen and background
|
|
899
1165
|
if (typeof window !== `undefined`) {
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
}
|
|
1166
|
+
if (fullscreen && !document.fullscreenElement && wrapper) {
|
|
1167
|
+
wrapper.requestFullscreen().catch(console.error)
|
|
1168
|
+
} else if (!fullscreen && document.fullscreenElement) {
|
|
1169
|
+
document.exitFullscreen()
|
|
1170
|
+
}
|
|
906
1171
|
}
|
|
907
|
-
set_fullscreen_bg(wrapper, fullscreen, `--struct-bg-fullscreen`)
|
|
908
|
-
})
|
|
1172
|
+
set_fullscreen_bg(wrapper, fullscreen, `--struct-bg-fullscreen`)
|
|
1173
|
+
})
|
|
909
1174
|
</script>
|
|
910
1175
|
|
|
911
1176
|
<svelte:document
|
|
@@ -1071,7 +1336,7 @@ $effect(() => {
|
|
|
1071
1336
|
onclick={() => [measure_mode, measure_menu_open] = [mode, false]}
|
|
1072
1337
|
>
|
|
1073
1338
|
<Icon {icon} style="transform: scale({scale})" />
|
|
1074
|
-
<span>{@html label}</span>
|
|
1339
|
+
<span>{@html sanitize_html(label)}</span>
|
|
1075
1340
|
</button>
|
|
1076
1341
|
{/each}
|
|
1077
1342
|
</div>
|
|
@@ -1232,14 +1497,14 @@ $effect(() => {
|
|
|
1232
1497
|
<!-- prevent from rendering in vitest runner since WebGLRenderingContext not available -->
|
|
1233
1498
|
{#if typeof WebGLRenderingContext !== `undefined`}
|
|
1234
1499
|
<!-- prevent HTML labels from rendering outside of the canvas -->
|
|
1235
|
-
<div style="overflow: hidden; height: 100%;
|
|
1500
|
+
<div style="overflow: hidden; height: 100%; width: 100%">
|
|
1236
1501
|
<Canvas>
|
|
1237
1502
|
<StructureScene
|
|
1238
1503
|
structure={displayed_structure}
|
|
1239
1504
|
base_structure={cell_transformed_structure}
|
|
1240
1505
|
{...scene_props}
|
|
1241
1506
|
{lattice_props}
|
|
1242
|
-
volumetric_data={
|
|
1507
|
+
volumetric_data={supercell_volume}
|
|
1243
1508
|
{isosurface_settings}
|
|
1244
1509
|
bind:camera_is_moving
|
|
1245
1510
|
bind:selected_sites
|
|
@@ -1372,7 +1637,8 @@ $effect(() => {
|
|
|
1372
1637
|
pointer-events: auto;
|
|
1373
1638
|
}
|
|
1374
1639
|
/* Mode: hover - controls visible on component hover */
|
|
1375
|
-
.structure:hover section.control-buttons.hover-visible
|
|
1640
|
+
.structure:hover section.control-buttons.hover-visible,
|
|
1641
|
+
.structure:focus-within section.control-buttons.hover-visible {
|
|
1376
1642
|
opacity: 1;
|
|
1377
1643
|
pointer-events: auto;
|
|
1378
1644
|
}
|
|
@@ -1380,7 +1646,7 @@ $effect(() => {
|
|
|
1380
1646
|
section.control-buttons > :global(button) {
|
|
1381
1647
|
background-color: transparent;
|
|
1382
1648
|
display: flex;
|
|
1383
|
-
padding:
|
|
1649
|
+
padding: 1px 6px;
|
|
1384
1650
|
border-radius: var(--border-radius, 3pt);
|
|
1385
1651
|
font-size: clamp(0.85em, 2cqmin, 1.3em);
|
|
1386
1652
|
}
|
|
@@ -1432,7 +1698,7 @@ $effect(() => {
|
|
|
1432
1698
|
}
|
|
1433
1699
|
.measure-mode-dropdown > button {
|
|
1434
1700
|
background: transparent;
|
|
1435
|
-
padding:
|
|
1701
|
+
padding: 1px 6px;
|
|
1436
1702
|
font-size: clamp(0.85em, 2cqmin, 1.3em);
|
|
1437
1703
|
}
|
|
1438
1704
|
.selection-limit-text {
|