matterviz 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FilePicker.svelte +40 -50
- package/dist/api/optimade.js +10 -7
- package/dist/app.css +15 -0
- package/dist/brillouin/BrillouinZone.svelte +18 -9
- package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
- package/dist/brillouin/compute.js +10 -16
- package/dist/brillouin/geometry.js +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
- package/dist/chempot-diagram/async-compute.svelte.js +12 -13
- package/dist/chempot-diagram/color.d.ts +21 -1
- package/dist/chempot-diagram/color.js +70 -1
- package/dist/chempot-diagram/compute.d.ts +7 -0
- package/dist/chempot-diagram/compute.js +85 -26
- package/dist/chempot-diagram/export.js +1 -6
- package/dist/chempot-diagram/temperature.js +9 -10
- package/dist/composition/BarChart.svelte +7 -9
- package/dist/composition/Composition.svelte +3 -8
- package/dist/composition/Formula.svelte +2 -2
- package/dist/composition/FormulaFilter.svelte +27 -93
- package/dist/composition/format.js +6 -8
- package/dist/composition/parse.js +50 -105
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +6 -17
- package/dist/convex-hull/ConvexHull2D.svelte +0 -2
- package/dist/convex-hull/ConvexHull3D.svelte +19 -47
- package/dist/convex-hull/ConvexHull4D.svelte +65 -95
- package/dist/convex-hull/StructurePopup.svelte +1 -1
- package/dist/convex-hull/gas-thermodynamics.js +21 -53
- package/dist/convex-hull/helpers.js +1 -7
- package/dist/convex-hull/thermodynamics.js +19 -27
- package/dist/coordination/CoordinationBarPlot.svelte +12 -21
- package/dist/element/BohrAtom.svelte +0 -1
- package/dist/element/ElementTile.svelte +26 -44
- package/dist/element/data.d.ts +2 -0
- package/dist/element/data.js +5 -1
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -1
- package/dist/fermi-surface/FermiSurface.svelte +22 -23
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
- package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
- package/dist/fermi-surface/compute.js +22 -55
- package/dist/fermi-surface/parse.js +38 -70
- package/dist/file-viewer/JsonBrowser.svelte +1043 -0
- package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
- package/dist/file-viewer/PlotPanel.svelte +350 -0
- package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
- package/dist/file-viewer/detect.d.ts +11 -0
- package/dist/file-viewer/detect.js +381 -0
- package/dist/file-viewer/eligibility.d.ts +4 -0
- package/dist/file-viewer/eligibility.js +53 -0
- package/dist/file-viewer/host-protocol.d.ts +50 -0
- package/dist/file-viewer/host-transfer.d.ts +25 -0
- package/dist/file-viewer/host-transfer.js +42 -0
- package/dist/file-viewer/index.d.ts +5 -0
- package/dist/file-viewer/index.js +5 -0
- package/dist/file-viewer/main.d.ts +44 -0
- package/dist/file-viewer/main.js +535 -0
- package/dist/file-viewer/parse.d.ts +9 -0
- package/dist/file-viewer/parse.js +170 -0
- package/dist/file-viewer/plot-utils.d.ts +24 -0
- package/dist/file-viewer/plot-utils.js +191 -0
- package/dist/file-viewer/types.d.ts +4 -0
- package/dist/file-viewer/types.js +5 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/io/ExportPane.svelte +1 -1
- package/dist/io/decompress.d.ts +2 -0
- package/dist/io/decompress.js +3 -2
- package/dist/io/export.d.ts +7 -4
- package/dist/io/export.js +70 -18
- package/dist/io/file-drop.js +60 -35
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +28 -16
- package/dist/isosurface/Isosurface.svelte +687 -196
- package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
- package/dist/isosurface/IsosurfaceControls.svelte +564 -85
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
- package/dist/isosurface/VolumeSlice.svelte +224 -0
- package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
- package/dist/isosurface/coloring.d.ts +20 -0
- package/dist/isosurface/coloring.js +118 -0
- package/dist/isosurface/geometry-worker-types.d.ts +38 -0
- package/dist/isosurface/geometry.worker.d.ts +1 -0
- package/dist/isosurface/geometry.worker.js +59 -0
- package/dist/isosurface/grid.d.ts +10 -0
- package/dist/isosurface/grid.js +31 -0
- package/dist/isosurface/index.d.ts +4 -0
- package/dist/isosurface/index.js +4 -0
- package/dist/isosurface/profile.d.ts +12 -0
- package/dist/isosurface/profile.js +19 -0
- package/dist/isosurface/sampling.d.ts +34 -0
- package/dist/isosurface/sampling.js +408 -0
- package/dist/isosurface/slice-rendering.d.ts +13 -0
- package/dist/isosurface/slice-rendering.js +79 -0
- package/dist/isosurface/slice.d.ts +22 -2
- package/dist/isosurface/slice.js +200 -116
- package/dist/isosurface/types.d.ts +24 -1
- package/dist/isosurface/types.js +120 -8
- package/dist/json-path.d.ts +4 -0
- package/dist/json-path.js +89 -0
- package/dist/labels.js +1 -1
- package/dist/layout/InfoCard.svelte +1 -3
- package/dist/layout/SettingsSection.svelte +52 -61
- package/dist/layout/SettingsSection.svelte.d.ts +6 -7
- package/dist/layout/ViewerChrome.svelte +7 -1
- package/dist/layout/json-tree/JsonNode.svelte +14 -48
- package/dist/layout/json-tree/JsonTree.svelte +48 -95
- package/dist/layout/json-tree/JsonValue.svelte +3 -18
- package/dist/layout/json-tree/utils.d.ts +0 -3
- package/dist/layout/json-tree/utils.js +50 -179
- package/dist/marching-cubes.d.ts +8 -1
- package/dist/marching-cubes.js +115 -48
- package/dist/math.d.ts +3 -2
- package/dist/math.js +29 -38
- package/dist/overlays/ContextMenu.svelte +15 -32
- package/dist/overlays/DraggablePane.svelte +39 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
- package/dist/overlays/GlassChip.svelte +3 -1
- package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
- package/dist/phase-diagram/build-diagram.js +1 -6
- package/dist/phase-diagram/svg-to-diagram.js +9 -14
- package/dist/plot/bar/BarPlot.svelte +38 -42
- package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
- package/dist/plot/box/BoxPlot.svelte +27 -23
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +1 -1
- package/dist/plot/core/data-transform.d.ts +1 -2
- package/dist/plot/core/data-transform.js +0 -10
- package/dist/plot/core/layout.d.ts +12 -5
- package/dist/plot/core/layout.js +37 -27
- package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
- package/dist/plot/core/pan-zoom.svelte.js +34 -17
- package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -21
- package/dist/plot/core/svg.js +0 -1
- package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
- package/dist/plot/core/utils/hierarchy-chart.js +12 -0
- package/dist/plot/core/utils/hierarchy-labels.js +3 -2
- package/dist/plot/histogram/Histogram.svelte +21 -26
- package/dist/plot/sankey/sankey.js +13 -9
- package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
- package/dist/plot/scatter/ScatterPlot.svelte +67 -52
- package/dist/plot/scatter/adaptive-density.d.ts +1 -1
- package/dist/plot/scatter/adaptive-density.js +36 -20
- package/dist/plot/sunburst/Sunburst.svelte +20 -20
- package/dist/plot/treemap/Treemap.svelte +175 -60
- package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
- package/dist/plot/treemap/index.d.ts +1 -0
- package/dist/plot/treemap/labels.d.ts +38 -0
- package/dist/plot/treemap/labels.js +80 -0
- package/dist/plot/treemap/treemap.d.ts +4 -2
- package/dist/plot/treemap/treemap.js +5 -2
- package/dist/rdf/RdfPlot.svelte +17 -35
- package/dist/rdf/calc-rdf.js +59 -44
- package/dist/rdf/index.d.ts +0 -1
- package/dist/sanitize.js +84 -26
- package/dist/scene/SceneCamera.svelte +1 -1
- package/dist/scene/SceneCamera.svelte.d.ts +1 -1
- package/dist/scene/index.d.ts +1 -2
- package/dist/scene/index.js +1 -1
- package/dist/scene/{props.js → props.svelte.js} +15 -1
- package/dist/settings.js +159 -482
- package/dist/spectral/Bands.svelte +151 -158
- package/dist/spectral/BrillouinBandsDos.svelte +4 -19
- package/dist/spectral/Dos.svelte +26 -24
- package/dist/spectral/helpers.js +40 -105
- package/dist/structure/AtomLegend.svelte +8 -13
- package/dist/structure/AtomLegend.svelte.d.ts +2 -3
- package/dist/structure/CellSelect.svelte +35 -10
- package/dist/structure/Structure.svelte +177 -99
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureCarousel.svelte +10 -3
- package/dist/structure/StructureControls.svelte +8 -26
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +65 -76
- package/dist/structure/StructureScene.svelte +75 -109
- package/dist/structure/StructureScene.svelte.d.ts +3 -1
- package/dist/structure/StructureViewport.svelte +120 -55
- package/dist/structure/StructureViewport.svelte.d.ts +3 -1
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +4 -3
- package/dist/structure/bond-order-perception.js +3 -8
- package/dist/structure/bonding.d.ts +0 -1
- package/dist/structure/bonding.js +16 -23
- package/dist/structure/export.js +50 -74
- package/dist/structure/format-detect.js +5 -7
- package/dist/structure/index.js +6 -12
- package/dist/structure/parse.js +38 -100
- package/dist/structure/partial-occupancy.js +7 -4
- package/dist/structure/pbc.js +8 -10
- package/dist/structure/polyhedra.js +5 -4
- package/dist/structure/prop-groups.d.ts +85 -0
- package/dist/structure/prop-groups.js +18 -0
- package/dist/symmetry/SymmetryStats.svelte +0 -4
- package/dist/symmetry/WyckoffTable.svelte +5 -4
- package/dist/symmetry/cell-transform.js +5 -14
- package/dist/symmetry/index.js +6 -9
- package/dist/table/HeatmapTable.svelte +61 -103
- package/dist/table/ToggleMenu.svelte +166 -44
- package/dist/table/ToggleMenu.svelte.d.ts +1 -1
- package/dist/theme/embedded.d.ts +18 -0
- package/dist/theme/embedded.js +206 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +1 -2
- package/dist/theme/themes.mjs +31 -60
- package/dist/tooltip/TooltipContent.svelte +1 -1
- package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
- package/dist/tooltip/index.d.ts +8 -1
- package/dist/trajectory/Trajectory.svelte +140 -120
- package/dist/trajectory/TrajectoryError.svelte +1 -66
- package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
- package/dist/trajectory/extract.js +19 -29
- package/dist/trajectory/format-detect.d.ts +2 -0
- package/dist/trajectory/format-detect.js +7 -5
- package/dist/trajectory/frame-reader.js +3 -4
- package/dist/trajectory/helpers.js +16 -49
- package/dist/trajectory/index.js +2 -8
- package/dist/trajectory/parse/ase.js +1 -1
- package/dist/trajectory/parse/index.d.ts +4 -3
- package/dist/trajectory/parse/index.js +12 -17
- package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
- package/dist/trajectory/parse/vaspout-h5.js +1 -4
- package/dist/trajectory/parse/xyz.js +48 -2
- package/dist/trajectory/plotting.js +14 -37
- package/dist/xrd/XrdPlot.svelte +43 -68
- package/dist/xrd/calc-xrd.js +22 -58
- package/dist/xrd/parse.js +13 -22
- package/package.json +40 -8
- package/readme.md +6 -5
- package/dist/scene/types.d.ts +0 -26
- package/dist/tooltip/types.d.ts +0 -8
- package/dist/trajectory/constants.d.ts +0 -6
- package/dist/trajectory/constants.js +0 -11
- /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
- /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
|
@@ -6,14 +6,17 @@
|
|
|
6
6
|
import { StatusMessage } from '../feedback'
|
|
7
7
|
import Spinner from '../feedback/Spinner.svelte'
|
|
8
8
|
import Icon from '../Icon.svelte'
|
|
9
|
-
import
|
|
9
|
+
import * as io from '../io'
|
|
10
10
|
import { forward_window_keydown, handle_and_prevent } from '../keyboard'
|
|
11
11
|
import { parse_volumetric_file } from '../isosurface/parse'
|
|
12
12
|
import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
|
|
13
13
|
import {
|
|
14
14
|
auto_isosurface_settings,
|
|
15
15
|
DEFAULT_ISOSURFACE_SETTINGS,
|
|
16
|
-
|
|
16
|
+
label_file_volumes,
|
|
17
|
+
lattices_match,
|
|
18
|
+
materialize_layers,
|
|
19
|
+
merge_imported_volumes,
|
|
17
20
|
} from '../isosurface/types'
|
|
18
21
|
import { type FullscreenToggleProp, toggle_fullscreen, ViewerChrome } from '../layout'
|
|
19
22
|
import { sync_fullscreen } from '../layout/fullscreen.svelte'
|
|
@@ -302,23 +305,39 @@
|
|
|
302
305
|
// completions so a newer data_url (or an externally-supplied structure, via the cleanup)
|
|
303
306
|
// can't be clobbered by a slow earlier fetch.
|
|
304
307
|
let data_url_load_id = 0
|
|
308
|
+
let loaded_data_url: string | undefined
|
|
309
|
+
let url_owned_structure: AnyStructure | undefined
|
|
310
|
+
|
|
305
311
|
$effect(() => {
|
|
306
|
-
|
|
312
|
+
const requested_url = data_url
|
|
313
|
+
const current_structure = structure
|
|
314
|
+
// Host on_file_drop owns the structure; don't treat it as caller-owned cancel.
|
|
315
|
+
const caller_owns_structure = Boolean(
|
|
316
|
+
!on_file_drop && current_structure && current_structure !== url_owned_structure,
|
|
317
|
+
)
|
|
318
|
+
if (!requested_url || caller_owns_structure) {
|
|
319
|
+
loaded_data_url = undefined
|
|
320
|
+
url_owned_structure = undefined
|
|
321
|
+
return
|
|
322
|
+
}
|
|
323
|
+
if (loaded_data_url === requested_url) return
|
|
324
|
+
|
|
307
325
|
const load_id = ++data_url_load_id
|
|
308
326
|
const is_current = () => load_id === data_url_load_id
|
|
309
327
|
loading = true
|
|
310
328
|
error_msg = undefined
|
|
311
329
|
|
|
312
|
-
load_from_url(
|
|
330
|
+
io.load_from_url(requested_url, (content, filename) => {
|
|
313
331
|
if (!is_current()) return // stale response
|
|
314
|
-
if (on_file_drop)
|
|
315
|
-
|
|
332
|
+
if (on_file_drop) {
|
|
333
|
+
on_file_drop(content, filename)
|
|
334
|
+
loaded_data_url = requested_url
|
|
335
|
+
} else {
|
|
316
336
|
// Parse structure internally when no handler provided
|
|
317
337
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
emit_file_load_event(parsed, filename, content)
|
|
338
|
+
parse_and_emit_file(content, filename)
|
|
339
|
+
url_owned_structure = structure
|
|
340
|
+
loaded_data_url = requested_url
|
|
322
341
|
} catch (error) {
|
|
323
342
|
error_msg = `Failed to parse structure: ${to_error(error).message}`
|
|
324
343
|
on_error?.({ error_msg, filename })
|
|
@@ -329,7 +348,7 @@
|
|
|
329
348
|
if (!is_current()) return
|
|
330
349
|
console.error(`Failed to load structure from URL:`, error)
|
|
331
350
|
error_msg = `Failed to load structure: ${error.message}`
|
|
332
|
-
on_error?.({ error_msg, filename:
|
|
351
|
+
on_error?.({ error_msg, filename: io.basename_from_url(requested_url) })
|
|
333
352
|
})
|
|
334
353
|
.finally(() => {
|
|
335
354
|
if (is_current()) loading = false
|
|
@@ -680,6 +699,14 @@
|
|
|
680
699
|
// Flag set before internal edits (undo/redo/delete/add/move) to distinguish
|
|
681
700
|
// them from external structure changes (file load, trajectory step, etc.)
|
|
682
701
|
let is_internal_edit = false
|
|
702
|
+
// Claim URL ownership before regular effects so internal edits aren't treated as caller-owned.
|
|
703
|
+
$effect.pre(() => {
|
|
704
|
+
void structure
|
|
705
|
+
if (is_internal_edit && loaded_data_url && loaded_data_url === data_url && structure) {
|
|
706
|
+
url_owned_structure = structure
|
|
707
|
+
}
|
|
708
|
+
})
|
|
709
|
+
|
|
683
710
|
// Add-atom sub-mode state (bound to StructureScene)
|
|
684
711
|
let add_atom_mode = $state(false)
|
|
685
712
|
let add_element = $state<ElementSymbol>(`C` as ElementSymbol)
|
|
@@ -830,7 +857,9 @@
|
|
|
830
857
|
$effect(() => {
|
|
831
858
|
viewer_active = hovered || focused
|
|
832
859
|
})
|
|
833
|
-
|
|
860
|
+
// Keep the gizmo mounted whenever enabled — toggling via `{#if gizmo}` on hover remounts
|
|
861
|
+
// OrbitControls/Gizmo and resets camera rotation. Visibility is CSS-gated by `gizmo-visible`.
|
|
862
|
+
let scene_gizmo = $derived(scene_props.gizmo ?? scene_props.show_gizmo)
|
|
834
863
|
let active_scene_sites = $derived([
|
|
835
864
|
...new SvelteSet([...(scene_props.active_sites ?? []), ...highlighted_sites]),
|
|
836
865
|
])
|
|
@@ -898,16 +927,16 @@
|
|
|
898
927
|
})
|
|
899
928
|
})
|
|
900
929
|
|
|
901
|
-
//
|
|
902
|
-
//
|
|
903
|
-
//
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
if (!
|
|
930
|
+
// Supercell tiling factors for isosurface geometry (tiling itself happens in
|
|
931
|
+
// the Isosurface component so color sampling can use the untiled full-res
|
|
932
|
+
// volumes). Gate on !supercell_loading so tiled surfaces and the supercell
|
|
933
|
+
// structure update in the same frame (large supercells defer via setTimeout).
|
|
934
|
+
let volume_scaling = $derived.by((): Vec3 => {
|
|
935
|
+
if (!has_supercell || supercell_loading) return [1, 1, 1]
|
|
907
936
|
try {
|
|
908
|
-
return
|
|
937
|
+
return parse_supercell_scaling(supercell_scaling)
|
|
909
938
|
} catch {
|
|
910
|
-
return
|
|
939
|
+
return [1, 1, 1]
|
|
911
940
|
}
|
|
912
941
|
})
|
|
913
942
|
|
|
@@ -1034,7 +1063,9 @@
|
|
|
1034
1063
|
scene_props,
|
|
1035
1064
|
gizmo: scene_gizmo,
|
|
1036
1065
|
lattice_props,
|
|
1037
|
-
volumetric_data
|
|
1066
|
+
volumetric_data,
|
|
1067
|
+
volume_scaling,
|
|
1068
|
+
active_volume_idx,
|
|
1038
1069
|
isosurface_settings,
|
|
1039
1070
|
bond_edits_enabled,
|
|
1040
1071
|
bond_edit_order,
|
|
@@ -1100,18 +1131,65 @@
|
|
|
1100
1131
|
total_atoms: loaded_structure.sites?.length || 0,
|
|
1101
1132
|
})
|
|
1102
1133
|
|
|
1134
|
+
// The currently loaded structure's lattice matrix (undefined for molecules/none)
|
|
1135
|
+
const current_lattice_matrix = () =>
|
|
1136
|
+
structure && `lattice` in structure ? structure.lattice.matrix : undefined
|
|
1137
|
+
const shares_current_lattice = (matrix: Parameters<typeof lattices_match>[1]): boolean =>
|
|
1138
|
+
lattices_match(current_lattice_matrix(), matrix)
|
|
1139
|
+
|
|
1103
1140
|
// Try to parse content as a volumetric file, setting both structure and volumetric data.
|
|
1104
1141
|
// Delegates format detection entirely to parse_volumetric_file (filename + content sniffing).
|
|
1142
|
+
// When the file describes the same cell as already-loaded volumes, its volumes are
|
|
1143
|
+
// APPENDED (or replaced in place on re-import of the same source file) so multiple
|
|
1144
|
+
// fields — e.g. density + ESP — coexist and can cross-color each other's isosurfaces.
|
|
1145
|
+
// A file with a different lattice replaces the current structure and volumes.
|
|
1105
1146
|
// Returns the parsed structure on success, or null if the file isn't a volumetric format.
|
|
1106
1147
|
function try_parse_volumetric(text_content: string, filename: string): AnyStructure | null {
|
|
1107
1148
|
const vol_result = parse_volumetric_file(text_content, filename)
|
|
1108
1149
|
if (!vol_result) return null
|
|
1150
|
+
|
|
1151
|
+
const incoming = label_file_volumes(vol_result.volumes, filename)
|
|
1152
|
+
const same_cell = shares_current_lattice(vol_result.structure.lattice?.matrix)
|
|
1153
|
+
const added_toast = (count: number) =>
|
|
1154
|
+
`Added ${count} volume${count > 1 ? `s` : ``} from ${filename}`
|
|
1155
|
+
|
|
1156
|
+
if (same_cell && structure) {
|
|
1157
|
+
// Same cell: keep the structure and camera, only update volumes
|
|
1158
|
+
if (volumetric_data?.length) {
|
|
1159
|
+
// Materialize the implicit single surface into explicit layers so existing
|
|
1160
|
+
// surfaces survive the transition to multi-volume mode
|
|
1161
|
+
const merged = merge_imported_volumes(
|
|
1162
|
+
volumetric_data,
|
|
1163
|
+
materialize_layers(isosurface_settings, active_volume_idx),
|
|
1164
|
+
incoming,
|
|
1165
|
+
active_volume_idx,
|
|
1166
|
+
)
|
|
1167
|
+
volumetric_data = merged.volumes
|
|
1168
|
+
isosurface_settings = { ...isosurface_settings, layers: merged.layers }
|
|
1169
|
+
active_volume_idx = merged.first_touched_idx
|
|
1170
|
+
show_toast(
|
|
1171
|
+
merged.n_added > 0
|
|
1172
|
+
? added_toast(merged.n_added)
|
|
1173
|
+
: `Reloaded volumes from ${filename}`,
|
|
1174
|
+
)
|
|
1175
|
+
} else if (incoming[0]) {
|
|
1176
|
+
// First volumetric file for this structure
|
|
1177
|
+
volumetric_data = incoming
|
|
1178
|
+
isosurface_settings = auto_isosurface_settings(incoming[0].data_range)
|
|
1179
|
+
active_volume_idx = 0
|
|
1180
|
+
show_toast(added_toast(incoming.length))
|
|
1181
|
+
}
|
|
1182
|
+
return structure
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
// Replace: new system (or nothing loaded yet)
|
|
1186
|
+
clear_camera_state()
|
|
1109
1187
|
// parse_volumetric_file extracts structure from file header;
|
|
1110
1188
|
// parsers set pbc so the lattice conforms to Crystal's LatticeType
|
|
1111
1189
|
structure = vol_result.structure as AnyStructure
|
|
1112
|
-
volumetric_data =
|
|
1190
|
+
volumetric_data = incoming
|
|
1113
1191
|
// Auto-compute reasonable isosurface settings from data range
|
|
1114
|
-
const vol =
|
|
1192
|
+
const vol = incoming[0]
|
|
1115
1193
|
if (vol) {
|
|
1116
1194
|
isosurface_settings = auto_isosurface_settings(vol.data_range)
|
|
1117
1195
|
active_volume_idx = 0
|
|
@@ -1122,30 +1200,36 @@
|
|
|
1122
1200
|
// Parse file content, trying volumetric format first then falling back to plain structure.
|
|
1123
1201
|
// Returns the parsed structure on success, throws on failure.
|
|
1124
1202
|
function parse_file_content(text_content: string, filename: string): AnyStructure {
|
|
1125
|
-
clear_camera_state()
|
|
1126
1203
|
const vol_struct = try_parse_volumetric(text_content, filename)
|
|
1127
1204
|
if (vol_struct) return vol_struct
|
|
1128
|
-
// Clear stale volumetric data when loading a non-volumetric file
|
|
1129
|
-
volumetric_data = []
|
|
1130
1205
|
const parsed = parse_any_structure(text_content, filename)
|
|
1131
1206
|
if (!parsed) throw new Error(`Failed to parse structure from ${filename}`)
|
|
1207
|
+
// Keep loaded volumes and camera when the new structure describes the same
|
|
1208
|
+
// cell (e.g. a mixed batch drop of CHGCAR + POSCAR, in either order);
|
|
1209
|
+
// clear both for a genuinely new system
|
|
1210
|
+
const same_cell = shares_current_lattice(
|
|
1211
|
+
`lattice` in parsed ? parsed.lattice?.matrix : undefined,
|
|
1212
|
+
)
|
|
1213
|
+
if (!same_cell) {
|
|
1214
|
+
clear_camera_state()
|
|
1215
|
+
volumetric_data = []
|
|
1216
|
+
}
|
|
1132
1217
|
structure = parsed
|
|
1133
1218
|
return parsed
|
|
1134
1219
|
}
|
|
1135
1220
|
|
|
1136
|
-
|
|
1221
|
+
function parse_and_emit_file(content: string | ArrayBuffer, filename: string): void {
|
|
1222
|
+
const text = content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
|
|
1223
|
+
emit_file_load_event(parse_file_content(text, filename), filename, content)
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const handle_file_drop = io.create_file_drop_handler({
|
|
1137
1227
|
allow: () => allow_file_drop,
|
|
1228
|
+
// Parse errors propagate so multi-file batches aggregate all failures into
|
|
1229
|
+
// one message instead of the last error overwriting earlier ones
|
|
1138
1230
|
on_drop: (content, filename) => {
|
|
1139
1231
|
if (on_file_drop) return on_file_drop(content, filename)
|
|
1140
|
-
|
|
1141
|
-
const text_content =
|
|
1142
|
-
content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
|
|
1143
|
-
const parsed = parse_file_content(text_content, filename)
|
|
1144
|
-
emit_file_load_event(parsed, filename, content)
|
|
1145
|
-
} catch (err) {
|
|
1146
|
-
error_msg = `Failed to parse structure: ${to_error(err).message}`
|
|
1147
|
-
on_error?.({ error_msg, filename })
|
|
1148
|
-
}
|
|
1232
|
+
parse_and_emit_file(content, filename)
|
|
1149
1233
|
},
|
|
1150
1234
|
on_error: (msg) => {
|
|
1151
1235
|
error_msg = msg
|
|
@@ -1470,13 +1554,14 @@
|
|
|
1470
1554
|
|
|
1471
1555
|
// Only set background override when background_color is explicitly provided
|
|
1472
1556
|
$effect(() => {
|
|
1473
|
-
if (
|
|
1557
|
+
if (!wrapper) return
|
|
1558
|
+
if (background_color) {
|
|
1474
1559
|
// Convert opacity (0-1) to hex alpha value (00-FF)
|
|
1475
1560
|
const alpha_hex = Math.round(background_opacity * 255)
|
|
1476
1561
|
.toString(16)
|
|
1477
1562
|
.padStart(2, `0`)
|
|
1478
1563
|
wrapper.style.setProperty(`--struct-bg-override`, `${background_color}${alpha_hex}`)
|
|
1479
|
-
} else
|
|
1564
|
+
} else {
|
|
1480
1565
|
// Remove override to use theme system
|
|
1481
1566
|
wrapper.style.removeProperty(`--struct-bg-override`)
|
|
1482
1567
|
}
|
|
@@ -1500,7 +1585,7 @@
|
|
|
1500
1585
|
<div
|
|
1501
1586
|
class:dragover
|
|
1502
1587
|
class:active={info_pane_open || controls_open || export_pane_open}
|
|
1503
|
-
class:gizmo-visible={Boolean(scene_gizmo)}
|
|
1588
|
+
class:gizmo-visible={viewer_active && Boolean(scene_gizmo)}
|
|
1504
1589
|
class:multi-view={multi_view}
|
|
1505
1590
|
role="application"
|
|
1506
1591
|
tabindex="0"
|
|
@@ -1517,7 +1602,7 @@
|
|
|
1517
1602
|
}
|
|
1518
1603
|
}}
|
|
1519
1604
|
ondrop={handle_file_drop}
|
|
1520
|
-
{...drag_over_handlers({
|
|
1605
|
+
{...io.drag_over_handlers({
|
|
1521
1606
|
allow: () => allow_file_drop,
|
|
1522
1607
|
set_dragover: (over) => (dragover = over),
|
|
1523
1608
|
})}
|
|
@@ -1555,7 +1640,7 @@
|
|
|
1555
1640
|
fullscreen_btn_style="padding: 0 3px"
|
|
1556
1641
|
{wrapper}
|
|
1557
1642
|
before={reset_camera_btn}
|
|
1558
|
-
style="--viewer-buttons-gap: 4pt; --viewer-buttons-btn-padding: 1px 6px; --viewer-buttons-align: stretch"
|
|
1643
|
+
style="--viewer-buttons-gap: 4pt; --viewer-buttons-btn-padding: 1px 6px; --viewer-buttons-align: stretch; --viewer-buttons-hover-bg: transparent; --viewer-buttons-hover-color: light-dark(#000, #fff)"
|
|
1559
1644
|
>
|
|
1560
1645
|
{#if controls_config.visible(`multi-view`)}
|
|
1561
1646
|
<button
|
|
@@ -1639,36 +1724,43 @@
|
|
|
1639
1724
|
{/if}
|
|
1640
1725
|
</div>
|
|
1641
1726
|
|
|
1642
|
-
|
|
1643
|
-
|
|
1727
|
+
{#snippet undo_redo_snippet(
|
|
1728
|
+
buttons: {
|
|
1729
|
+
icon: `Undo` | `Redo`
|
|
1730
|
+
title: string
|
|
1731
|
+
stack: unknown[]
|
|
1732
|
+
action: () => void
|
|
1733
|
+
}[],
|
|
1734
|
+
)}
|
|
1644
1735
|
<div class="undo-redo-container">
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
aria-label="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
|
|
1661
|
-
disabled={redo_stack.length === 0}
|
|
1662
|
-
onclick={redo}
|
|
1663
|
-
title="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
|
|
1664
|
-
class="undo-redo-btn"
|
|
1665
|
-
>
|
|
1666
|
-
<Icon icon="Redo" />
|
|
1667
|
-
{#if redo_stack.length > 0}
|
|
1668
|
-
<span class="history-count">{redo_stack.length}</span>
|
|
1669
|
-
{/if}
|
|
1670
|
-
</button>
|
|
1736
|
+
{#each buttons as { icon, title, stack, action } (icon)}
|
|
1737
|
+
<button
|
|
1738
|
+
type="button"
|
|
1739
|
+
aria-label={title}
|
|
1740
|
+
disabled={stack.length === 0}
|
|
1741
|
+
onclick={action}
|
|
1742
|
+
{title}
|
|
1743
|
+
class="undo-redo-btn"
|
|
1744
|
+
>
|
|
1745
|
+
<Icon {icon} />
|
|
1746
|
+
{#if stack.length > 0}
|
|
1747
|
+
<span class="history-count">{stack.length}</span>
|
|
1748
|
+
{/if}
|
|
1749
|
+
</button>
|
|
1750
|
+
{/each}
|
|
1671
1751
|
</div>
|
|
1752
|
+
{/snippet}
|
|
1753
|
+
|
|
1754
|
+
{#if measure_mode === `edit-atoms`}
|
|
1755
|
+
{@render undo_redo_snippet([
|
|
1756
|
+
{ icon: `Undo`, title: `Undo (Cmd/Ctrl+Z)`, stack: undo_stack, action: undo },
|
|
1757
|
+
{
|
|
1758
|
+
icon: `Redo`,
|
|
1759
|
+
title: `Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)`,
|
|
1760
|
+
stack: redo_stack,
|
|
1761
|
+
action: redo,
|
|
1762
|
+
},
|
|
1763
|
+
])}
|
|
1672
1764
|
{/if}
|
|
1673
1765
|
|
|
1674
1766
|
{#if measure_mode === `edit-bonds`}
|
|
@@ -1697,34 +1789,20 @@
|
|
|
1697
1789
|
{/each}
|
|
1698
1790
|
</div>
|
|
1699
1791
|
</div>
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
<button
|
|
1715
|
-
type="button"
|
|
1716
|
-
aria-label="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
|
|
1717
|
-
disabled={bond_redo_stack.length === 0}
|
|
1718
|
-
onclick={redo_bond_edit}
|
|
1719
|
-
title="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
|
|
1720
|
-
class="undo-redo-btn"
|
|
1721
|
-
>
|
|
1722
|
-
<Icon icon="Redo" />
|
|
1723
|
-
{#if bond_redo_stack.length > 0}
|
|
1724
|
-
<span class="history-count">{bond_redo_stack.length}</span>
|
|
1725
|
-
{/if}
|
|
1726
|
-
</button>
|
|
1727
|
-
</div>
|
|
1792
|
+
{@render undo_redo_snippet([
|
|
1793
|
+
{
|
|
1794
|
+
icon: `Undo`,
|
|
1795
|
+
title: `Undo bond edit (Cmd/Ctrl+Z)`,
|
|
1796
|
+
stack: bond_undo_stack,
|
|
1797
|
+
action: undo_bond_edit,
|
|
1798
|
+
},
|
|
1799
|
+
{
|
|
1800
|
+
icon: `Redo`,
|
|
1801
|
+
title: `Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)`,
|
|
1802
|
+
stack: bond_redo_stack,
|
|
1803
|
+
action: redo_bond_edit,
|
|
1804
|
+
},
|
|
1805
|
+
])}
|
|
1728
1806
|
{/if}
|
|
1729
1807
|
|
|
1730
1808
|
<!-- Add-atom element input (shown when add_atom_mode is active) -->
|
|
@@ -2098,7 +2176,7 @@
|
|
|
2098
2176
|
gap: 1rem;
|
|
2099
2177
|
max-width: min(90%, 400px);
|
|
2100
2178
|
font-size: 0.9rem;
|
|
2101
|
-
z-index: 1000;
|
|
2179
|
+
z-index: var(--z-index-viewer-tooltip, 1000);
|
|
2102
2180
|
}
|
|
2103
2181
|
.symmetry-error span {
|
|
2104
2182
|
flex: 1;
|
|
@@ -2125,7 +2203,7 @@
|
|
|
2125
2203
|
padding: 0.4rem 0.8rem;
|
|
2126
2204
|
border-radius: var(--border-radius, 3pt);
|
|
2127
2205
|
font-size: 0.8rem;
|
|
2128
|
-
z-index: 100;
|
|
2206
|
+
z-index: var(--z-index-viewer-dropdown, 100);
|
|
2129
2207
|
pointer-events: none;
|
|
2130
2208
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
|
|
2131
2209
|
animation: toast-fade 2s ease-in-out;
|
|
@@ -76,6 +76,6 @@ type $$ComponentProps = {
|
|
|
76
76
|
on_camera_reset?: EventHandler;
|
|
77
77
|
on_bonds_change?: (bonds: StructureBond[] | undefined) => void;
|
|
78
78
|
} & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`> & Omit<HTMLAttributes<HTMLDivElement>, `children`>;
|
|
79
|
-
declare const Structure: import("svelte").Component<$$ComponentProps, {}, "height" | "width" | "dragover" | "color_scheme" | "fullscreen" | "wrapper" | "hovered" | "controls_open" | "structure" | "background_color" | "background_opacity" | "show_image_atoms" | "sym_data" | "lattice_props" | "measure_mode" | "selected_sites" | "measured_sites" | "bond_edit_mode" | "bond_edit_order" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "site_radius_overrides" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "hovered_site_idx" | "bonds" | "element_mapping" | "supercell_scaling" | "cell_type" | "loading" | "
|
|
79
|
+
declare const Structure: import("svelte").Component<$$ComponentProps, {}, "height" | "width" | "dragover" | "color_scheme" | "fullscreen" | "wrapper" | "hovered" | "controls_open" | "structure" | "active_volume_idx" | "background_color" | "background_opacity" | "show_image_atoms" | "sym_data" | "lattice_props" | "measure_mode" | "selected_sites" | "measured_sites" | "bond_edit_mode" | "bond_edit_order" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "site_radius_overrides" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "hovered_site_idx" | "bonds" | "element_mapping" | "supercell_scaling" | "cell_type" | "loading" | "png_dpi" | "highlighted_sites" | "info_pane_open" | "multi_view" | "enable_measure_mode" | "error_msg" | "performance_mode" | "displayed_structure" | "symmetry_settings">;
|
|
80
80
|
type Structure = ReturnType<typeof Structure>;
|
|
81
81
|
export default Structure;
|
|
@@ -531,6 +531,8 @@
|
|
|
531
531
|
.structure-carousel-pager {
|
|
532
532
|
position: absolute;
|
|
533
533
|
z-index: 6;
|
|
534
|
+
/* own compositing layer, or WKWebView paints the canvas over this (see app.css) */
|
|
535
|
+
will-change: transform;
|
|
534
536
|
inset-block-end: 12px;
|
|
535
537
|
inset-inline-start: 50%;
|
|
536
538
|
display: flex;
|
|
@@ -549,17 +551,22 @@
|
|
|
549
551
|
/* hosted in a panel title bar instead of floating over the cards */
|
|
550
552
|
.structure-carousel-pager.portaled {
|
|
551
553
|
position: static;
|
|
552
|
-
|
|
554
|
+
gap: 1px;
|
|
555
|
+
padding: 0 2px;
|
|
553
556
|
background: transparent;
|
|
554
557
|
box-shadow: none;
|
|
555
558
|
transform: none;
|
|
556
559
|
}
|
|
560
|
+
.structure-carousel-pager.portaled button {
|
|
561
|
+
inline-size: 18px;
|
|
562
|
+
block-size: 18px;
|
|
563
|
+
}
|
|
557
564
|
.structure-carousel-pager button {
|
|
558
565
|
display: grid;
|
|
559
566
|
place-items: center;
|
|
560
567
|
inline-size: 22px;
|
|
561
568
|
block-size: 22px;
|
|
562
|
-
padding: 0;
|
|
569
|
+
padding: 0 0 2px;
|
|
563
570
|
border: 0;
|
|
564
571
|
border-radius: 50%;
|
|
565
572
|
background: transparent;
|
|
@@ -570,7 +577,7 @@
|
|
|
570
577
|
line-height: 1;
|
|
571
578
|
}
|
|
572
579
|
.structure-carousel-pager button:hover:not(:disabled) {
|
|
573
|
-
|
|
580
|
+
color: var(--active-color, #6ea8ff);
|
|
574
581
|
}
|
|
575
582
|
.structure-carousel-pager button:disabled {
|
|
576
583
|
opacity: 0.3;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { PaneProps, PaneToggleProps } from '../overlays'
|
|
3
3
|
import type { ColorSchemeName, D3InterpolateName } from '../colors'
|
|
4
4
|
import { AXIS_COLORS, ELEMENT_COLOR_SCHEMES } from '../colors'
|
|
5
|
+
import Spinner from '../feedback/Spinner.svelte'
|
|
5
6
|
import IsosurfaceControls from '../isosurface/IsosurfaceControls.svelte'
|
|
6
7
|
import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
|
|
7
8
|
import { format_num } from '../labels'
|
|
@@ -286,17 +287,11 @@
|
|
|
286
287
|
scene_props.rotation = [...scene_props.rotation]
|
|
287
288
|
}
|
|
288
289
|
|
|
289
|
-
//
|
|
290
|
+
// Sample colors for common elements, used to preview an element color scheme
|
|
290
291
|
function get_representative_colors(scheme_name: string): string[] {
|
|
291
292
|
const scheme = ELEMENT_COLOR_SCHEMES[scheme_name as ColorSchemeName]
|
|
292
293
|
if (!scheme) return []
|
|
293
|
-
|
|
294
|
-
// Get colors for common elements: H, C, N, O, Fe, Ca, Si, Al
|
|
295
|
-
const sample_elements = [`H`, `C`, `N`, `O`, `Fe`, `Ca`, `Si`, `Al`]
|
|
296
|
-
return sample_elements
|
|
297
|
-
.slice(0, 4) // Take first 4
|
|
298
|
-
.map((el) => scheme[el] || scheme.H || `#cccccc`)
|
|
299
|
-
.filter(Boolean)
|
|
294
|
+
return [`H`, `C`, `N`, `O`].map((elem) => scheme[elem] || scheme.H || `#cccccc`)
|
|
300
295
|
}
|
|
301
296
|
</script>
|
|
302
297
|
|
|
@@ -318,7 +313,7 @@
|
|
|
318
313
|
{#if volumetric_data?.length && isosurface_settings}
|
|
319
314
|
<IsosurfaceControls
|
|
320
315
|
bind:settings={isosurface_settings}
|
|
321
|
-
volumes={volumetric_data}
|
|
316
|
+
bind:volumes={volumetric_data}
|
|
322
317
|
bind:active_volume_idx
|
|
323
318
|
/>
|
|
324
319
|
{/if}
|
|
@@ -923,15 +918,10 @@
|
|
|
923
918
|
/>
|
|
924
919
|
</label>
|
|
925
920
|
{#if supercell_loading}
|
|
926
|
-
<
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
class="spinner-icon"
|
|
931
|
-
style="display: inline-block; width: 12px; height: 12px; border: 2px solid currentColor; border-right-color: transparent; border-radius: 50%; animation: spin 0.8s linear infinite"
|
|
932
|
-
></span>
|
|
933
|
-
<span>Generating supercell...</span>
|
|
934
|
-
</div>
|
|
921
|
+
<Spinner
|
|
922
|
+
text="Generating supercell..."
|
|
923
|
+
style="--spinner-size: 12px; --spinner-border-width: 2px; --spinner-margin: 4pt 0 0; font-size: 0.85em; color: var(--accent-color)"
|
|
924
|
+
/>
|
|
935
925
|
{/if}
|
|
936
926
|
|
|
937
927
|
{#if !supercell_input_valid}
|
|
@@ -1235,12 +1225,4 @@
|
|
|
1235
1225
|
flex: 1;
|
|
1236
1226
|
min-width: 40px;
|
|
1237
1227
|
}
|
|
1238
|
-
@keyframes spin {
|
|
1239
|
-
from {
|
|
1240
|
-
transform: rotate(0deg);
|
|
1241
|
-
}
|
|
1242
|
-
to {
|
|
1243
|
-
transform: rotate(360deg);
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
1228
|
</style>
|
|
@@ -28,6 +28,6 @@ type $$ComponentProps = Omit<ComponentProps<typeof DraggablePane>, `children`> &
|
|
|
28
28
|
pane_props?: PaneProps;
|
|
29
29
|
toggle_props?: PaneToggleProps;
|
|
30
30
|
};
|
|
31
|
-
declare const StructureControls: import("svelte").Component<$$ComponentProps, {}, "color_scheme" | "controls_open" | "background_color" | "background_opacity" | "show_image_atoms" | "lattice_props" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "supercell_scaling" | "cell_type" | "
|
|
31
|
+
declare const StructureControls: import("svelte").Component<$$ComponentProps, {}, "color_scheme" | "controls_open" | "active_volume_idx" | "background_color" | "background_opacity" | "show_image_atoms" | "lattice_props" | "atom_color_config" | "volumetric_data" | "isosurface_settings" | "scene_props" | "supercell_scaling" | "cell_type" | "supercell_loading">;
|
|
32
32
|
type StructureControls = ReturnType<typeof StructureControls>;
|
|
33
33
|
export default StructureControls;
|