matterviz 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FilePicker.svelte +37 -20
- package/dist/Icon.svelte +2 -2
- package/dist/MillerIndexInput.svelte +60 -0
- package/dist/MillerIndexInput.svelte.d.ts +7 -0
- package/dist/app.css +38 -2
- package/dist/brillouin/BrillouinZone.svelte +20 -62
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
- package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
- package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
- package/dist/chempot-diagram/color.d.ts +10 -0
- package/dist/chempot-diagram/color.js +33 -0
- package/dist/chempot-diagram/compute.d.ts +38 -0
- package/dist/chempot-diagram/compute.js +650 -0
- package/dist/chempot-diagram/index.d.ts +5 -0
- package/dist/chempot-diagram/index.js +5 -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 +37 -0
- package/dist/chempot-diagram/types.d.ts +83 -0
- package/dist/chempot-diagram/types.js +27 -0
- package/dist/colors/index.d.ts +3 -1
- package/dist/colors/index.js +4 -0
- package/dist/composition/BarChart.svelte +13 -22
- package/dist/composition/BubbleChart.svelte +5 -3
- package/dist/composition/FormulaFilter.svelte +770 -90
- package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
- package/dist/composition/PieChart.svelte +43 -18
- package/dist/composition/PieChart.svelte.d.ts +1 -1
- package/dist/constants.d.ts +1 -0
- package/dist/constants.js +2 -0
- package/dist/convex-hull/ConvexHull.svelte +14 -1
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +14 -45
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +396 -134
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +93 -42
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +94 -31
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
- package/dist/convex-hull/ConvexHullStats.svelte +697 -128
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
- package/dist/convex-hull/GasPressureControls.svelte +72 -38
- package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
- package/dist/convex-hull/TemperatureSlider.svelte +46 -19
- package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
- package/dist/convex-hull/demo-temperature.d.ts +6 -0
- package/dist/convex-hull/demo-temperature.js +36 -0
- package/dist/convex-hull/gas-thermodynamics.js +16 -5
- package/dist/convex-hull/helpers.d.ts +7 -1
- package/dist/convex-hull/helpers.js +45 -15
- package/dist/convex-hull/index.d.ts +15 -1
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +8 -21
- package/dist/convex-hull/thermodynamics.js +106 -17
- package/dist/convex-hull/types.d.ts +7 -0
- package/dist/convex-hull/types.js +11 -0
- package/dist/coordination/CoordinationBarPlot.svelte +29 -46
- package/dist/element/BohrAtom.svelte +1 -1
- package/dist/element/data.js +2 -14
- package/dist/element/data.json.gz +0 -0
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -0
- package/dist/element/types.d.ts +1 -0
- package/dist/fermi-surface/FermiSurface.svelte +21 -65
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/compute.js +1 -21
- package/dist/fermi-surface/marching-cubes.d.ts +2 -13
- package/dist/fermi-surface/marching-cubes.js +2 -519
- package/dist/fermi-surface/parse.js +17 -23
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -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 +119 -0
- package/dist/icons.js +119 -0
- package/dist/index.d.ts +6 -1
- package/dist/index.js +6 -1
- package/dist/io/export.js +15 -3
- 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/types.d.ts +1 -0
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +118 -0
- package/dist/isosurface/Isosurface.svelte +231 -0
- package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
- package/dist/isosurface/IsosurfaceControls.svelte +273 -0
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
- package/dist/isosurface/index.d.ts +5 -0
- package/dist/isosurface/index.js +6 -0
- package/dist/isosurface/parse.d.ts +6 -0
- package/dist/isosurface/parse.js +548 -0
- package/dist/isosurface/slice.d.ts +11 -0
- package/dist/isosurface/slice.js +145 -0
- package/dist/isosurface/types.d.ts +55 -0
- package/dist/isosurface/types.js +178 -0
- package/dist/labels.d.ts +2 -1
- package/dist/labels.js +1 -0
- package/dist/layout/InfoTag.svelte +62 -62
- package/dist/layout/SubpageGrid.svelte +74 -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 +226 -53
- package/dist/layout/json-tree/JsonTree.svelte +425 -51
- package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
- package/dist/layout/json-tree/JsonValue.svelte +218 -97
- package/dist/layout/json-tree/types.d.ts +27 -2
- package/dist/layout/json-tree/utils.d.ts +14 -1
- package/dist/layout/json-tree/utils.js +254 -0
- package/dist/marching-cubes.d.ts +14 -0
- package/dist/marching-cubes.js +519 -0
- package/dist/math.d.ts +8 -0
- package/dist/math.js +374 -7
- package/dist/overlays/ContextMenu.svelte +3 -2
- package/dist/overlays/DraggablePane.svelte +163 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
- package/dist/phase-diagram/index.d.ts +2 -0
- package/dist/phase-diagram/index.js +2 -0
- package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
- package/dist/phase-diagram/svg-to-diagram.js +865 -0
- package/dist/phase-diagram/types.d.ts +10 -0
- package/dist/phase-diagram/utils.d.ts +7 -4
- package/dist/phase-diagram/utils.js +149 -59
- package/dist/plot/AxisLabel.svelte +26 -0
- package/dist/plot/AxisLabel.svelte.d.ts +16 -0
- package/dist/plot/BarPlot.svelte +473 -228
- package/dist/plot/BarPlot.svelte.d.ts +3 -3
- package/dist/plot/BarPlotControls.svelte +3 -2
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +54 -54
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/ElementScatter.svelte +4 -3
- package/dist/plot/FillArea.svelte +4 -1
- package/dist/plot/Histogram.svelte +320 -230
- package/dist/plot/Histogram.svelte.d.ts +2 -2
- package/dist/plot/HistogramControls.svelte +29 -10
- package/dist/plot/HistogramControls.svelte.d.ts +6 -2
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
- package/dist/plot/PlotControls.svelte +109 -27
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +1 -1
- package/dist/plot/PortalSelect.svelte +2 -1
- package/dist/plot/ReferenceLine.svelte +2 -1
- package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
- package/dist/plot/ReferencePlane.svelte +1 -3
- package/dist/plot/ScatterPlot.svelte +343 -209
- package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +203 -250
- package/dist/plot/ScatterPlot3DScene.svelte +4 -7
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +95 -55
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ZeroLines.svelte +44 -0
- package/dist/plot/ZeroLines.svelte.d.ts +32 -0
- package/dist/plot/ZoomRect.svelte +21 -0
- package/dist/plot/ZoomRect.svelte.d.ts +8 -0
- package/dist/plot/axis-utils.d.ts +1 -1
- package/dist/plot/data-cleaning.js +1 -5
- 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 +10 -19
- package/dist/plot/layout.d.ts +7 -1
- package/dist/plot/layout.js +12 -4
- package/dist/plot/reference-line.d.ts +4 -21
- package/dist/plot/reference-line.js +7 -81
- package/dist/plot/types.d.ts +42 -17
- package/dist/plot/types.js +10 -0
- package/dist/plot/utils/label-placement.js +14 -11
- package/dist/plot/utils.d.ts +1 -0
- package/dist/plot/utils.js +14 -0
- package/dist/rdf/RdfPlot.svelte +55 -66
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/rdf/index.d.ts +1 -1
- package/dist/rdf/index.js +1 -1
- package/dist/settings.d.ts +5 -0
- package/dist/settings.js +37 -3
- package/dist/spectral/Bands.svelte +515 -143
- package/dist/spectral/Bands.svelte.d.ts +22 -2
- package/dist/spectral/helpers.d.ts +23 -1
- package/dist/spectral/helpers.js +65 -9
- package/dist/spectral/types.d.ts +2 -0
- package/dist/structure/AtomLegend.svelte +31 -10
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CellSelect.svelte +92 -22
- package/dist/structure/Lattice.svelte +2 -0
- package/dist/structure/Structure.svelte +716 -173
- package/dist/structure/Structure.svelte.d.ts +7 -2
- package/dist/structure/StructureControls.svelte +26 -14
- package/dist/structure/StructureControls.svelte.d.ts +5 -1
- package/dist/structure/StructureInfoPane.svelte +7 -1
- package/dist/structure/StructureScene.svelte +386 -95
- package/dist/structure/StructureScene.svelte.d.ts +15 -4
- package/dist/structure/atom-properties.d.ts +6 -2
- package/dist/structure/atom-properties.js +38 -25
- package/dist/structure/export.js +10 -7
- 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 +1 -2
- package/dist/structure/index.d.ts +7 -0
- package/dist/structure/index.js +22 -0
- package/dist/structure/parse.js +19 -16
- package/dist/structure/partial-occupancy.d.ts +25 -0
- package/dist/structure/partial-occupancy.js +102 -0
- package/dist/structure/validation.js +6 -3
- package/dist/symmetry/SymmetryStats.svelte +18 -4
- package/dist/symmetry/WyckoffTable.svelte +18 -10
- package/dist/symmetry/index.d.ts +7 -4
- package/dist/symmetry/index.js +83 -18
- package/dist/table/HeatmapTable.svelte +468 -69
- package/dist/table/HeatmapTable.svelte.d.ts +13 -1
- package/dist/table/ToggleMenu.svelte +291 -44
- package/dist/table/ToggleMenu.svelte.d.ts +4 -1
- package/dist/table/index.d.ts +3 -0
- package/dist/tooltip/index.d.ts +1 -1
- package/dist/tooltip/index.js +1 -0
- package/dist/trajectory/Trajectory.svelte +147 -145
- package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
- package/dist/trajectory/constants.d.ts +6 -0
- package/dist/trajectory/constants.js +7 -0
- package/dist/trajectory/extract.js +3 -5
- 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 +339 -0
- package/dist/trajectory/helpers.d.ts +15 -0
- package/dist/trajectory/helpers.js +187 -0
- package/dist/trajectory/index.d.ts +1 -0
- package/dist/trajectory/index.js +11 -4
- package/dist/trajectory/parse/ase.d.ts +2 -0
- package/dist/trajectory/parse/ase.js +76 -0
- package/dist/trajectory/parse/hdf5.d.ts +2 -0
- package/dist/trajectory/parse/hdf5.js +121 -0
- package/dist/trajectory/parse/index.d.ts +12 -0
- package/dist/trajectory/parse/index.js +304 -0
- package/dist/trajectory/parse/lammps.d.ts +5 -0
- package/dist/trajectory/parse/lammps.js +169 -0
- package/dist/trajectory/parse/vasp.d.ts +2 -0
- package/dist/trajectory/parse/vasp.js +65 -0
- package/dist/trajectory/parse/xyz.d.ts +2 -0
- package/dist/trajectory/parse/xyz.js +109 -0
- package/dist/trajectory/types.d.ts +11 -0
- package/dist/trajectory/types.js +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +4 -0
- package/dist/xrd/XrdPlot.svelte +6 -4
- package/dist/xrd/calc-xrd.js +0 -1
- package/package.json +33 -23
- 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
|
@@ -1,19 +1,25 @@
|
|
|
1
1
|
<script lang="ts">import { ELEMENT_COLOR_SCHEMES } from '../colors';
|
|
2
2
|
import { normalize_show_controls } from '../controls';
|
|
3
|
+
import { StatusMessage } from '../feedback';
|
|
3
4
|
import Spinner from '../feedback/Spinner.svelte';
|
|
4
5
|
import Icon from '../Icon.svelte';
|
|
5
|
-
import {
|
|
6
|
+
import { create_file_drop_handler, load_from_url } from '../io';
|
|
7
|
+
import { parse_volumetric_file } from '../isosurface/parse';
|
|
8
|
+
import { auto_isosurface_settings, DEFAULT_ISOSURFACE_SETTINGS, } from '../isosurface/types';
|
|
9
|
+
import { ELEM_SYMBOLS } from '../labels';
|
|
6
10
|
import { set_fullscreen_bg, toggle_fullscreen } from '../layout';
|
|
11
|
+
import { create_cart_to_frac, create_frac_to_cart } from '../math';
|
|
7
12
|
import { DEFAULTS } from '../settings';
|
|
8
13
|
import { colors } from '../state.svelte';
|
|
9
|
-
import { get_element_counts, get_pbc_image_sites } from './';
|
|
14
|
+
import { get_element_counts, get_pbc_image_sites, get_site_vector, } from './';
|
|
15
|
+
import { wrap_to_unit_cell } from './pbc';
|
|
10
16
|
import { is_valid_supercell_input, make_supercell } from './supercell';
|
|
11
17
|
import * as symmetry from '../symmetry';
|
|
12
18
|
import { transform_cell } from '../symmetry';
|
|
13
19
|
import { Canvas } from '@threlte/core';
|
|
14
20
|
import { untrack } from 'svelte';
|
|
15
21
|
import { click_outside, tooltip } from 'svelte-multiselect';
|
|
16
|
-
import { SvelteMap } from 'svelte/reactivity';
|
|
22
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity';
|
|
17
23
|
import { get_property_colors } from './atom-properties';
|
|
18
24
|
import AtomLegend from './AtomLegend.svelte';
|
|
19
25
|
import CellSelect from './CellSelect.svelte';
|
|
@@ -34,7 +40,7 @@ let lattice_props = $state({
|
|
|
34
40
|
cell_edge_width: DEFAULTS.structure.cell_edge_width,
|
|
35
41
|
show_cell_vectors: DEFAULTS.structure.show_cell_vectors,
|
|
36
42
|
});
|
|
37
|
-
let { structure = $bindable(), scene_props: scene_props_in = $bindable(), lattice_props: lattice_props_in = $bindable(), controls_open = $bindable(false), info_pane_open = $bindable(false), enable_measure_mode = $bindable(true), background_color = $bindable(), background_opacity = $bindable(0.1), show_controls, fullscreen = $bindable(false), wrapper = $bindable(), width = $bindable(0), height = $bindable(0), reset_text = `Reset camera (or double-click)`, color_scheme = $bindable(`Vesta`), atom_color_config = $bindable({
|
|
43
|
+
let { structure = $bindable(), scene_props: scene_props_in = $bindable(), lattice_props: lattice_props_in = $bindable(), controls_open = $bindable(false), info_pane_open = $bindable(false), enable_measure_mode = $bindable(true), measure_mode = $bindable(`distance`), background_color = $bindable(), background_opacity = $bindable(0.1), show_controls, fullscreen = $bindable(false), wrapper = $bindable(), width = $bindable(0), height = $bindable(0), reset_text = `Reset camera (or double-click)`, color_scheme = $bindable(`Vesta`), atom_color_config = $bindable({
|
|
38
44
|
mode: DEFAULTS.structure.atom_color_mode,
|
|
39
45
|
scale: DEFAULTS.structure.atom_color_scale,
|
|
40
46
|
scale_type: DEFAULTS.structure.atom_color_scale_type,
|
|
@@ -46,9 +52,9 @@ measured_sites = $bindable([]),
|
|
|
46
52
|
// expose the displayed structure (with image atoms and supercell) for external use
|
|
47
53
|
displayed_structure = $bindable(),
|
|
48
54
|
// Track hidden elements across component lifecycle
|
|
49
|
-
hidden_elements = $bindable(new
|
|
55
|
+
hidden_elements = $bindable(new SvelteSet()),
|
|
50
56
|
// Track hidden property values (e.g. Wyckoff positions, coordination numbers)
|
|
51
|
-
hidden_prop_vals = $bindable(new
|
|
57
|
+
hidden_prop_vals = $bindable(new SvelteSet()),
|
|
52
58
|
// Per-element radius overrides (absolute values in Angstroms)
|
|
53
59
|
element_radius_overrides = $bindable({}),
|
|
54
60
|
// Per-site radius overrides (absolute values in Angstroms)
|
|
@@ -61,7 +67,15 @@ symmetry_settings = $bindable(symmetry.default_sym_settings),
|
|
|
61
67
|
// Useful for LAMMPS files where atom types are mapped to H, He, Li by default
|
|
62
68
|
element_mapping = $bindable(),
|
|
63
69
|
// Cell type: original, conventional, or primitive (requires symmetry analysis)
|
|
64
|
-
cell_type = $bindable(`original`),
|
|
70
|
+
cell_type = $bindable(`original`),
|
|
71
|
+
// Volumetric data for isosurface rendering (parsed from CHGCAR or .cube files)
|
|
72
|
+
volumetric_data = $bindable(),
|
|
73
|
+
// Isosurface rendering settings
|
|
74
|
+
isosurface_settings = $bindable({
|
|
75
|
+
...DEFAULT_ISOSURFACE_SETTINGS,
|
|
76
|
+
}),
|
|
77
|
+
// Active volume index when multiple volumes are present
|
|
78
|
+
active_volume_idx = $bindable(0), children, top_right_controls, on_file_load, on_error, on_fullscreen_change, on_camera_move, on_camera_reset, ...rest } = $props();
|
|
65
79
|
// Initialize models from incoming props; mutations come from UI controls; we mirror into local dicts (NOTE only doing shallow merge)
|
|
66
80
|
$effect.pre(() => {
|
|
67
81
|
if (scene_props_in && typeof scene_props_in === `object`) {
|
|
@@ -85,21 +99,8 @@ $effect(() => {
|
|
|
85
99
|
const text_content = content instanceof ArrayBuffer
|
|
86
100
|
? new TextDecoder().decode(content)
|
|
87
101
|
: content;
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
structure = parsed_structure;
|
|
91
|
-
// Emit file load event
|
|
92
|
-
on_file_load?.({
|
|
93
|
-
structure,
|
|
94
|
-
filename,
|
|
95
|
-
file_size: new Blob([content]).size,
|
|
96
|
-
total_atoms: structure.sites?.length || 0,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
error_msg = `Failed to parse structure from ${filename}`;
|
|
101
|
-
on_error?.({ error_msg, filename });
|
|
102
|
-
}
|
|
102
|
+
const parsed = parse_file_content(text_content, filename);
|
|
103
|
+
emit_file_load_event(parsed, filename, content);
|
|
103
104
|
}
|
|
104
105
|
catch (error) {
|
|
105
106
|
error_msg = `Failed to parse structure: ${error instanceof Error ? error.message : String(error)}`;
|
|
@@ -141,17 +142,18 @@ $effect(() => {
|
|
|
141
142
|
});
|
|
142
143
|
// Track if force vectors were auto-enabled to prevent repeated triggering
|
|
143
144
|
let force_vectors_auto_enabled = $state(false);
|
|
144
|
-
// Auto-enable force vectors when structure has force
|
|
145
|
+
// Auto-enable force vectors when structure has vector data (force, magmom, or spin)
|
|
145
146
|
$effect(() => {
|
|
146
147
|
if (structure?.sites && !force_vectors_auto_enabled) {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
const has_vector_data = structure.sites.some((site) => get_site_vector(site) !== null);
|
|
149
|
+
if (!has_vector_data)
|
|
150
|
+
return;
|
|
151
|
+
if (!scene_props.show_force_vectors) {
|
|
150
152
|
scene_props.show_force_vectors = true;
|
|
151
153
|
scene_props.force_scale ??= DEFAULTS.structure.force_scale;
|
|
152
154
|
scene_props.force_color ??= DEFAULTS.structure.force_color;
|
|
153
|
-
force_vectors_auto_enabled = true;
|
|
154
155
|
}
|
|
156
|
+
force_vectors_auto_enabled = true;
|
|
155
157
|
}
|
|
156
158
|
});
|
|
157
159
|
// Optimize scene props for performance based on structure size and mode
|
|
@@ -172,23 +174,40 @@ $effect(() => {
|
|
|
172
174
|
let property_colors = $derived(get_property_colors(structure, atom_color_config, scene_props.bonding_strategy, sym_data));
|
|
173
175
|
let symmetry_run_id = 0;
|
|
174
176
|
let symmetry_error = $state();
|
|
175
|
-
|
|
177
|
+
let last_symmetry_structure_ref = null;
|
|
178
|
+
// Trigger symmetry analysis when structure is loaded or settings change.
|
|
179
|
+
// Skip during atom drags — symmetry doesn't change from moving atoms,
|
|
180
|
+
// and WASM analysis on every drag frame causes severe frame drops.
|
|
176
181
|
$effect(() => {
|
|
182
|
+
if (dragging_atoms)
|
|
183
|
+
return;
|
|
177
184
|
if (!structure || !(`lattice` in structure)) {
|
|
178
185
|
untrack(() => {
|
|
179
186
|
sym_data = null;
|
|
180
187
|
symmetry_error = undefined;
|
|
181
188
|
});
|
|
189
|
+
last_symmetry_structure_ref = null;
|
|
182
190
|
return;
|
|
183
191
|
}
|
|
184
192
|
const current_structure = structure;
|
|
193
|
+
const structure_changed = current_structure !== last_symmetry_structure_ref;
|
|
194
|
+
if (structure_changed) {
|
|
195
|
+
untrack(() => {
|
|
196
|
+
sym_data = null;
|
|
197
|
+
symmetry_error = undefined;
|
|
198
|
+
});
|
|
199
|
+
last_symmetry_structure_ref = current_structure;
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
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
|
+
}
|
|
185
206
|
const run_id = ++symmetry_run_id;
|
|
186
207
|
// Destructure symmetry_settings to ensure Svelte tracks changes to symprec and algo
|
|
187
208
|
// (reading just the object reference isn't sufficient for fine-grained reactivity)
|
|
188
209
|
const { symprec, algo } = symmetry_settings ?? symmetry.default_sym_settings;
|
|
189
210
|
const current_settings = { symprec, algo };
|
|
190
|
-
// Use untrack to prevent cascading reactivity when resetting state
|
|
191
|
-
untrack(() => [sym_data, symmetry_error] = [null, undefined]);
|
|
192
211
|
symmetry.ensure_moyo_wasm_ready()
|
|
193
212
|
.then(() => run_id === symmetry_run_id
|
|
194
213
|
? symmetry.analyze_structure_symmetry(current_structure, current_settings)
|
|
@@ -200,18 +219,129 @@ $effect(() => {
|
|
|
200
219
|
})
|
|
201
220
|
.catch((err) => {
|
|
202
221
|
if (run_id === symmetry_run_id) {
|
|
222
|
+
untrack(() => sym_data = null);
|
|
203
223
|
symmetry_error = `Symmetry analysis failed: ${err?.message || err}`;
|
|
204
224
|
console.error(`Symmetry analysis failed:`, err);
|
|
205
225
|
}
|
|
206
226
|
});
|
|
207
227
|
});
|
|
208
|
-
// Measurement mode and selection state
|
|
209
|
-
let measure_mode = $state(`distance`);
|
|
210
228
|
let measure_menu_open = $state(false);
|
|
211
229
|
let export_pane_open = $state(false);
|
|
212
230
|
// Bond customization state
|
|
213
231
|
let added_bonds = $state([]);
|
|
214
232
|
let removed_bonds = $state([]);
|
|
233
|
+
// === Edit-atoms mode state ===
|
|
234
|
+
let dragging_atoms = $state(false);
|
|
235
|
+
let undo_stack = $state([]);
|
|
236
|
+
let redo_stack = $state([]);
|
|
237
|
+
const MAX_HISTORY = 20;
|
|
238
|
+
// Flag set before internal edits (undo/redo/delete/add/move) to distinguish
|
|
239
|
+
// them from external structure changes (file load, trajectory step, etc.)
|
|
240
|
+
let is_internal_edit = false;
|
|
241
|
+
// Add-atom sub-mode state (bound to StructureScene)
|
|
242
|
+
let add_atom_mode = $state(false);
|
|
243
|
+
let add_element = $state(`C`);
|
|
244
|
+
let canvas_cursor = $state(`default`);
|
|
245
|
+
let change_element_mode = $state(false);
|
|
246
|
+
let change_element_value = $state(``);
|
|
247
|
+
// Ephemeral toast message for edit operations
|
|
248
|
+
let toast_msg = $state(null);
|
|
249
|
+
let toast_timer;
|
|
250
|
+
function show_toast(msg, duration_ms = 2000) {
|
|
251
|
+
clearTimeout(toast_timer);
|
|
252
|
+
toast_msg = msg;
|
|
253
|
+
toast_timer = setTimeout(() => (toast_msg = null), duration_ms);
|
|
254
|
+
}
|
|
255
|
+
// Normalize and validate element symbol (e.g. "fe" → "Fe", "Xx" → null)
|
|
256
|
+
function normalize_element(input) {
|
|
257
|
+
const normalized = (input.charAt(0).toUpperCase() +
|
|
258
|
+
input.slice(1).toLowerCase());
|
|
259
|
+
return ELEM_SYMBOLS.includes(normalized) ? normalized : null;
|
|
260
|
+
}
|
|
261
|
+
function clear_selection() {
|
|
262
|
+
selected_sites = [];
|
|
263
|
+
measured_sites = [];
|
|
264
|
+
dragging_atoms = false;
|
|
265
|
+
}
|
|
266
|
+
function push_undo() {
|
|
267
|
+
if (!structure)
|
|
268
|
+
return;
|
|
269
|
+
if (undo_stack.length >= MAX_HISTORY) {
|
|
270
|
+
undo_stack.splice(0, undo_stack.length - MAX_HISTORY + 1);
|
|
271
|
+
}
|
|
272
|
+
undo_stack.push($state.snapshot(structure));
|
|
273
|
+
redo_stack.length = 0;
|
|
274
|
+
}
|
|
275
|
+
// Shared undo/redo: pop from `source`, push current state onto `target`
|
|
276
|
+
function apply_history(source, target) {
|
|
277
|
+
if (source.length === 0 || !structure)
|
|
278
|
+
return;
|
|
279
|
+
const restored = source.pop();
|
|
280
|
+
if (!restored)
|
|
281
|
+
return;
|
|
282
|
+
is_internal_edit = true;
|
|
283
|
+
target.push($state.snapshot(structure));
|
|
284
|
+
structure = restored;
|
|
285
|
+
clear_selection();
|
|
286
|
+
}
|
|
287
|
+
const undo = () => apply_history(undo_stack, redo_stack);
|
|
288
|
+
const redo = () => apply_history(redo_stack, undo_stack);
|
|
289
|
+
// Clear undo/redo stacks when structure changes externally (file load, etc.)
|
|
290
|
+
// Internal edits set is_internal_edit=true before modifying structure.
|
|
291
|
+
// This $effect runs after microtask, so the flag is still set from the edit.
|
|
292
|
+
$effect(() => {
|
|
293
|
+
// Track structure to re-run when it changes
|
|
294
|
+
void structure;
|
|
295
|
+
if (is_internal_edit) {
|
|
296
|
+
is_internal_edit = false;
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
// External change — clear history and stale edit-atoms state
|
|
300
|
+
untrack(() => {
|
|
301
|
+
if (undo_stack.length > 0 || redo_stack.length > 0) {
|
|
302
|
+
undo_stack = [];
|
|
303
|
+
redo_stack = [];
|
|
304
|
+
}
|
|
305
|
+
if (measure_mode === `edit-atoms`) {
|
|
306
|
+
if (selected_sites.length > 0 || measured_sites.length > 0)
|
|
307
|
+
clear_selection();
|
|
308
|
+
if (site_radius_overrides?.size > 0)
|
|
309
|
+
site_radius_overrides.clear();
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
// Clear selection when switching measure/edit mode so stale state doesn't carry over
|
|
314
|
+
let mode_first_run = true;
|
|
315
|
+
$effect(() => {
|
|
316
|
+
void measure_mode; // track reactively
|
|
317
|
+
if (mode_first_run) {
|
|
318
|
+
mode_first_run = false;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
untrack(() => {
|
|
322
|
+
if (selected_sites.length > 0 || measured_sites.length > 0)
|
|
323
|
+
clear_selection();
|
|
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;
|
|
330
|
+
untrack(() => {
|
|
331
|
+
// Clear bond edits from edit-bonds mode to avoid stale state
|
|
332
|
+
if (added_bonds.length > 0 || removed_bonds.length > 0) {
|
|
333
|
+
added_bonds = [];
|
|
334
|
+
removed_bonds = [];
|
|
335
|
+
}
|
|
336
|
+
if (cell_type !== `original` && cell_transformed_structure && structure) {
|
|
337
|
+
// Bake the transformed cell: push original to undo, replace structure
|
|
338
|
+
is_internal_edit = true;
|
|
339
|
+
push_undo();
|
|
340
|
+
structure = $state.snapshot(cell_transformed_structure);
|
|
341
|
+
cell_type = `original`;
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
});
|
|
215
345
|
let controls_config = $derived(normalize_show_controls(show_controls));
|
|
216
346
|
// Normalize structure coordinates: wrap fractional coords to [0,1) and recompute Cartesian
|
|
217
347
|
// This ensures atoms are rendered inside the unit cell regardless of data source
|
|
@@ -242,13 +372,10 @@ let cell_transformed_structure = $derived.by(() => {
|
|
|
242
372
|
// Create supercell if needed (uses cell_transformed_structure as base)
|
|
243
373
|
let supercell_structure = $state(structure);
|
|
244
374
|
let supercell_loading = $state(false);
|
|
375
|
+
let has_supercell = $derived(!!supercell_scaling && ![``, `1x1x1`, `1`].includes(supercell_scaling));
|
|
245
376
|
$effect(() => {
|
|
246
377
|
const base_structure = cell_transformed_structure;
|
|
247
|
-
if (!base_structure || !(`lattice` in base_structure)) {
|
|
248
|
-
supercell_structure = base_structure;
|
|
249
|
-
supercell_loading = false;
|
|
250
|
-
}
|
|
251
|
-
else if ([``, `1x1x1`, `1`].includes(supercell_scaling)) {
|
|
378
|
+
if (!base_structure || !(`lattice` in base_structure) || !has_supercell) {
|
|
252
379
|
supercell_structure = base_structure;
|
|
253
380
|
supercell_loading = false;
|
|
254
381
|
}
|
|
@@ -303,16 +430,20 @@ $effect(() => {
|
|
|
303
430
|
return;
|
|
304
431
|
}
|
|
305
432
|
untrack(() => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
433
|
+
// In edit-atoms mode, structure changes are intentional user edits
|
|
434
|
+
// (move/add/delete) — preserve the selection so TransformControls stays active
|
|
435
|
+
if (measure_mode === `edit-atoms`)
|
|
436
|
+
return;
|
|
437
|
+
if (selected_sites.length > 0 || measured_sites.length > 0)
|
|
438
|
+
clear_selection();
|
|
310
439
|
// Clear site radius overrides since site indices are no longer valid
|
|
311
440
|
if (site_radius_overrides?.size > 0)
|
|
312
441
|
site_radius_overrides.clear();
|
|
313
442
|
});
|
|
314
443
|
});
|
|
315
|
-
// Apply element mapping then image atoms to the supercell structure
|
|
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.
|
|
316
447
|
$effect(() => {
|
|
317
448
|
let struct = supercell_structure;
|
|
318
449
|
if (struct && element_mapping && Object.keys(element_mapping).length > 0) {
|
|
@@ -330,7 +461,8 @@ $effect(() => {
|
|
|
330
461
|
};
|
|
331
462
|
}
|
|
332
463
|
displayed_structure =
|
|
333
|
-
show_image_atoms && struct && `lattice` in struct &&
|
|
464
|
+
!dragging_atoms && show_image_atoms && struct && `lattice` in struct &&
|
|
465
|
+
struct.lattice
|
|
334
466
|
? get_pbc_image_sites(struct)
|
|
335
467
|
: struct;
|
|
336
468
|
});
|
|
@@ -342,7 +474,6 @@ let camera = $state(undefined);
|
|
|
342
474
|
let orbit_controls = $state(undefined);
|
|
343
475
|
let rotation_target_ref = $state(undefined);
|
|
344
476
|
let initial_computed_zoom = $state(undefined);
|
|
345
|
-
let camera_move_timeout = $state(null);
|
|
346
477
|
// Mutual exclusion: opening one pane closes others
|
|
347
478
|
$effect(() => {
|
|
348
479
|
if (info_pane_open) {
|
|
@@ -364,29 +495,53 @@ $effect(() => {
|
|
|
364
495
|
if (structure)
|
|
365
496
|
camera_has_moved = false;
|
|
366
497
|
});
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
498
|
+
const read_orbit_target = () => {
|
|
499
|
+
if (!orbit_controls?.target)
|
|
500
|
+
return;
|
|
501
|
+
const { x, y, z } = orbit_controls.target;
|
|
502
|
+
return [x, y, z];
|
|
503
|
+
};
|
|
504
|
+
const read_camera_position = () => camera
|
|
505
|
+
? [camera.position.x, camera.position.y, camera.position.z]
|
|
506
|
+
: scene_props.camera_position;
|
|
507
|
+
// Emit debounced camera updates while controls are active.
|
|
508
|
+
$effect(() => {
|
|
509
|
+
if (!camera_is_moving)
|
|
510
|
+
return;
|
|
511
|
+
camera_has_moved = true;
|
|
512
|
+
const emit_camera_move = () => {
|
|
513
|
+
const camera_position = read_camera_position();
|
|
514
|
+
if (camera_position === undefined)
|
|
515
|
+
return;
|
|
516
|
+
const camera_target = read_orbit_target();
|
|
517
|
+
scene_props.camera_position = camera_position;
|
|
518
|
+
scene_props.camera_target = camera_target;
|
|
519
|
+
on_camera_move?.({
|
|
520
|
+
structure,
|
|
521
|
+
camera_has_moved,
|
|
522
|
+
camera_position,
|
|
523
|
+
camera_target,
|
|
524
|
+
});
|
|
525
|
+
};
|
|
526
|
+
emit_camera_move();
|
|
527
|
+
const emit_interval = setInterval(emit_camera_move, 200);
|
|
528
|
+
return () => clearInterval(emit_interval);
|
|
529
|
+
});
|
|
380
530
|
function reset_camera() {
|
|
381
|
-
// Reset camera position to trigger automatic positioning
|
|
531
|
+
// Reset camera position to trigger automatic positioning.
|
|
382
532
|
scene_props.camera_position = [0, 0, 0];
|
|
533
|
+
scene_props.camera_target = rotation_target_ref;
|
|
383
534
|
camera_has_moved = false;
|
|
384
|
-
|
|
535
|
+
let camera_position = [0, 0, 0];
|
|
536
|
+
let camera_target = rotation_target_ref;
|
|
537
|
+
// Reset pan/zoom and ensure controls target returns to structure center.
|
|
385
538
|
if (orbit_controls && camera) {
|
|
386
|
-
|
|
539
|
+
if (`reset` in orbit_controls &&
|
|
540
|
+
typeof orbit_controls.reset === `function`)
|
|
541
|
+
orbit_controls.reset();
|
|
387
542
|
if (orbit_controls.target && rotation_target_ref) {
|
|
388
|
-
const [
|
|
389
|
-
orbit_controls.target.set(
|
|
543
|
+
const [target_x, target_y, target_z] = rotation_target_ref;
|
|
544
|
+
orbit_controls.target.set(target_x, target_y, target_z);
|
|
390
545
|
}
|
|
391
546
|
// Reset zoom for orthographic camera
|
|
392
547
|
if (`zoom` in camera && initial_computed_zoom !== undefined) {
|
|
@@ -395,11 +550,14 @@ function reset_camera() {
|
|
|
395
550
|
ortho_camera.updateProjectionMatrix();
|
|
396
551
|
}
|
|
397
552
|
// Call update to apply changes immediately
|
|
398
|
-
if (typeof orbit_controls.update === `function`)
|
|
553
|
+
if (typeof orbit_controls.update === `function`)
|
|
399
554
|
orbit_controls.update();
|
|
400
|
-
|
|
555
|
+
camera_position = read_camera_position() ?? camera_position;
|
|
556
|
+
camera_target = read_orbit_target();
|
|
401
557
|
}
|
|
402
|
-
|
|
558
|
+
scene_props.camera_position = camera_position;
|
|
559
|
+
scene_props.camera_target = camera_target;
|
|
560
|
+
on_camera_reset?.({ structure, camera_has_moved, camera_position, camera_target });
|
|
403
561
|
}
|
|
404
562
|
const emit_file_load_event = (structure, filename, content) => on_file_load?.({
|
|
405
563
|
structure: structure,
|
|
@@ -409,93 +567,320 @@ const emit_file_load_event = (structure, filename, content) => on_file_load?.({
|
|
|
409
567
|
: content.byteLength,
|
|
410
568
|
total_atoms: structure.sites?.length || 0,
|
|
411
569
|
});
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
structure = parsed_structure;
|
|
429
|
-
emit_file_load_event(parsed_structure, filename, content);
|
|
430
|
-
}
|
|
431
|
-
else
|
|
432
|
-
throw new Error(`Failed to parse structure from ${filename}`);
|
|
433
|
-
}
|
|
434
|
-
catch (err) {
|
|
435
|
-
error_msg = `Failed to parse structure: ${err}`;
|
|
436
|
-
on_error?.({ error_msg, filename });
|
|
437
|
-
}
|
|
438
|
-
})).catch(() => false);
|
|
439
|
-
if (handled)
|
|
440
|
-
return;
|
|
441
|
-
// Handle file system drops
|
|
442
|
-
const file = event.dataTransfer?.files[0];
|
|
443
|
-
if (file) {
|
|
444
|
-
try {
|
|
445
|
-
const { content, filename } = await decompress_file(file);
|
|
446
|
-
if (content) {
|
|
447
|
-
if (on_file_drop)
|
|
448
|
-
on_file_drop(content, filename);
|
|
449
|
-
else {
|
|
450
|
-
// Parse structure internally when no handler provided
|
|
451
|
-
try {
|
|
452
|
-
const parsed_structure = parse_any_structure(content, filename);
|
|
453
|
-
if (parsed_structure) {
|
|
454
|
-
structure = parsed_structure;
|
|
455
|
-
emit_file_load_event(parsed_structure, filename, content);
|
|
456
|
-
}
|
|
457
|
-
else
|
|
458
|
-
throw new Error(`Failed to parse structure from ${filename}`);
|
|
459
|
-
}
|
|
460
|
-
catch (err) {
|
|
461
|
-
error_msg = `Failed to parse structure: ${err}`;
|
|
462
|
-
on_error?.({ error_msg, filename });
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
catch (error) {
|
|
468
|
-
error_msg = `Failed to load file ${file.name}: ${error}`;
|
|
469
|
-
on_error?.({ error_msg, filename: file.name });
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
finally {
|
|
474
|
-
loading = false;
|
|
570
|
+
// Try to parse content as a volumetric file, setting both structure and volumetric data.
|
|
571
|
+
// Delegates format detection entirely to parse_volumetric_file (filename + content sniffing).
|
|
572
|
+
// Returns the parsed structure on success, or null if the file isn't a volumetric format.
|
|
573
|
+
function try_parse_volumetric(text_content, filename) {
|
|
574
|
+
const vol_result = parse_volumetric_file(text_content, filename);
|
|
575
|
+
if (!vol_result)
|
|
576
|
+
return null;
|
|
577
|
+
// parse_volumetric_file extracts structure from file header;
|
|
578
|
+
// parsers set pbc so the lattice conforms to Crystal's LatticeType
|
|
579
|
+
structure = vol_result.structure;
|
|
580
|
+
volumetric_data = vol_result.volumes;
|
|
581
|
+
// Auto-compute reasonable isosurface settings from data range
|
|
582
|
+
const vol = vol_result.volumes[0];
|
|
583
|
+
if (vol) {
|
|
584
|
+
isosurface_settings = auto_isosurface_settings(vol.data_range);
|
|
585
|
+
active_volume_idx = 0;
|
|
475
586
|
}
|
|
587
|
+
return structure;
|
|
476
588
|
}
|
|
477
|
-
|
|
589
|
+
// Parse file content, trying volumetric format first then falling back to plain structure.
|
|
590
|
+
// Returns the parsed structure on success, throws on failure.
|
|
591
|
+
function parse_file_content(text_content, filename) {
|
|
592
|
+
const vol_struct = try_parse_volumetric(text_content, filename);
|
|
593
|
+
if (vol_struct)
|
|
594
|
+
return vol_struct;
|
|
595
|
+
// 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
|
+
throw new Error(`Failed to parse structure from ${filename}`);
|
|
600
|
+
structure = parsed;
|
|
601
|
+
return parsed;
|
|
602
|
+
}
|
|
603
|
+
const handle_file_drop = create_file_drop_handler({
|
|
604
|
+
allow: () => allow_file_drop,
|
|
605
|
+
on_drop: (content, filename) => {
|
|
606
|
+
if (on_file_drop)
|
|
607
|
+
return on_file_drop(content, filename);
|
|
608
|
+
try {
|
|
609
|
+
const text_content = content instanceof ArrayBuffer
|
|
610
|
+
? new TextDecoder().decode(content)
|
|
611
|
+
: content;
|
|
612
|
+
const parsed = parse_file_content(text_content, filename);
|
|
613
|
+
emit_file_load_event(parsed, filename, content);
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
error_msg = `Failed to parse structure: ${err instanceof Error ? err.message : String(err)}`;
|
|
617
|
+
on_error?.({ error_msg, filename });
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
on_error: (msg) => {
|
|
621
|
+
error_msg = msg;
|
|
622
|
+
on_error?.({ error_msg: msg });
|
|
623
|
+
},
|
|
624
|
+
set_loading: (val) => {
|
|
625
|
+
loading = val;
|
|
626
|
+
if (val)
|
|
627
|
+
[error_msg, dragover] = [undefined, false];
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
function handle_keydown(event) {
|
|
478
631
|
// Don't handle shortcuts if user is typing in an input field
|
|
479
632
|
const target = event.target;
|
|
480
633
|
const is_input_focused = target.tagName === `INPUT` ||
|
|
481
634
|
target.tagName === `TEXTAREA`;
|
|
635
|
+
// Allow Escape to cancel add-atom mode even when the element input is focused
|
|
636
|
+
if (event.key === `Escape` && measure_mode === `edit-atoms` && add_atom_mode) {
|
|
637
|
+
event.preventDefault();
|
|
638
|
+
add_atom_mode = false;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
482
641
|
if (is_input_focused)
|
|
483
642
|
return;
|
|
484
|
-
//
|
|
485
|
-
if (
|
|
643
|
+
// Edit-atoms mode shortcuts (including undo/redo)
|
|
644
|
+
if (measure_mode === `edit-atoms`) {
|
|
645
|
+
// Undo/redo shortcuts (Ctrl/Cmd + Z/Y) — only active in edit-atoms mode
|
|
646
|
+
if (event.ctrlKey || event.metaKey) {
|
|
647
|
+
const key = event.key.toLowerCase();
|
|
648
|
+
if (key === `z` && !event.shiftKey) {
|
|
649
|
+
event.preventDefault();
|
|
650
|
+
undo();
|
|
651
|
+
show_toast(`Undo (${undo_stack.length} left)`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
else if (key === `y` || (key === `z` && event.shiftKey)) {
|
|
655
|
+
event.preventDefault();
|
|
656
|
+
redo();
|
|
657
|
+
show_toast(`Redo (${redo_stack.length} left)`);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (event.key === `Delete` || event.key === `Backspace`) {
|
|
662
|
+
// Delete selected atoms
|
|
663
|
+
if (selected_sites.length > 0 && structure?.sites) {
|
|
664
|
+
event.preventDefault();
|
|
665
|
+
is_internal_edit = true;
|
|
666
|
+
push_undo();
|
|
667
|
+
const to_delete = scene_to_structure_indices(selected_sites, true);
|
|
668
|
+
const n_deleted = to_delete.size;
|
|
669
|
+
clear_selection();
|
|
670
|
+
structure = {
|
|
671
|
+
...structure,
|
|
672
|
+
sites: structure.sites.filter((_, idx) => !to_delete.has(idx)),
|
|
673
|
+
};
|
|
674
|
+
// Clear per-site overrides since indices shifted after deletion
|
|
675
|
+
if (site_radius_overrides?.size > 0)
|
|
676
|
+
site_radius_overrides.clear();
|
|
677
|
+
added_bonds = [];
|
|
678
|
+
removed_bonds = [];
|
|
679
|
+
show_toast(`Deleted ${n_deleted} site${n_deleted > 1 ? `s` : ``}`);
|
|
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;
|
|
690
|
+
}
|
|
691
|
+
// Change element of selected atoms
|
|
692
|
+
if (key === `e` && plain && selected_sites.length > 0) {
|
|
693
|
+
event.preventDefault();
|
|
694
|
+
change_element_mode = !change_element_mode;
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
// Duplicate selected atoms at a small offset
|
|
698
|
+
if (key === `d` && (event.ctrlKey || event.metaKey) &&
|
|
699
|
+
selected_sites.length > 0 && structure?.sites) {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
is_internal_edit = true;
|
|
702
|
+
push_undo();
|
|
703
|
+
const orig_indices = scene_to_structure_indices(selected_sites);
|
|
704
|
+
const cart_to_frac = get_cart_to_frac();
|
|
705
|
+
const new_sites = structure.sites
|
|
706
|
+
.filter((_, idx) => orig_indices.has(idx))
|
|
707
|
+
.map((site) => {
|
|
708
|
+
const new_xyz = [
|
|
709
|
+
site.xyz[0] + 0.5,
|
|
710
|
+
site.xyz[1] + 0.5,
|
|
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;
|
|
730
|
+
}
|
|
731
|
+
// add_atom_mode Escape is already handled above (before is_input_focused guard)
|
|
732
|
+
if (event.key === `Escape`) {
|
|
733
|
+
if (change_element_mode) {
|
|
734
|
+
change_element_mode = false;
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (selected_sites.length > 0) {
|
|
738
|
+
clear_selection();
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
// Interface shortcuts (require Ctrl/Cmd modifier to avoid accidental triggers)
|
|
744
|
+
const has_modifier = event.ctrlKey || event.metaKey;
|
|
745
|
+
if (event.key === `f` && has_modifier && fullscreen_toggle) {
|
|
746
|
+
event.preventDefault();
|
|
486
747
|
toggle_fullscreen(wrapper);
|
|
487
|
-
|
|
748
|
+
}
|
|
749
|
+
else if (event.key === `i` && has_modifier && enable_info_pane) {
|
|
750
|
+
event.preventDefault();
|
|
488
751
|
info_pane_open = !info_pane_open;
|
|
752
|
+
}
|
|
489
753
|
else if (event.key === `Escape`) {
|
|
490
|
-
// Prioritize closing panes
|
|
754
|
+
// Prioritize closing panes, then exit edit modes, then exit fullscreen
|
|
491
755
|
if (info_pane_open)
|
|
492
756
|
info_pane_open = false;
|
|
493
757
|
else if (controls_open)
|
|
494
758
|
controls_open = false;
|
|
495
759
|
else if (export_pane_open)
|
|
496
760
|
export_pane_open = false;
|
|
761
|
+
else if (measure_mode === `edit-bonds` || measure_mode === `edit-atoms`) {
|
|
762
|
+
measure_mode = `distance`;
|
|
763
|
+
}
|
|
497
764
|
}
|
|
498
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();
|
|
772
|
+
for (const scene_idx of scene_indices) {
|
|
773
|
+
const displayed_site = displayed_structure?.sites?.[scene_idx];
|
|
774
|
+
if (!displayed_site)
|
|
775
|
+
continue;
|
|
776
|
+
if (skip_image_atoms && displayed_site.properties?.orig_site_idx != null) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
if (has_supercell && displayed_site.properties?.orig_unit_cell_idx != null) {
|
|
780
|
+
result.add(displayed_site.properties.orig_unit_cell_idx);
|
|
781
|
+
}
|
|
782
|
+
else if (displayed_site.properties?.orig_site_idx != null) {
|
|
783
|
+
// Image atom (PBC ghost) — map back to its original site index
|
|
784
|
+
result.add(displayed_site.properties.orig_site_idx);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
result.add(scene_idx);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
return result;
|
|
791
|
+
}
|
|
792
|
+
// Try to create a Cartesian→fractional converter for the current structure's lattice
|
|
793
|
+
function get_cart_to_frac() {
|
|
794
|
+
if (!structure || !(`lattice` in structure))
|
|
795
|
+
return undefined;
|
|
796
|
+
try {
|
|
797
|
+
return create_cart_to_frac(structure.lattice.matrix);
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
console.warn(`Failed to compute lattice inverse for fractional coordinates`);
|
|
801
|
+
return undefined;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
// Handle atom moves from TransformControls. Applies Cartesian delta and wraps
|
|
805
|
+
// fractional coords inline so normalize_fractional_coords hits its fast path.
|
|
806
|
+
function handle_sites_moved(scene_indices, delta) {
|
|
807
|
+
if (!structure?.sites)
|
|
808
|
+
return;
|
|
809
|
+
is_internal_edit = true;
|
|
810
|
+
const orig_indices = scene_to_structure_indices(scene_indices);
|
|
811
|
+
// For crystals, wrap to [0,1) inline so normalize_fractional_coords fast-paths.
|
|
812
|
+
// For molecules (no lattice), just apply the Cartesian delta directly.
|
|
813
|
+
const lattice = `lattice` in structure
|
|
814
|
+
? structure.lattice.matrix
|
|
815
|
+
: null;
|
|
816
|
+
const cart_to_frac = lattice ? create_cart_to_frac(lattice) : null;
|
|
817
|
+
const frac_to_cart = lattice ? create_frac_to_cart(lattice) : null;
|
|
818
|
+
structure = {
|
|
819
|
+
...structure,
|
|
820
|
+
sites: structure.sites.map((site, idx) => {
|
|
821
|
+
if (!orig_indices.has(idx))
|
|
822
|
+
return site;
|
|
823
|
+
const new_xyz = [
|
|
824
|
+
site.xyz[0] + delta[0],
|
|
825
|
+
site.xyz[1] + delta[1],
|
|
826
|
+
site.xyz[2] + delta[2],
|
|
827
|
+
];
|
|
828
|
+
if (!cart_to_frac || !frac_to_cart) {
|
|
829
|
+
return { ...site, xyz: new_xyz, abc: new_xyz };
|
|
830
|
+
}
|
|
831
|
+
const wrapped_abc = wrap_to_unit_cell(cart_to_frac(new_xyz));
|
|
832
|
+
return { ...site, xyz: frac_to_cart(wrapped_abc), abc: wrapped_abc };
|
|
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
|
+
return;
|
|
840
|
+
const elem = normalize_element(new_element);
|
|
841
|
+
if (!elem)
|
|
842
|
+
return;
|
|
843
|
+
is_internal_edit = true;
|
|
844
|
+
push_undo();
|
|
845
|
+
const orig_indices = scene_to_structure_indices(selected_sites);
|
|
846
|
+
structure = {
|
|
847
|
+
...structure,
|
|
848
|
+
sites: structure.sites.map((site, idx) => {
|
|
849
|
+
if (!orig_indices.has(idx))
|
|
850
|
+
return site;
|
|
851
|
+
return {
|
|
852
|
+
...site,
|
|
853
|
+
species: [{ element: elem, occu: 1, oxidation_state: 0 }],
|
|
854
|
+
label: elem,
|
|
855
|
+
};
|
|
856
|
+
}),
|
|
857
|
+
};
|
|
858
|
+
change_element_mode = false;
|
|
859
|
+
change_element_value = ``;
|
|
860
|
+
show_toast(`Changed ${orig_indices.size} site${orig_indices.size > 1 ? `s` : ``} to ${elem}`);
|
|
861
|
+
}
|
|
862
|
+
// Handle add-atom from StructureScene click-to-place
|
|
863
|
+
function handle_add_atom(xyz, element) {
|
|
864
|
+
if (!structure)
|
|
865
|
+
return;
|
|
866
|
+
const elem = normalize_element(element);
|
|
867
|
+
if (!elem) {
|
|
868
|
+
return console.warn(`Invalid element symbol "${element}", ignoring add-atom`);
|
|
869
|
+
}
|
|
870
|
+
is_internal_edit = true;
|
|
871
|
+
push_undo();
|
|
872
|
+
structure = {
|
|
873
|
+
...structure,
|
|
874
|
+
sites: [...structure.sites, {
|
|
875
|
+
species: [{ element: elem, occu: 1, oxidation_state: 0 }],
|
|
876
|
+
xyz,
|
|
877
|
+
abc: get_cart_to_frac()?.(xyz) ?? xyz,
|
|
878
|
+
label: elem,
|
|
879
|
+
properties: {},
|
|
880
|
+
}],
|
|
881
|
+
};
|
|
882
|
+
show_toast(`Added ${elem} at (${xyz.map((c) => c.toFixed(2)).join(`, `)})`);
|
|
883
|
+
}
|
|
499
884
|
// Only set background override when background_color is explicitly provided
|
|
500
885
|
$effect(() => {
|
|
501
886
|
if (typeof window !== `undefined` && wrapper && background_color) {
|
|
@@ -530,10 +915,13 @@ $effect(() => {
|
|
|
530
915
|
}}
|
|
531
916
|
/>
|
|
532
917
|
|
|
918
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
533
919
|
<div
|
|
534
920
|
class:dragover
|
|
535
921
|
class:active={info_pane_open || controls_open || export_pane_open}
|
|
536
|
-
role="
|
|
922
|
+
role="application"
|
|
923
|
+
tabindex="0"
|
|
924
|
+
style:--canvas-cursor={canvas_cursor}
|
|
537
925
|
aria-label="Structure viewer"
|
|
538
926
|
bind:this={wrapper}
|
|
539
927
|
bind:clientWidth={width}
|
|
@@ -567,7 +955,7 @@ $effect(() => {
|
|
|
567
955
|
event.preventDefault()
|
|
568
956
|
dragover = false
|
|
569
957
|
}}
|
|
570
|
-
{
|
|
958
|
+
onkeydown={handle_keydown}
|
|
571
959
|
{...rest}
|
|
572
960
|
class="structure {rest.class ?? ``}"
|
|
573
961
|
>
|
|
@@ -579,10 +967,7 @@ $effect(() => {
|
|
|
579
967
|
style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%)"
|
|
580
968
|
/>
|
|
581
969
|
{:else if error_msg}
|
|
582
|
-
<
|
|
583
|
-
<p class="error">{error_msg}</p>
|
|
584
|
-
<button onclick={() => (error_msg = undefined)}>Dismiss</button>
|
|
585
|
-
</div>
|
|
970
|
+
<StatusMessage bind:message={error_msg} type="error" dismissible />
|
|
586
971
|
{:else if (structure?.sites?.length ?? 0) > 0}
|
|
587
972
|
<section
|
|
588
973
|
class="control-buttons {controls_config.class}"
|
|
@@ -620,7 +1005,7 @@ $effect(() => {
|
|
|
620
1005
|
>
|
|
621
1006
|
<button
|
|
622
1007
|
onclick={() => (measure_menu_open = !measure_menu_open)}
|
|
623
|
-
title="
|
|
1008
|
+
title="Measure / Edit"
|
|
624
1009
|
class="view-mode-button"
|
|
625
1010
|
class:active={measure_menu_open}
|
|
626
1011
|
aria-expanded={measure_menu_open}
|
|
@@ -636,6 +1021,7 @@ $effect(() => {
|
|
|
636
1021
|
distance: `Ruler`,
|
|
637
1022
|
angle: `Angle`,
|
|
638
1023
|
'edit-bonds': `Link`,
|
|
1024
|
+
'edit-atoms': `Edit`,
|
|
639
1025
|
} as const)[measure_mode]}
|
|
640
1026
|
/>
|
|
641
1027
|
{/if}
|
|
@@ -650,7 +1036,7 @@ $effect(() => {
|
|
|
650
1036
|
type="button"
|
|
651
1037
|
aria-label="Reset selection and bond edits"
|
|
652
1038
|
onclick={() => {
|
|
653
|
-
|
|
1039
|
+
clear_selection()
|
|
654
1040
|
added_bonds = []
|
|
655
1041
|
removed_bonds = []
|
|
656
1042
|
}}
|
|
@@ -663,6 +1049,12 @@ $effect(() => {
|
|
|
663
1049
|
{#each [
|
|
664
1050
|
{ mode: `distance`, icon: `Ruler`, label: `Distance`, scale: 1.1 },
|
|
665
1051
|
{ mode: `angle`, icon: `Angle`, label: `Angle`, scale: 1.3 },
|
|
1052
|
+
{
|
|
1053
|
+
mode: `edit-atoms`,
|
|
1054
|
+
icon: `Edit`,
|
|
1055
|
+
label: `Edit Atoms`,
|
|
1056
|
+
scale: 1.0,
|
|
1057
|
+
},
|
|
666
1058
|
{
|
|
667
1059
|
mode: `edit-bonds`,
|
|
668
1060
|
icon: `Link`,
|
|
@@ -685,6 +1077,84 @@ $effect(() => {
|
|
|
685
1077
|
</div>
|
|
686
1078
|
{/if}
|
|
687
1079
|
</div>
|
|
1080
|
+
|
|
1081
|
+
<!-- Undo/redo buttons (only in edit-atoms mode) -->
|
|
1082
|
+
{#if measure_mode === `edit-atoms`}
|
|
1083
|
+
<div class="undo-redo-container">
|
|
1084
|
+
<button
|
|
1085
|
+
type="button"
|
|
1086
|
+
aria-label="Undo (Ctrl+Z)"
|
|
1087
|
+
disabled={undo_stack.length === 0}
|
|
1088
|
+
onclick={undo}
|
|
1089
|
+
title="Undo (Ctrl+Z)"
|
|
1090
|
+
class="undo-redo-btn"
|
|
1091
|
+
>
|
|
1092
|
+
<Icon icon="Undo" />
|
|
1093
|
+
{#if undo_stack.length > 0}
|
|
1094
|
+
<span class="history-count">{undo_stack.length}</span>
|
|
1095
|
+
{/if}
|
|
1096
|
+
</button>
|
|
1097
|
+
<button
|
|
1098
|
+
type="button"
|
|
1099
|
+
aria-label="Redo (Ctrl+Y)"
|
|
1100
|
+
disabled={redo_stack.length === 0}
|
|
1101
|
+
onclick={redo}
|
|
1102
|
+
title="Redo (Ctrl+Y)"
|
|
1103
|
+
class="undo-redo-btn"
|
|
1104
|
+
>
|
|
1105
|
+
<Icon icon="Redo" />
|
|
1106
|
+
{#if redo_stack.length > 0}
|
|
1107
|
+
<span class="history-count">{redo_stack.length}</span>
|
|
1108
|
+
{/if}
|
|
1109
|
+
</button>
|
|
1110
|
+
</div>
|
|
1111
|
+
{/if}
|
|
1112
|
+
|
|
1113
|
+
<!-- Add-atom element input (shown when add_atom_mode is active) -->
|
|
1114
|
+
{#if measure_mode === `edit-atoms` && add_atom_mode}
|
|
1115
|
+
<div class="add-atom-input">
|
|
1116
|
+
<label>
|
|
1117
|
+
<span>Element:</span>
|
|
1118
|
+
<input
|
|
1119
|
+
type="text"
|
|
1120
|
+
bind:value={add_element}
|
|
1121
|
+
maxlength="2"
|
|
1122
|
+
placeholder="C"
|
|
1123
|
+
style="width: 3em; text-align: center"
|
|
1124
|
+
/>
|
|
1125
|
+
</label>
|
|
1126
|
+
<span style="font-size: 0.75em; opacity: 0.7">Click to place</span>
|
|
1127
|
+
</div>
|
|
1128
|
+
{/if}
|
|
1129
|
+
|
|
1130
|
+
<!-- Change-element input (shown when 'e' pressed with selection) -->
|
|
1131
|
+
{#if measure_mode === `edit-atoms` && change_element_mode &&
|
|
1132
|
+
selected_sites.length > 0}
|
|
1133
|
+
<div class="add-atom-input">
|
|
1134
|
+
<label>
|
|
1135
|
+
<span>New element:</span>
|
|
1136
|
+
<input
|
|
1137
|
+
type="text"
|
|
1138
|
+
bind:value={change_element_value}
|
|
1139
|
+
maxlength="2"
|
|
1140
|
+
placeholder="Fe"
|
|
1141
|
+
style="width: 3em; text-align: center"
|
|
1142
|
+
onkeydown={(event: KeyboardEvent) => {
|
|
1143
|
+
if (event.key === `Enter`) {
|
|
1144
|
+
handle_change_element(change_element_value)
|
|
1145
|
+
} else if (event.key === `Escape`) {
|
|
1146
|
+
change_element_mode = false
|
|
1147
|
+
}
|
|
1148
|
+
event.stopPropagation()
|
|
1149
|
+
}}
|
|
1150
|
+
{@attach (node: HTMLInputElement) => {
|
|
1151
|
+
node.focus()
|
|
1152
|
+
}}
|
|
1153
|
+
/>
|
|
1154
|
+
</label>
|
|
1155
|
+
<span style="font-size: 0.75em; opacity: 0.7">Enter to apply</span>
|
|
1156
|
+
</div>
|
|
1157
|
+
{/if}
|
|
688
1158
|
{/if}
|
|
689
1159
|
|
|
690
1160
|
{#if enable_info_pane && normalized_structure &&
|
|
@@ -722,6 +1192,9 @@ $effect(() => {
|
|
|
722
1192
|
bind:color_scheme
|
|
723
1193
|
bind:atom_color_config
|
|
724
1194
|
bind:cell_type
|
|
1195
|
+
bind:volumetric_data
|
|
1196
|
+
bind:isosurface_settings
|
|
1197
|
+
bind:active_volume_idx
|
|
725
1198
|
{structure}
|
|
726
1199
|
{supercell_loading}
|
|
727
1200
|
{sym_data}
|
|
@@ -759,13 +1232,15 @@ $effect(() => {
|
|
|
759
1232
|
<!-- prevent from rendering in vitest runner since WebGLRenderingContext not available -->
|
|
760
1233
|
{#if typeof WebGLRenderingContext !== `undefined`}
|
|
761
1234
|
<!-- prevent HTML labels from rendering outside of the canvas -->
|
|
762
|
-
<div style="overflow: hidden; height: 100
|
|
1235
|
+
<div style="overflow: hidden; height: 100%; flex: 1">
|
|
763
1236
|
<Canvas>
|
|
764
1237
|
<StructureScene
|
|
765
1238
|
structure={displayed_structure}
|
|
766
1239
|
base_structure={cell_transformed_structure}
|
|
767
1240
|
{...scene_props}
|
|
768
1241
|
{lattice_props}
|
|
1242
|
+
volumetric_data={volumetric_data?.[active_volume_idx]}
|
|
1243
|
+
{isosurface_settings}
|
|
769
1244
|
bind:camera_is_moving
|
|
770
1245
|
bind:selected_sites
|
|
771
1246
|
bind:measured_sites
|
|
@@ -785,6 +1260,13 @@ $effect(() => {
|
|
|
785
1260
|
{height}
|
|
786
1261
|
{atom_color_config}
|
|
787
1262
|
{sym_data}
|
|
1263
|
+
on_sites_moved={handle_sites_moved}
|
|
1264
|
+
on_operation_start={push_undo}
|
|
1265
|
+
on_add_atom={handle_add_atom}
|
|
1266
|
+
bind:add_atom_mode
|
|
1267
|
+
bind:add_element
|
|
1268
|
+
bind:cursor={canvas_cursor}
|
|
1269
|
+
bind:dragging_atoms
|
|
788
1270
|
/>
|
|
789
1271
|
</Canvas>
|
|
790
1272
|
</div>
|
|
@@ -794,7 +1276,11 @@ $effect(() => {
|
|
|
794
1276
|
{@render bottom_left?.({ structure: displayed_structure })}
|
|
795
1277
|
</div>
|
|
796
1278
|
|
|
797
|
-
{#if
|
|
1279
|
+
{#if toast_msg}
|
|
1280
|
+
<div class="edit-toast">{toast_msg}</div>
|
|
1281
|
+
{/if}
|
|
1282
|
+
|
|
1283
|
+
{#if measure_mode === `edit-bonds` &&
|
|
798
1284
|
(added_bonds.length > 0 || removed_bonds.length > 0)}
|
|
799
1285
|
<div class="bond-edit-status">
|
|
800
1286
|
{#if added_bonds.length > 0}
|
|
@@ -849,6 +1335,11 @@ $effect(() => {
|
|
|
849
1335
|
background: var(--struct-dragover-bg, var(--dragover-bg));
|
|
850
1336
|
border: var(--struct-dragover-border, var(--dragover-border));
|
|
851
1337
|
}
|
|
1338
|
+
/* Ensure canvas is transparent so the themed --struct-bg shows through */
|
|
1339
|
+
.structure :global(canvas) {
|
|
1340
|
+
background: transparent;
|
|
1341
|
+
cursor: var(--canvas-cursor, default);
|
|
1342
|
+
}
|
|
852
1343
|
/* Avoid accidental text selection while interacting with the viewer */
|
|
853
1344
|
.structure :global(canvas),
|
|
854
1345
|
.structure section.control-buttons,
|
|
@@ -891,7 +1382,7 @@ $effect(() => {
|
|
|
891
1382
|
display: flex;
|
|
892
1383
|
padding: 4px;
|
|
893
1384
|
border-radius: var(--border-radius, 3pt);
|
|
894
|
-
font-size: clamp(0.85em, 2cqmin,
|
|
1385
|
+
font-size: clamp(0.85em, 2cqmin, 1.3em);
|
|
895
1386
|
}
|
|
896
1387
|
section.control-buttons :global(button:hover) {
|
|
897
1388
|
background-color: color-mix(in srgb, currentColor 8%, transparent);
|
|
@@ -942,7 +1433,7 @@ $effect(() => {
|
|
|
942
1433
|
.measure-mode-dropdown > button {
|
|
943
1434
|
background: transparent;
|
|
944
1435
|
padding: 0 0 0 4px;
|
|
945
|
-
font-size: clamp(0.85em, 2cqmin,
|
|
1436
|
+
font-size: clamp(0.85em, 2cqmin, 1.3em);
|
|
946
1437
|
}
|
|
947
1438
|
.selection-limit-text {
|
|
948
1439
|
font-weight: bold;
|
|
@@ -957,32 +1448,6 @@ $effect(() => {
|
|
|
957
1448
|
display: grid;
|
|
958
1449
|
place-content: center;
|
|
959
1450
|
}
|
|
960
|
-
.error-state {
|
|
961
|
-
display: flex;
|
|
962
|
-
flex-direction: column;
|
|
963
|
-
align-items: center;
|
|
964
|
-
justify-content: center;
|
|
965
|
-
height: var(--struct-height, 500px);
|
|
966
|
-
padding: 2rem;
|
|
967
|
-
text-align: center;
|
|
968
|
-
box-sizing: border-box;
|
|
969
|
-
}
|
|
970
|
-
.error-state p {
|
|
971
|
-
color: var(--error-color, #ff6b6b);
|
|
972
|
-
margin: 0 0 1rem;
|
|
973
|
-
}
|
|
974
|
-
.error-state button {
|
|
975
|
-
padding: 0.5rem 1rem;
|
|
976
|
-
background: var(--error-color, #ff6b6b);
|
|
977
|
-
color: white;
|
|
978
|
-
border: none;
|
|
979
|
-
border-radius: var(--border-radius, 3pt);
|
|
980
|
-
cursor: pointer;
|
|
981
|
-
font-size: 0.9rem;
|
|
982
|
-
}
|
|
983
|
-
.error-state button:hover {
|
|
984
|
-
background: var(--error-color-hover, #ff5252);
|
|
985
|
-
}
|
|
986
1451
|
.symmetry-error {
|
|
987
1452
|
position: absolute;
|
|
988
1453
|
bottom: 1rem;
|
|
@@ -1013,13 +1478,37 @@ $effect(() => {
|
|
|
1013
1478
|
.symmetry-error button:hover {
|
|
1014
1479
|
opacity: 1;
|
|
1015
1480
|
}
|
|
1481
|
+
.edit-toast {
|
|
1482
|
+
position: absolute;
|
|
1483
|
+
bottom: 3rem;
|
|
1484
|
+
left: 50%;
|
|
1485
|
+
transform: translateX(-50%);
|
|
1486
|
+
background: color-mix(in srgb, var(--page-bg, Canvas) 85%, currentColor);
|
|
1487
|
+
color: var(--text-color, currentColor);
|
|
1488
|
+
padding: 0.4rem 0.8rem;
|
|
1489
|
+
border-radius: var(--border-radius, 3pt);
|
|
1490
|
+
font-size: 0.8rem;
|
|
1491
|
+
z-index: 100;
|
|
1492
|
+
pointer-events: none;
|
|
1493
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
|
1494
|
+
animation: toast-fade 2s ease-in-out;
|
|
1495
|
+
opacity: 0;
|
|
1496
|
+
}
|
|
1497
|
+
@keyframes toast-fade {
|
|
1498
|
+
0%, 70% {
|
|
1499
|
+
opacity: 1;
|
|
1500
|
+
}
|
|
1501
|
+
100% {
|
|
1502
|
+
opacity: 0;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1016
1505
|
.bond-edit-status {
|
|
1017
1506
|
position: absolute;
|
|
1018
1507
|
bottom: 1rem;
|
|
1019
1508
|
left: 50%;
|
|
1020
1509
|
transform: translateX(-50%);
|
|
1021
|
-
background:
|
|
1022
|
-
color:
|
|
1510
|
+
background: color-mix(in srgb, var(--page-bg, Canvas) 85%, currentColor);
|
|
1511
|
+
color: var(--text-color, currentColor);
|
|
1023
1512
|
padding: 0.5rem 1rem;
|
|
1024
1513
|
border-radius: var(--border-radius, 3pt);
|
|
1025
1514
|
font-size: 0.85rem;
|
|
@@ -1027,12 +1516,15 @@ $effect(() => {
|
|
|
1027
1516
|
gap: 0.75rem;
|
|
1028
1517
|
z-index: 100;
|
|
1029
1518
|
pointer-events: none;
|
|
1519
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
|
1030
1520
|
}
|
|
1031
1521
|
.bond-edit-status .added {
|
|
1032
1522
|
color: #4caf50;
|
|
1523
|
+
font-weight: bold;
|
|
1033
1524
|
}
|
|
1034
1525
|
.bond-edit-status .removed {
|
|
1035
1526
|
color: #f44336;
|
|
1527
|
+
font-weight: bold;
|
|
1036
1528
|
}
|
|
1037
1529
|
/* CellSelect: position at left of legend, show on hover */
|
|
1038
1530
|
.structure :global(.cell-select) {
|
|
@@ -1045,4 +1537,55 @@ $effect(() => {
|
|
|
1045
1537
|
opacity: 1;
|
|
1046
1538
|
pointer-events: auto;
|
|
1047
1539
|
}
|
|
1540
|
+
.undo-redo-container {
|
|
1541
|
+
display: flex;
|
|
1542
|
+
}
|
|
1543
|
+
.undo-redo-btn {
|
|
1544
|
+
position: relative;
|
|
1545
|
+
display: flex;
|
|
1546
|
+
align-items: center;
|
|
1547
|
+
justify-content: center;
|
|
1548
|
+
}
|
|
1549
|
+
.history-count {
|
|
1550
|
+
position: absolute;
|
|
1551
|
+
bottom: -2px;
|
|
1552
|
+
right: -2px;
|
|
1553
|
+
background: var(--accent-color, #007acc);
|
|
1554
|
+
color: white;
|
|
1555
|
+
border-radius: 50%;
|
|
1556
|
+
width: 12px;
|
|
1557
|
+
height: 12px;
|
|
1558
|
+
font-size: 8px;
|
|
1559
|
+
font-weight: bold;
|
|
1560
|
+
display: flex;
|
|
1561
|
+
align-items: center;
|
|
1562
|
+
justify-content: center;
|
|
1563
|
+
line-height: 1;
|
|
1564
|
+
pointer-events: none;
|
|
1565
|
+
z-index: 1;
|
|
1566
|
+
}
|
|
1567
|
+
.add-atom-input {
|
|
1568
|
+
display: flex;
|
|
1569
|
+
align-items: center;
|
|
1570
|
+
gap: 0.5em;
|
|
1571
|
+
background: color-mix(in srgb, var(--page-bg, Canvas) 85%, currentColor);
|
|
1572
|
+
color: var(--text-color, currentColor);
|
|
1573
|
+
padding: 0.3em 0.6em;
|
|
1574
|
+
border-radius: var(--border-radius, 3pt);
|
|
1575
|
+
font-size: 0.8rem;
|
|
1576
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
|
1577
|
+
label {
|
|
1578
|
+
display: flex;
|
|
1579
|
+
align-items: center;
|
|
1580
|
+
gap: 0.3em;
|
|
1581
|
+
}
|
|
1582
|
+
input {
|
|
1583
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
1584
|
+
border: 1px solid color-mix(in srgb, currentColor 20%, transparent);
|
|
1585
|
+
border-radius: 3px;
|
|
1586
|
+
color: inherit;
|
|
1587
|
+
font-size: 0.85rem;
|
|
1588
|
+
padding: 0.1em 0.3em;
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1048
1591
|
</style>
|