matterviz 0.3.1 → 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 +154 -96
- package/dist/Icon.svelte +20 -14
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -178
- package/dist/brillouin/BrillouinZone.svelte +299 -198
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
- 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 +327 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- 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.d.ts +10 -0
- package/dist/chempot-diagram/color.js +32 -0
- package/dist/chempot-diagram/compute.d.ts +48 -0
- package/dist/chempot-diagram/compute.js +812 -0
- package/dist/chempot-diagram/index.d.ts +6 -0
- package/dist/chempot-diagram/index.js +6 -0
- package/dist/chempot-diagram/pointer.d.ts +16 -0
- package/dist/chempot-diagram/pointer.js +40 -0
- package/dist/chempot-diagram/temperature.d.ts +15 -0
- package/dist/chempot-diagram/temperature.js +36 -0
- package/dist/chempot-diagram/types.d.ts +86 -0
- package/dist/chempot-diagram/types.js +28 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +9 -3
- package/dist/composition/BarChart.svelte +141 -77
- package/dist/composition/BubbleChart.svelte +107 -52
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +973 -353
- package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
- package/dist/composition/PieChart.svelte +199 -99
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- 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 -38
- package/dist/convex-hull/ConvexHull2D.svelte +551 -393
- package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
- package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +821 -249
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +41 -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.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +40 -0
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +10 -1
- package/dist/convex-hull/helpers.js +79 -38
- package/dist/convex-hull/index.d.ts +1 -0
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +8 -21
- package/dist/convex-hull/thermodynamics.js +163 -69
- package/dist/convex-hull/types.d.ts +12 -12
- package/dist/convex-hull/types.js +0 -12
- package/dist/coordination/CoordinationBarPlot.svelte +232 -176
- package/dist/element/BohrAtom.svelte +56 -13
- 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 +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/types.d.ts +1 -0
- 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 +336 -239
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
- 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 +37 -33
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
- package/dist/heatmap-matrix/index.d.ts +53 -0
- package/dist/heatmap-matrix/index.js +100 -0
- package/dist/heatmap-matrix/shared.d.ts +2 -0
- package/dist/heatmap-matrix/shared.js +4 -0
- package/dist/icons.d.ts +111 -0
- package/dist/icons.js +158 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +138 -140
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +43 -0
- package/dist/io/index.d.ts +2 -2
- package/dist/io/index.js +2 -112
- package/dist/io/is-binary.js +2 -3
- package/dist/io/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +117 -0
- package/dist/isosurface/Isosurface.svelte +220 -110
- package/dist/isosurface/IsosurfaceControls.svelte +65 -28
- package/dist/isosurface/parse.js +104 -56
- package/dist/isosurface/slice.d.ts +2 -1
- package/dist/isosurface/slice.js +8 -13
- package/dist/isosurface/types.d.ts +14 -1
- package/dist/isosurface/types.js +152 -5
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +12 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +125 -94
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +82 -0
- package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
- package/dist/layout/index.d.ts +1 -0
- package/dist/layout/index.js +1 -0
- package/dist/layout/json-tree/JsonNode.svelte +266 -223
- package/dist/layout/json-tree/JsonTree.svelte +516 -429
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +281 -173
- package/dist/layout/json-tree/types.d.ts +10 -2
- package/dist/layout/json-tree/utils.d.ts +2 -0
- package/dist/layout/json-tree/utils.js +37 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +20 -17
- package/dist/math.js +474 -57
- package/dist/overlays/ContextMenu.svelte +66 -40
- package/dist/overlays/DraggablePane.svelte +331 -154
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- 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 +559 -267
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- 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/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +869 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +8 -4
- package/dist/phase-diagram/utils.js +219 -74
- package/dist/plot/AxisLabel.svelte +51 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +1461 -768
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +33 -6
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +533 -383
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ColorScaleSelect.svelte +28 -7
- package/dist/plot/ElementScatter.svelte +38 -16
- package/dist/plot/FillArea.svelte +152 -92
- package/dist/plot/Histogram.svelte +1162 -709
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +81 -18
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- 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 +221 -96
- 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 -146
- package/dist/plot/ReferenceLine.svelte +77 -22
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -123
- package/dist/plot/ScatterPlot.svelte +1880 -1156
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +300 -297
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -406
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +150 -70
- 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 +96 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +23 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- 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/index.d.ts +6 -2
- package/dist/plot/index.js +6 -2
- package/dist/plot/interactions.d.ts +8 -10
- package/dist/plot/interactions.js +2 -3
- package/dist/plot/layout.d.ts +11 -2
- package/dist/plot/layout.js +44 -17
- package/dist/plot/reference-line.d.ts +5 -22
- package/dist/plot/reference-line.js +12 -84
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +53 -40
- package/dist/plot/types.js +12 -7
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -63
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +173 -132
- 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 +21 -6
- package/dist/settings.js +63 -19
- package/dist/spectral/Bands.svelte +963 -412
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- 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.d.ts +23 -1
- package/dist/spectral/helpers.js +119 -51
- package/dist/spectral/types.d.ts +2 -0
- 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 +231 -129
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +148 -51
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1077 -821
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +373 -139
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -231
- package/dist/structure/StructureScene.svelte +919 -445
- package/dist/structure/StructureScene.svelte.d.ts +16 -7
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +42 -29
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +22 -34
- package/dist/structure/ferrox-wasm-types.d.ts +3 -2
- package/dist/structure/ferrox-wasm-types.js +0 -3
- package/dist/structure/ferrox-wasm.d.ts +3 -2
- package/dist/structure/ferrox-wasm.js +2 -3
- package/dist/structure/index.d.ts +16 -0
- package/dist/structure/index.js +88 -6
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +130 -155
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +99 -0
- 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 +5 -3
- package/dist/symmetry/SymmetryStats.svelte +94 -37
- package/dist/symmetry/WyckoffTable.svelte +42 -14
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +87 -21
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +1112 -516
- package/dist/table/HeatmapTable.svelte.d.ts +12 -1
- package/dist/table/ToggleMenu.svelte +125 -90
- package/dist/table/index.d.ts +2 -0
- 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 +889 -687
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +13 -31
- package/dist/trajectory/format-detect.d.ts +9 -0
- package/dist/trajectory/format-detect.js +76 -0
- package/dist/trajectory/frame-reader.d.ts +17 -0
- package/dist/trajectory/frame-reader.js +332 -0
- package/dist/trajectory/helpers.d.ts +14 -0
- package/dist/trajectory/helpers.js +172 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +23 -14
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +77 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +129 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +299 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +179 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +68 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +110 -0
- package/dist/trajectory/plotting.js +13 -8
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +3 -0
- package/dist/utils.js +17 -0
- package/dist/xrd/XrdPlot.svelte +337 -245
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -19
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +103 -101
- package/readme.md +4 -4
- package/dist/trajectory/parse.d.ts +0 -42
- package/dist/trajectory/parse.js +0 -1267
- /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,892 +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
|
-
|
|
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({
|
|
35
72
|
cell_edge_opacity: DEFAULTS.structure.cell_edge_opacity,
|
|
36
73
|
cell_surface_opacity: DEFAULTS.structure.cell_surface_opacity,
|
|
37
74
|
cell_edge_color: DEFAULTS.structure.cell_edge_color,
|
|
38
75
|
cell_surface_color: DEFAULTS.structure.cell_surface_color,
|
|
39
76
|
cell_edge_width: DEFAULTS.structure.cell_edge_width,
|
|
40
77
|
show_cell_vectors: DEFAULTS.structure.show_cell_vectors,
|
|
41
|
-
})
|
|
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
|
-
$
|
|
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(() => {
|
|
80
245
|
if (scene_props_in && typeof scene_props_in === `object`) {
|
|
81
|
-
|
|
246
|
+
Object.assign(scene_props, scene_props_in)
|
|
82
247
|
}
|
|
83
248
|
if (lattice_props_in && typeof lattice_props_in === `object`) {
|
|
84
|
-
|
|
249
|
+
Object.assign(lattice_props, lattice_props_in)
|
|
85
250
|
}
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
// Load structure from URL when data_url is provided
|
|
254
|
+
$effect(() => {
|
|
89
255
|
if (data_url && !structure) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
else {
|
|
96
|
-
// Parse structure internally when no handler provided
|
|
97
|
-
try {
|
|
98
|
-
const text_content = content instanceof ArrayBuffer
|
|
99
|
-
? new TextDecoder().decode(content)
|
|
100
|
-
: content;
|
|
101
|
-
const parsed = parse_file_content(text_content, filename);
|
|
102
|
-
emit_file_load_event(parsed, filename, content);
|
|
103
|
-
}
|
|
104
|
-
catch (error) {
|
|
105
|
-
error_msg = `Failed to parse structure: ${error instanceof Error ? error.message : String(error)}`;
|
|
106
|
-
on_error?.({ error_msg, filename });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
.then(() => loading = false)
|
|
111
|
-
.catch((error) => {
|
|
112
|
-
console.error(`Failed to load structure from URL:`, error);
|
|
113
|
-
error_msg = `Failed to load structure: ${error.message}`;
|
|
114
|
-
loading = false;
|
|
115
|
-
on_error?.({ error_msg, filename: data_url });
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
$effect(() => {
|
|
120
|
-
if (!structure_string || data_url)
|
|
121
|
-
return;
|
|
122
|
-
loading = true;
|
|
123
|
-
error_msg = undefined;
|
|
124
|
-
try {
|
|
125
|
-
const parsed = parse_any_structure(structure_string, `string`);
|
|
126
|
-
if (parsed) {
|
|
127
|
-
structure = parsed;
|
|
128
|
-
untrack(() => emit_file_load_event(parsed, `string`, structure_string));
|
|
129
|
-
}
|
|
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)
|
|
130
261
|
else {
|
|
131
|
-
|
|
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
|
+
}
|
|
132
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
|
+
})
|
|
133
284
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
140
307
|
}
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
|
155
326
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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(() => {
|
|
159
342
|
if (structure?.sites && performance_mode === `speed`) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
350
|
}
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
let
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
|
181
376
|
if (!structure || !(`lattice` in structure)) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
377
|
+
untrack(() => {
|
|
378
|
+
sym_data = null
|
|
379
|
+
symmetry_error = undefined
|
|
380
|
+
})
|
|
381
|
+
last_symmetry_structure_ref = null
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const current_structure = structure
|
|
386
|
+
const structure_changed = current_structure !== last_symmetry_structure_ref
|
|
387
|
+
if (structure_changed) {
|
|
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)
|
|
187
397
|
}
|
|
188
|
-
const
|
|
189
|
-
const run_id = ++symmetry_run_id;
|
|
398
|
+
const run_id = ++symmetry_run_id
|
|
190
399
|
// Destructure symmetry_settings to ensure Svelte tracks changes to symprec and algo
|
|
191
400
|
// (reading just the object reference isn't sufficient for fine-grained reactivity)
|
|
192
|
-
const { symprec, algo } = symmetry_settings ?? symmetry.default_sym_settings
|
|
193
|
-
const current_settings = { symprec, algo }
|
|
194
|
-
//
|
|
195
|
-
|
|
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
|
+
|
|
196
406
|
symmetry.ensure_moyo_wasm_ready()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
407
|
+
.then(() =>
|
|
408
|
+
run_id === symmetry_run_id
|
|
409
|
+
? symmetry.analyze_structure_symmetry(current_structure, current_settings)
|
|
410
|
+
: null
|
|
411
|
+
)
|
|
412
|
+
.then((data) => {
|
|
201
413
|
if (data && run_id === symmetry_run_id) {
|
|
202
|
-
|
|
414
|
+
untrack(() => sym_data = data)
|
|
203
415
|
}
|
|
204
|
-
|
|
205
|
-
|
|
416
|
+
})
|
|
417
|
+
.catch((err) => {
|
|
206
418
|
if (run_id === symmetry_run_id) {
|
|
207
|
-
|
|
208
|
-
|
|
419
|
+
untrack(() => sym_data = null)
|
|
420
|
+
symmetry_error = `Symmetry analysis failed: ${err?.message || err}`
|
|
421
|
+
console.error(`Symmetry analysis failed:`, err)
|
|
209
422
|
}
|
|
210
|
-
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
let
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
let
|
|
228
|
-
|
|
229
|
-
let
|
|
230
|
-
let
|
|
231
|
-
|
|
232
|
-
let
|
|
233
|
-
let
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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 {
|
|
241
458
|
const normalized = (input.charAt(0).toUpperCase() +
|
|
242
|
-
|
|
243
|
-
return ELEM_SYMBOLS.includes(normalized) ? normalized : null
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
|
253
471
|
if (undo_stack.length >= MAX_HISTORY) {
|
|
254
|
-
|
|
472
|
+
undo_stack.splice(0, undo_stack.length - MAX_HISTORY + 1)
|
|
255
473
|
}
|
|
256
|
-
undo_stack.push($state.snapshot(structure))
|
|
257
|
-
redo_stack.length = 0
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
const restored = source.pop()
|
|
264
|
-
if (!restored)
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const undo = () => apply_history(undo_stack, redo_stack)
|
|
272
|
-
const redo = () => apply_history(redo_stack, undo_stack)
|
|
273
|
-
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
$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(() => {
|
|
277
496
|
// Track structure to re-run when it changes
|
|
278
|
-
void structure
|
|
497
|
+
void structure
|
|
279
498
|
if (is_internal_edit) {
|
|
280
|
-
|
|
281
|
-
|
|
499
|
+
is_internal_edit = false
|
|
500
|
+
return
|
|
282
501
|
}
|
|
283
502
|
// External change — clear history and stale edit-atoms state
|
|
284
503
|
untrack(() => {
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
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
|
|
301
519
|
if (mode_first_run) {
|
|
302
|
-
|
|
303
|
-
|
|
520
|
+
mode_first_run = false
|
|
521
|
+
return
|
|
304
522
|
}
|
|
305
523
|
untrack(() => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
// Auto-bake cell type transform and clear stale state when entering edit-atoms mode
|
|
311
|
-
$effect(() => {
|
|
312
|
-
if (measure_mode !== `edit-atoms`)
|
|
313
|
-
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
|
|
314
531
|
untrack(() => {
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
})
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
|
343
564
|
}
|
|
344
565
|
// Cell type transformation requires symmetry data
|
|
345
566
|
if (!sym_data) {
|
|
346
|
-
|
|
567
|
+
return normalized_structure
|
|
347
568
|
}
|
|
348
569
|
try {
|
|
349
|
-
|
|
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
|
|
350
574
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
354
594
|
}
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
let
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
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)
|
|
362
601
|
if (!base_structure || !(`lattice` in base_structure) || !has_supercell) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
if (base_structure && `lattice` in base_structure) {
|
|
385
|
-
supercell_structure = make_supercell(base_structure, supercell_scaling);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
catch (error) {
|
|
389
|
-
console.error(`Failed to create supercell:`, error);
|
|
390
|
-
supercell_structure = base_structure;
|
|
391
|
-
}
|
|
392
|
-
finally {
|
|
393
|
-
supercell_loading = false;
|
|
394
|
-
}
|
|
395
|
-
}, 10);
|
|
396
|
-
}
|
|
397
|
-
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 {
|
|
398
623
|
if (base_structure && `lattice` in base_structure) {
|
|
399
|
-
|
|
624
|
+
supercell_structure = make_supercell(
|
|
625
|
+
base_structure as Crystal,
|
|
626
|
+
supercell_scaling,
|
|
627
|
+
)
|
|
400
628
|
}
|
|
401
|
-
|
|
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
|
+
)
|
|
402
642
|
}
|
|
643
|
+
supercell_loading = false
|
|
644
|
+
}
|
|
403
645
|
}
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
[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
|
|
412
653
|
if (first_run) {
|
|
413
|
-
|
|
414
|
-
|
|
654
|
+
first_run = false
|
|
655
|
+
return
|
|
415
656
|
}
|
|
416
657
|
untrack(() => {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
// Apply element mapping then image atoms to the supercell structure.
|
|
429
|
-
// Skip get_pbc_image_sites during atom drags — the vector math + doubled site
|
|
430
|
-
// count causes frame drops. Image atoms reappear instantly on drag release.
|
|
431
|
-
$effect(() => {
|
|
432
|
-
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
|
|
433
674
|
if (struct && element_mapping && Object.keys(element_mapping).length > 0) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
+
}
|
|
446
687
|
}
|
|
447
688
|
displayed_structure =
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
})
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
let
|
|
456
|
-
let
|
|
457
|
-
let
|
|
458
|
-
let
|
|
459
|
-
let
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
$
|
|
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(() => {
|
|
464
708
|
if (info_pane_open) {
|
|
465
|
-
|
|
709
|
+
untrack(() => [controls_open, export_pane_open] = [false, false])
|
|
466
710
|
}
|
|
467
|
-
})
|
|
468
|
-
$effect(() => {
|
|
711
|
+
})
|
|
712
|
+
$effect(() => {
|
|
469
713
|
if (controls_open) {
|
|
470
|
-
|
|
714
|
+
untrack(() => [info_pane_open, export_pane_open] = [false, false])
|
|
471
715
|
}
|
|
472
|
-
})
|
|
473
|
-
$effect(() => {
|
|
716
|
+
})
|
|
717
|
+
$effect(() => {
|
|
474
718
|
if (export_pane_open) {
|
|
475
|
-
|
|
719
|
+
untrack(() => [info_pane_open, controls_open] = [false, false])
|
|
476
720
|
}
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
})
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
|
|
751
|
+
const emit_camera_move = () => {
|
|
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
|
+
})
|
|
494
763
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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() {
|
|
771
|
+
// Reset camera position to trigger automatic positioning.
|
|
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
|
+
|
|
779
|
+
// Reset pan/zoom and ensure controls target returns to structure center.
|
|
501
780
|
if (orbit_controls && camera) {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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()
|
|
517
801
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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`
|
|
524
817
|
? new Blob([content]).size
|
|
525
818
|
: content.byteLength,
|
|
526
|
-
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
//
|
|
530
|
-
//
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
535
831
|
// parse_volumetric_file extracts structure from file header;
|
|
536
832
|
// parsers set pbc so the lattice conforms to Crystal's LatticeType
|
|
537
|
-
structure = vol_result.structure
|
|
538
|
-
volumetric_data = vol_result.volumes
|
|
833
|
+
structure = vol_result.structure as AnyStructure
|
|
834
|
+
volumetric_data = vol_result.volumes
|
|
539
835
|
// Auto-compute reasonable isosurface settings from data range
|
|
540
|
-
const vol = vol_result.volumes[0]
|
|
836
|
+
const vol = vol_result.volumes[0]
|
|
541
837
|
if (vol) {
|
|
542
|
-
|
|
543
|
-
|
|
838
|
+
isosurface_settings = auto_isosurface_settings(vol.data_range)
|
|
839
|
+
active_volume_idx = 0
|
|
544
840
|
}
|
|
545
|
-
return structure
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
|
553
850
|
// Clear stale volumetric data when loading a non-volumetric file
|
|
554
|
-
volumetric_data = []
|
|
555
|
-
const parsed = parse_any_structure(text_content, filename)
|
|
556
|
-
if (!parsed)
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
const { content, filename } = await decompress_file(file);
|
|
590
|
-
if (content) {
|
|
591
|
-
if (on_file_drop)
|
|
592
|
-
on_file_drop(content, filename);
|
|
593
|
-
else {
|
|
594
|
-
// Parse structure internally when no handler provided
|
|
595
|
-
try {
|
|
596
|
-
const parsed = parse_file_content(content, filename);
|
|
597
|
-
emit_file_load_event(parsed, filename, content);
|
|
598
|
-
}
|
|
599
|
-
catch (err) {
|
|
600
|
-
error_msg = `Failed to parse structure: ${err}`;
|
|
601
|
-
on_error?.({ error_msg, filename });
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
catch (error) {
|
|
607
|
-
error_msg = `Failed to load file ${file.name}: ${error}`;
|
|
608
|
-
on_error?.({ error_msg, filename: file.name });
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
finally {
|
|
613
|
-
loading = false;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
function handle_keydown(event) {
|
|
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({
|
|
859
|
+
allow: () => allow_file_drop,
|
|
860
|
+
on_drop: (content, filename) => {
|
|
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
|
+
}
|
|
874
|
+
},
|
|
875
|
+
on_error: (msg) => {
|
|
876
|
+
error_msg = msg
|
|
877
|
+
on_error?.({ error_msg: msg })
|
|
878
|
+
},
|
|
879
|
+
set_loading: (val) => {
|
|
880
|
+
loading = val
|
|
881
|
+
if (val) [error_msg, dragover] = [undefined, false]
|
|
882
|
+
},
|
|
883
|
+
})
|
|
884
|
+
|
|
885
|
+
function handle_keydown(event: KeyboardEvent) {
|
|
617
886
|
// Don't handle shortcuts if user is typing in an input field
|
|
618
|
-
const target = event.target
|
|
887
|
+
const target = event.target as HTMLElement
|
|
619
888
|
const is_input_focused = target.tagName === `INPUT` ||
|
|
620
|
-
|
|
889
|
+
target.tagName === `TEXTAREA`
|
|
890
|
+
|
|
621
891
|
// Allow Escape to cancel add-atom mode even when the element input is focused
|
|
622
892
|
if (event.key === `Escape` && measure_mode === `edit-atoms` && add_atom_mode) {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
893
|
+
event.preventDefault()
|
|
894
|
+
add_atom_mode = false
|
|
895
|
+
return
|
|
626
896
|
}
|
|
627
|
-
|
|
628
|
-
|
|
897
|
+
|
|
898
|
+
if (is_input_focused) return
|
|
899
|
+
|
|
629
900
|
// Edit-atoms mode shortcuts (including undo/redo)
|
|
630
901
|
if (measure_mode === `edit-atoms`) {
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
if (event.key === `Delete` || event.key === `Backspace`) {
|
|
648
|
-
// Delete selected atoms
|
|
649
|
-
if (selected_sites.length > 0 && structure?.sites) {
|
|
650
|
-
event.preventDefault();
|
|
651
|
-
is_internal_edit = true;
|
|
652
|
-
push_undo();
|
|
653
|
-
const to_delete = scene_to_structure_indices(selected_sites, true);
|
|
654
|
-
const n_deleted = to_delete.size;
|
|
655
|
-
clear_selection();
|
|
656
|
-
structure = {
|
|
657
|
-
...structure,
|
|
658
|
-
sites: structure.sites.filter((_, idx) => !to_delete.has(idx)),
|
|
659
|
-
};
|
|
660
|
-
// Clear per-site overrides since indices shifted after deletion
|
|
661
|
-
if (site_radius_overrides?.size > 0)
|
|
662
|
-
site_radius_overrides.clear();
|
|
663
|
-
added_bonds = [];
|
|
664
|
-
removed_bonds = [];
|
|
665
|
-
show_toast(`Deleted ${n_deleted} site${n_deleted > 1 ? `s` : ``}`);
|
|
666
|
-
}
|
|
667
|
-
return;
|
|
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
|
|
668
915
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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` : ``}`)
|
|
676
936
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
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],
|
|
682
983
|
}
|
|
683
|
-
//
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
site.xyz[2] + 0.5,
|
|
698
|
-
];
|
|
699
|
-
return {
|
|
700
|
-
...site,
|
|
701
|
-
xyz: new_xyz,
|
|
702
|
-
abc: cart_to_frac?.(new_xyz) ?? new_xyz,
|
|
703
|
-
properties: { ...site.properties },
|
|
704
|
-
};
|
|
705
|
-
});
|
|
706
|
-
const base_idx = structure.sites.length;
|
|
707
|
-
structure = {
|
|
708
|
-
...structure,
|
|
709
|
-
sites: [...structure.sites, ...new_sites],
|
|
710
|
-
};
|
|
711
|
-
// Select the newly duplicated atoms
|
|
712
|
-
selected_sites = new_sites.map((_, idx) => base_idx + idx);
|
|
713
|
-
measured_sites = [...selected_sites];
|
|
714
|
-
show_toast(`Duplicated ${new_sites.length} site${new_sites.length > 1 ? `s` : ``}`);
|
|
715
|
-
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
|
|
716
998
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
change_element_mode = false;
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
if (selected_sites.length > 0) {
|
|
724
|
-
clear_selection();
|
|
725
|
-
return;
|
|
726
|
-
}
|
|
999
|
+
if (selected_sites.length > 0) {
|
|
1000
|
+
clear_selection()
|
|
1001
|
+
return
|
|
727
1002
|
}
|
|
1003
|
+
}
|
|
728
1004
|
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1005
|
+
|
|
1006
|
+
// Interface shortcuts (require Ctrl/Cmd modifier to avoid accidental triggers)
|
|
1007
|
+
const has_modifier = event.ctrlKey || event.metaKey
|
|
1008
|
+
if (event.key === `f` && has_modifier && fullscreen_toggle) {
|
|
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
|
+
}
|
|
745
1022
|
}
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
//
|
|
751
|
-
|
|
752
|
-
|
|
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>()
|
|
753
1035
|
for (const scene_idx of scene_indices) {
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
result.add(scene_idx);
|
|
769
|
-
}
|
|
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
|
+
}
|
|
770
1050
|
}
|
|
771
|
-
return result
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
|
777
1057
|
try {
|
|
778
|
-
|
|
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
|
|
779
1062
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
return;
|
|
790
|
-
is_internal_edit = true;
|
|
791
|
-
const orig_indices = scene_to_structure_indices(scene_indices);
|
|
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)
|
|
792
1072
|
// For crystals, wrap to [0,1) inline so normalize_fractional_coords fast-paths.
|
|
793
1073
|
// For molecules (no lattice), just apply the Cartesian delta directly.
|
|
794
1074
|
const lattice = `lattice` in structure
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
const cart_to_frac = lattice ? create_cart_to_frac(lattice) : null
|
|
798
|
-
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
|
|
799
1079
|
structure = {
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
// Change element symbol of selected atoms
|
|
818
|
-
function handle_change_element(new_element) {
|
|
819
|
-
if (!structure?.sites || selected_sites.length === 0)
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
push_undo();
|
|
826
|
-
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)
|
|
827
1105
|
structure = {
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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)
|
|
848
1129
|
if (!elem) {
|
|
849
|
-
|
|
1130
|
+
return console.warn(`Invalid element symbol "${element}", ignoring add-atom`)
|
|
850
1131
|
}
|
|
851
|
-
is_internal_edit = true
|
|
852
|
-
push_undo()
|
|
1132
|
+
is_internal_edit = true
|
|
1133
|
+
push_undo()
|
|
853
1134
|
structure = {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
};
|
|
863
|
-
show_toast(`Added ${elem} at (${xyz.map((c) => c.toFixed(2)).join(`, `)})`);
|
|
864
|
-
}
|
|
865
|
-
// Only set background override when background_color is explicitly provided
|
|
866
|
-
$effect(() => {
|
|
867
|
-
if (typeof window !== `undefined` && wrapper && background_color) {
|
|
868
|
-
// Convert opacity (0-1) to hex alpha value (00-FF)
|
|
869
|
-
const alpha_hex = Math.round(background_opacity * 255)
|
|
870
|
-
.toString(16)
|
|
871
|
-
.padStart(2, `0`);
|
|
872
|
-
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
|
+
}],
|
|
873
1143
|
}
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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`)
|
|
877
1161
|
}
|
|
878
|
-
})
|
|
879
|
-
|
|
1162
|
+
})
|
|
1163
|
+
|
|
1164
|
+
$effect(() => { // fullscreen and background
|
|
880
1165
|
if (typeof window !== `undefined`) {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
}
|
|
1166
|
+
if (fullscreen && !document.fullscreenElement && wrapper) {
|
|
1167
|
+
wrapper.requestFullscreen().catch(console.error)
|
|
1168
|
+
} else if (!fullscreen && document.fullscreenElement) {
|
|
1169
|
+
document.exitFullscreen()
|
|
1170
|
+
}
|
|
887
1171
|
}
|
|
888
|
-
set_fullscreen_bg(wrapper, fullscreen, `--struct-bg-fullscreen`)
|
|
889
|
-
})
|
|
1172
|
+
set_fullscreen_bg(wrapper, fullscreen, `--struct-bg-fullscreen`)
|
|
1173
|
+
})
|
|
890
1174
|
</script>
|
|
891
1175
|
|
|
892
1176
|
<svelte:document
|
|
@@ -948,10 +1232,7 @@ $effect(() => {
|
|
|
948
1232
|
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
|
|
949
1233
|
/>
|
|
950
1234
|
{:else if error_msg}
|
|
951
|
-
<
|
|
952
|
-
<p class="error">{error_msg}</p>
|
|
953
|
-
<button onclick={() => (error_msg = undefined)}>Dismiss</button>
|
|
954
|
-
</div>
|
|
1235
|
+
<StatusMessage bind:message={error_msg} type="error" dismissible />
|
|
955
1236
|
{:else if (structure?.sites?.length ?? 0) > 0}
|
|
956
1237
|
<section
|
|
957
1238
|
class="control-buttons {controls_config.class}"
|
|
@@ -1055,7 +1336,7 @@ $effect(() => {
|
|
|
1055
1336
|
onclick={() => [measure_mode, measure_menu_open] = [mode, false]}
|
|
1056
1337
|
>
|
|
1057
1338
|
<Icon {icon} style="transform: scale({scale})" />
|
|
1058
|
-
<span>{@html label}</span>
|
|
1339
|
+
<span>{@html sanitize_html(label)}</span>
|
|
1059
1340
|
</button>
|
|
1060
1341
|
{/each}
|
|
1061
1342
|
</div>
|
|
@@ -1216,14 +1497,14 @@ $effect(() => {
|
|
|
1216
1497
|
<!-- prevent from rendering in vitest runner since WebGLRenderingContext not available -->
|
|
1217
1498
|
{#if typeof WebGLRenderingContext !== `undefined`}
|
|
1218
1499
|
<!-- prevent HTML labels from rendering outside of the canvas -->
|
|
1219
|
-
<div style="overflow: hidden; height: 100%;
|
|
1500
|
+
<div style="overflow: hidden; height: 100%; width: 100%">
|
|
1220
1501
|
<Canvas>
|
|
1221
1502
|
<StructureScene
|
|
1222
1503
|
structure={displayed_structure}
|
|
1223
1504
|
base_structure={cell_transformed_structure}
|
|
1224
1505
|
{...scene_props}
|
|
1225
1506
|
{lattice_props}
|
|
1226
|
-
volumetric_data={
|
|
1507
|
+
volumetric_data={supercell_volume}
|
|
1227
1508
|
{isosurface_settings}
|
|
1228
1509
|
bind:camera_is_moving
|
|
1229
1510
|
bind:selected_sites
|
|
@@ -1356,7 +1637,8 @@ $effect(() => {
|
|
|
1356
1637
|
pointer-events: auto;
|
|
1357
1638
|
}
|
|
1358
1639
|
/* Mode: hover - controls visible on component hover */
|
|
1359
|
-
.structure:hover section.control-buttons.hover-visible
|
|
1640
|
+
.structure:hover section.control-buttons.hover-visible,
|
|
1641
|
+
.structure:focus-within section.control-buttons.hover-visible {
|
|
1360
1642
|
opacity: 1;
|
|
1361
1643
|
pointer-events: auto;
|
|
1362
1644
|
}
|
|
@@ -1364,7 +1646,7 @@ $effect(() => {
|
|
|
1364
1646
|
section.control-buttons > :global(button) {
|
|
1365
1647
|
background-color: transparent;
|
|
1366
1648
|
display: flex;
|
|
1367
|
-
padding:
|
|
1649
|
+
padding: 1px 6px;
|
|
1368
1650
|
border-radius: var(--border-radius, 3pt);
|
|
1369
1651
|
font-size: clamp(0.85em, 2cqmin, 1.3em);
|
|
1370
1652
|
}
|
|
@@ -1416,7 +1698,7 @@ $effect(() => {
|
|
|
1416
1698
|
}
|
|
1417
1699
|
.measure-mode-dropdown > button {
|
|
1418
1700
|
background: transparent;
|
|
1419
|
-
padding:
|
|
1701
|
+
padding: 1px 6px;
|
|
1420
1702
|
font-size: clamp(0.85em, 2cqmin, 1.3em);
|
|
1421
1703
|
}
|
|
1422
1704
|
.selection-limit-text {
|
|
@@ -1432,32 +1714,6 @@ $effect(() => {
|
|
|
1432
1714
|
display: grid;
|
|
1433
1715
|
place-content: center;
|
|
1434
1716
|
}
|
|
1435
|
-
.error-state {
|
|
1436
|
-
display: flex;
|
|
1437
|
-
flex-direction: column;
|
|
1438
|
-
align-items: center;
|
|
1439
|
-
justify-content: center;
|
|
1440
|
-
height: var(--struct-height, 500px);
|
|
1441
|
-
padding: 2rem;
|
|
1442
|
-
text-align: center;
|
|
1443
|
-
box-sizing: border-box;
|
|
1444
|
-
}
|
|
1445
|
-
.error-state p {
|
|
1446
|
-
color: var(--error-color, #ff6b6b);
|
|
1447
|
-
margin: 0 0 1rem;
|
|
1448
|
-
}
|
|
1449
|
-
.error-state button {
|
|
1450
|
-
padding: 0.5rem 1rem;
|
|
1451
|
-
background: var(--error-color, #ff6b6b);
|
|
1452
|
-
color: white;
|
|
1453
|
-
border: none;
|
|
1454
|
-
border-radius: var(--border-radius, 3pt);
|
|
1455
|
-
cursor: pointer;
|
|
1456
|
-
font-size: 0.9rem;
|
|
1457
|
-
}
|
|
1458
|
-
.error-state button:hover {
|
|
1459
|
-
background: var(--error-color-hover, #ff5252);
|
|
1460
|
-
}
|
|
1461
1717
|
.symmetry-error {
|
|
1462
1718
|
position: absolute;
|
|
1463
1719
|
bottom: 1rem;
|