matterviz 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/EmptyState.svelte +10 -2
- package/dist/FilePicker.svelte +123 -82
- package/dist/Icon.svelte +18 -12
- package/dist/MillerIndexInput.svelte +27 -21
- package/dist/api/optimade.js +6 -6
- package/dist/app.css +216 -207
- package/dist/brillouin/BrillouinZone.svelte +292 -149
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
- package/dist/brillouin/BrillouinZoneExportPane.svelte +69 -42
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
- package/dist/brillouin/BrillouinZoneScene.svelte +275 -163
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
- package/dist/brillouin/compute.js +11 -6
- package/dist/chempot-diagram/ChemPotDiagram.svelte +162 -27
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +451 -281
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2148 -1642
- package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -5
- package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
- package/dist/chempot-diagram/async-compute.svelte.js +77 -0
- package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
- package/dist/chempot-diagram/chempot-worker.js +11 -0
- package/dist/chempot-diagram/color.js +1 -2
- package/dist/chempot-diagram/compute.d.ts +10 -0
- package/dist/chempot-diagram/compute.js +250 -88
- package/dist/chempot-diagram/index.d.ts +2 -1
- package/dist/chempot-diagram/index.js +2 -1
- package/dist/chempot-diagram/temperature.js +8 -9
- package/dist/chempot-diagram/types.d.ts +3 -0
- package/dist/chempot-diagram/types.js +1 -0
- package/dist/colors/index.d.ts +1 -1
- package/dist/colors/index.js +5 -3
- package/dist/composition/BarChart.svelte +128 -55
- package/dist/composition/BubbleChart.svelte +102 -49
- package/dist/composition/Composition.svelte +100 -79
- package/dist/composition/Formula.svelte +108 -62
- package/dist/composition/FormulaFilter.svelte +665 -537
- package/dist/composition/PieChart.svelte +183 -108
- package/dist/composition/format.d.ts +5 -0
- package/dist/composition/format.js +20 -3
- package/dist/composition/parse.js +14 -9
- package/dist/convex-hull/ConvexHull.svelte +93 -40
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull2D.svelte +549 -360
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +1296 -827
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +1004 -688
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullControls.svelte +115 -28
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
- package/dist/convex-hull/ConvexHullStats.svelte +425 -328
- package/dist/convex-hull/ConvexHullTooltip.svelte +40 -16
- package/dist/convex-hull/GasPressureControls.svelte +104 -61
- package/dist/convex-hull/StructurePopup.svelte +25 -4
- package/dist/convex-hull/TemperatureSlider.svelte +45 -25
- package/dist/convex-hull/barycentric-coords.js +13 -7
- package/dist/convex-hull/demo-temperature.js +8 -4
- package/dist/convex-hull/gas-thermodynamics.js +17 -12
- package/dist/convex-hull/helpers.d.ts +9 -0
- package/dist/convex-hull/helpers.js +77 -34
- package/dist/convex-hull/thermodynamics.js +61 -56
- package/dist/convex-hull/types.d.ts +9 -14
- package/dist/convex-hull/types.js +0 -17
- package/dist/coordination/CoordinationBarPlot.svelte +227 -154
- package/dist/element/BohrAtom.svelte +55 -12
- package/dist/element/ElementHeading.svelte +7 -2
- package/dist/element/ElementPhoto.svelte +15 -9
- package/dist/element/ElementStats.svelte +10 -4
- package/dist/element/ElementTile.svelte +137 -73
- package/dist/element/Nucleus.svelte +39 -11
- package/dist/feedback/ClickFeedback.svelte +16 -5
- package/dist/feedback/DragOverlay.svelte +10 -2
- package/dist/feedback/Spinner.svelte +4 -2
- package/dist/feedback/StatusMessage.svelte +8 -2
- package/dist/fermi-surface/FermiSlice.svelte +118 -88
- package/dist/fermi-surface/FermiSurface.svelte +328 -187
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +535 -342
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
- package/dist/fermi-surface/compute.js +16 -20
- package/dist/fermi-surface/parse.js +24 -14
- package/dist/fermi-surface/symmetry.js +2 -7
- package/dist/fermi-surface/types.d.ts +3 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +1019 -765
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +1 -1
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +76 -22
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +2 -3
- package/dist/icons.js +47 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.d.ts +3 -0
- package/dist/io/export.js +129 -143
- package/dist/io/is-binary.js +2 -3
- package/dist/io/url-drop.js +1 -2
- package/dist/isosurface/Isosurface.svelte +202 -148
- package/dist/isosurface/IsosurfaceControls.svelte +46 -28
- package/dist/isosurface/parse.js +34 -29
- package/dist/isosurface/slice.js +5 -10
- package/dist/isosurface/types.d.ts +2 -1
- package/dist/isosurface/types.js +61 -12
- package/dist/labels.js +11 -8
- package/dist/layout/FullscreenToggle.svelte +11 -2
- package/dist/layout/InfoCard.svelte +38 -6
- package/dist/layout/InfoTag.svelte +63 -32
- package/dist/layout/PropertyFilter.svelte +82 -37
- package/dist/layout/SettingsSection.svelte +85 -55
- package/dist/layout/SubpageGrid.svelte +10 -2
- package/dist/layout/json-tree/JsonNode.svelte +183 -138
- package/dist/layout/json-tree/JsonTree.svelte +499 -413
- package/dist/layout/json-tree/JsonValue.svelte +127 -99
- package/dist/layout/json-tree/utils.js +4 -2
- package/dist/marching-cubes.js +25 -2
- package/dist/math.d.ts +13 -17
- package/dist/math.js +133 -67
- package/dist/overlays/ContextMenu.svelte +65 -40
- package/dist/overlays/DraggablePane.svelte +211 -139
- package/dist/periodic-table/PeriodicTable.svelte +278 -145
- package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
- package/dist/periodic-table/PropertySelect.svelte +25 -7
- package/dist/periodic-table/TableInset.svelte +8 -3
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +446 -309
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +102 -43
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +63 -40
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +71 -28
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +158 -101
- package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
- package/dist/phase-diagram/build-diagram.js +9 -9
- package/dist/phase-diagram/colors.js +1 -3
- package/dist/phase-diagram/parse.js +10 -9
- package/dist/phase-diagram/svg-to-diagram.js +53 -49
- package/dist/phase-diagram/utils.d.ts +1 -0
- package/dist/phase-diagram/utils.js +80 -25
- package/dist/plot/AxisLabel.svelte +28 -3
- package/dist/plot/BarPlot.svelte +1182 -734
- package/dist/plot/BarPlot.svelte.d.ts +2 -2
- package/dist/plot/BarPlotControls.svelte +31 -5
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ColorBar.svelte +479 -329
- package/dist/plot/ColorScaleSelect.svelte +27 -6
- package/dist/plot/ElementScatter.svelte +36 -15
- package/dist/plot/FillArea.svelte +152 -95
- package/dist/plot/Histogram.svelte +934 -571
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte +53 -9
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +34 -11
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/Line.svelte +63 -28
- package/dist/plot/PlotControls.svelte +157 -114
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/PlotLegend.svelte +174 -91
- package/dist/plot/PlotTooltip.svelte +45 -6
- package/dist/plot/PortalSelect.svelte +175 -147
- package/dist/plot/ReferenceLine.svelte +76 -22
- package/dist/plot/ReferenceLine3D.svelte +132 -107
- package/dist/plot/ReferencePlane.svelte +146 -121
- package/dist/plot/ScatterPlot.svelte +1681 -1091
- package/dist/plot/ScatterPlot.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3D.svelte +256 -131
- package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlot3DControls.svelte +113 -63
- package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
- package/dist/plot/ScatterPlot3DScene.svelte +608 -403
- package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
- package/dist/plot/ScatterPlotControls.svelte +65 -25
- package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/ScatterPoint.svelte +98 -26
- package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
- package/dist/plot/SpacegroupBarPlot.svelte +142 -85
- package/dist/plot/Surface3D.svelte +159 -108
- package/dist/plot/ZeroLines.svelte +55 -3
- package/dist/plot/ZoomRect.svelte +4 -2
- package/dist/plot/axis-utils.js +1 -3
- package/dist/plot/data-cleaning.js +12 -28
- package/dist/plot/data-transform.js +2 -1
- package/dist/plot/fill-utils.js +2 -0
- package/dist/plot/layout.d.ts +4 -1
- package/dist/plot/layout.js +33 -14
- package/dist/plot/reference-line.d.ts +2 -2
- package/dist/plot/reference-line.js +7 -5
- package/dist/plot/scales.js +24 -36
- package/dist/plot/types.d.ts +11 -23
- package/dist/plot/types.js +6 -11
- package/dist/plot/utils/label-placement.d.ts +32 -15
- package/dist/plot/utils/label-placement.js +227 -66
- package/dist/plot/utils/series-visibility.js +2 -3
- package/dist/rdf/RdfPlot.svelte +143 -91
- package/dist/rdf/calc-rdf.js +4 -5
- package/dist/sanitize.d.ts +4 -0
- package/dist/sanitize.js +107 -0
- package/dist/settings.d.ts +18 -6
- package/dist/settings.js +46 -16
- package/dist/spectral/Bands.svelte +632 -453
- package/dist/spectral/BandsAndDos.svelte +90 -49
- package/dist/spectral/BrillouinBandsDos.svelte +151 -93
- package/dist/spectral/Dos.svelte +389 -258
- package/dist/spectral/helpers.js +55 -43
- package/dist/state.svelte.d.ts +1 -1
- package/dist/state.svelte.js +3 -2
- package/dist/structure/Arrow.svelte +59 -20
- package/dist/structure/AtomLegend.svelte +215 -134
- package/dist/structure/Bond.svelte +73 -47
- package/dist/structure/CanvasTooltip.svelte +10 -2
- package/dist/structure/CellSelect.svelte +72 -45
- package/dist/structure/Cylinder.svelte +33 -17
- package/dist/structure/Lattice.svelte +88 -33
- package/dist/structure/Structure.svelte +1063 -797
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureControls.svelte +349 -118
- package/dist/structure/StructureExportPane.svelte +124 -89
- package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +304 -237
- package/dist/structure/StructureScene.svelte +879 -443
- package/dist/structure/StructureScene.svelte.d.ts +15 -7
- package/dist/structure/atom-properties.js +8 -8
- package/dist/structure/bonding.js +6 -7
- package/dist/structure/export.js +14 -29
- package/dist/structure/ferrox-wasm.js +1 -1
- package/dist/structure/index.d.ts +13 -3
- package/dist/structure/index.js +83 -23
- package/dist/structure/measure.d.ts +2 -2
- package/dist/structure/measure.js +4 -44
- package/dist/structure/parse.js +113 -141
- package/dist/structure/partial-occupancy.js +7 -10
- package/dist/structure/pbc.d.ts +1 -0
- package/dist/structure/pbc.js +16 -6
- package/dist/structure/supercell.d.ts +2 -2
- package/dist/structure/supercell.js +12 -22
- package/dist/structure/validation.js +1 -2
- package/dist/symmetry/SymmetryStats.svelte +84 -41
- package/dist/symmetry/WyckoffTable.svelte +26 -6
- package/dist/symmetry/cell-transform.js +5 -3
- package/dist/symmetry/index.js +8 -7
- package/dist/symmetry/spacegroups.js +148 -148
- package/dist/table/HeatmapTable.svelte +790 -554
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +125 -92
- package/dist/table/index.js +2 -4
- package/dist/theme/ThemeControl.svelte +21 -12
- package/dist/time.js +4 -1
- package/dist/tooltip/TooltipContent.svelte +33 -8
- package/dist/trajectory/Trajectory.svelte +758 -558
- package/dist/trajectory/TrajectoryError.svelte +14 -3
- package/dist/trajectory/TrajectoryExportPane.svelte +137 -83
- package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
- package/dist/trajectory/extract.js +10 -26
- package/dist/trajectory/format-detect.js +5 -5
- package/dist/trajectory/frame-reader.d.ts +1 -1
- package/dist/trajectory/frame-reader.js +5 -12
- package/dist/trajectory/helpers.d.ts +0 -1
- package/dist/trajectory/helpers.js +2 -17
- package/dist/trajectory/index.js +14 -12
- package/dist/trajectory/parse/ase.js +5 -4
- package/dist/trajectory/parse/hdf5.js +26 -18
- package/dist/trajectory/parse/index.js +13 -18
- package/dist/trajectory/parse/lammps.js +17 -7
- package/dist/trajectory/parse/vasp.js +5 -2
- package/dist/trajectory/parse/xyz.js +8 -7
- package/dist/trajectory/plotting.js +13 -8
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +13 -0
- package/dist/xrd/XrdPlot.svelte +337 -247
- package/dist/xrd/broadening.js +14 -9
- package/dist/xrd/calc-xrd.js +12 -18
- package/dist/xrd/parse.d.ts +1 -1
- package/dist/xrd/parse.js +17 -17
- package/package.json +99 -103
- package/readme.md +1 -1
- /package/dist/theme/{themes.js → themes.mjs} +0 -0
|
@@ -1,786 +1,1102 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
import
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { D3InterpolateName } from '../colors'
|
|
3
|
+
import {
|
|
4
|
+
add_alpha,
|
|
5
|
+
is_dark_mode,
|
|
6
|
+
PLOT_COLORS,
|
|
7
|
+
vesta_hex,
|
|
8
|
+
watch_dark_mode,
|
|
9
|
+
} from '../colors'
|
|
10
|
+
import { normalize_show_controls } from '../controls'
|
|
11
|
+
import { sanitize_html } from '../sanitize'
|
|
12
|
+
import { ClickFeedback, DragOverlay, Spinner } from '../feedback'
|
|
13
|
+
import Icon from '../Icon.svelte'
|
|
14
|
+
import {
|
|
15
|
+
set_fullscreen_bg,
|
|
16
|
+
setup_fullscreen_effect,
|
|
17
|
+
toggle_fullscreen,
|
|
18
|
+
} from '../layout'
|
|
19
|
+
import { ColorBar, PlotTooltip } from '../plot'
|
|
20
|
+
import { DEFAULTS } from '../settings'
|
|
21
|
+
import type { AnyStructure } from '../structure'
|
|
22
|
+
import {
|
|
23
|
+
barycentric_to_tetrahedral,
|
|
24
|
+
compute_4d_coords,
|
|
25
|
+
TETRAHEDRON_VERTICES,
|
|
26
|
+
} from './barycentric-coords'
|
|
27
|
+
import ConvexHullControls from './ConvexHullControls.svelte'
|
|
28
|
+
import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
|
|
29
|
+
import ConvexHullTooltip from './ConvexHullTooltip.svelte'
|
|
30
|
+
import GasPressureControls from './GasPressureControls.svelte'
|
|
31
|
+
import * as helpers from './helpers'
|
|
32
|
+
import type { BaseConvexHullProps, Hull3DProps } from './index'
|
|
33
|
+
import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
|
|
34
|
+
import StructurePopup from './StructurePopup.svelte'
|
|
35
|
+
import TemperatureSlider from './TemperatureSlider.svelte'
|
|
36
|
+
import type { Point4D } from './thermodynamics'
|
|
37
|
+
import * as thermo from './thermodynamics'
|
|
38
|
+
import type {
|
|
39
|
+
ConvexHullEntry,
|
|
40
|
+
HighlightStyle,
|
|
41
|
+
HoverData3D,
|
|
42
|
+
HullFaceColorMode,
|
|
43
|
+
} from './types'
|
|
44
|
+
import { compute_hull_stability } from './helpers'
|
|
45
|
+
|
|
46
|
+
let {
|
|
47
|
+
entries = [],
|
|
48
|
+
controls = {},
|
|
49
|
+
config = {},
|
|
50
|
+
on_point_click,
|
|
51
|
+
on_point_hover,
|
|
52
|
+
fullscreen = $bindable(DEFAULTS.convex_hull.quaternary.fullscreen),
|
|
53
|
+
enable_fullscreen = true,
|
|
54
|
+
enable_info_pane = true,
|
|
55
|
+
wrapper = $bindable(),
|
|
56
|
+
label_threshold = 50,
|
|
57
|
+
show_stable = $bindable(DEFAULTS.convex_hull.quaternary.show_stable),
|
|
58
|
+
show_unstable = $bindable(DEFAULTS.convex_hull.quaternary.show_unstable),
|
|
59
|
+
show_hull_faces = $bindable(DEFAULTS.convex_hull.quaternary.show_hull_faces),
|
|
60
|
+
hull_face_opacity = $bindable(DEFAULTS.convex_hull.quaternary.hull_face_opacity),
|
|
61
|
+
hull_face_color_mode = $bindable(
|
|
62
|
+
DEFAULTS.convex_hull.quaternary.hull_face_color_mode as HullFaceColorMode,
|
|
63
|
+
),
|
|
64
|
+
element_colors = vesta_hex,
|
|
65
|
+
color_mode = $bindable(DEFAULTS.convex_hull.quaternary.color_mode),
|
|
66
|
+
color_scale = $bindable(
|
|
67
|
+
DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName,
|
|
68
|
+
),
|
|
69
|
+
info_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.info_pane_open),
|
|
70
|
+
legend_pane_open = $bindable(DEFAULTS.convex_hull.quaternary.legend_pane_open),
|
|
71
|
+
max_hull_dist_show_phases = $bindable(
|
|
72
|
+
DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
|
|
73
|
+
),
|
|
74
|
+
max_hull_dist_show_labels = $bindable(
|
|
75
|
+
DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels,
|
|
76
|
+
),
|
|
77
|
+
show_stable_labels = $bindable(
|
|
78
|
+
DEFAULTS.convex_hull.quaternary.show_stable_labels,
|
|
79
|
+
),
|
|
80
|
+
show_unstable_labels = $bindable(
|
|
81
|
+
DEFAULTS.convex_hull.quaternary.show_unstable_labels,
|
|
82
|
+
),
|
|
83
|
+
on_file_drop,
|
|
84
|
+
enable_click_selection = true,
|
|
85
|
+
enable_structure_preview = true,
|
|
86
|
+
energy_source_mode = $bindable(`precomputed`),
|
|
87
|
+
phase_stats = $bindable(null),
|
|
88
|
+
stable_entries = $bindable([]),
|
|
89
|
+
unstable_entries = $bindable([]),
|
|
90
|
+
highlighted_entries = $bindable([]),
|
|
91
|
+
highlight_style = {},
|
|
92
|
+
selected_entry = $bindable(null),
|
|
93
|
+
temperature = $bindable(),
|
|
94
|
+
interpolate_temperature = true,
|
|
95
|
+
max_interpolation_gap = 500,
|
|
96
|
+
gas_config,
|
|
97
|
+
gas_pressures = $bindable({}),
|
|
98
|
+
children,
|
|
99
|
+
tooltip,
|
|
100
|
+
...rest
|
|
101
|
+
}: BaseConvexHullProps<ConvexHullEntry> & Hull3DProps & {
|
|
102
|
+
highlight_style?: HighlightStyle
|
|
103
|
+
} = $props()
|
|
104
|
+
|
|
105
|
+
const merged_controls = $derived({ ...default_controls, ...controls })
|
|
106
|
+
const controls_config = $derived(normalize_show_controls(merged_controls.show))
|
|
107
|
+
const merged_config = $derived({
|
|
22
108
|
...default_hull_config,
|
|
23
109
|
...config,
|
|
24
110
|
colors: { ...default_hull_config.colors, ...(config.colors || {}) },
|
|
25
111
|
margin: { t: 60, r: 60, b: 60, l: 60, ...(config.margin || {}) },
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
//
|
|
34
|
-
$
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Reactive dark mode detection for canvas text color
|
|
115
|
+
let dark_mode = $state(is_dark_mode())
|
|
116
|
+
$effect(() => watch_dark_mode((dark) => dark_mode = dark))
|
|
117
|
+
const text_color = $derived(helpers.get_canvas_text_color(dark_mode))
|
|
118
|
+
|
|
119
|
+
// Temperature-dependent free energy support
|
|
120
|
+
const { has_temp_data, available_temperatures } = $derived(
|
|
121
|
+
helpers.analyze_temperature_data(entries),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
// Initialize or reset temperature when it's undefined or no longer valid
|
|
125
|
+
$effect(() => {
|
|
126
|
+
if (
|
|
127
|
+
has_temp_data &&
|
|
128
|
+
available_temperatures.length > 0 &&
|
|
129
|
+
(temperature === undefined || !available_temperatures.includes(temperature))
|
|
130
|
+
) temperature = available_temperatures[0]
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Filter entries by temperature when in temperature mode
|
|
134
|
+
const temp_filtered_entries = $derived(
|
|
135
|
+
has_temp_data && temperature !== undefined
|
|
136
|
+
? helpers.filter_entries_at_temperature(entries, temperature, {
|
|
43
137
|
interpolate: interpolate_temperature,
|
|
44
138
|
max_interpolation_gap,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
139
|
+
})
|
|
140
|
+
: entries,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// Gas-dependent chemical potential support (corrections based on T, P)
|
|
144
|
+
const {
|
|
145
|
+
entries: gas_corrected_entries,
|
|
146
|
+
analysis: gas_analysis,
|
|
147
|
+
merged_config: merged_gas_config,
|
|
148
|
+
} = $derived(
|
|
149
|
+
helpers.get_gas_corrected_entries(
|
|
150
|
+
temp_filtered_entries,
|
|
151
|
+
gas_config,
|
|
152
|
+
gas_pressures,
|
|
153
|
+
temperature ?? helpers.DEFAULT_GAS_TEMP,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
let { // Compute energy mode information
|
|
158
|
+
has_precomputed_e_form,
|
|
159
|
+
has_precomputed_hull,
|
|
160
|
+
can_compute_e_form,
|
|
161
|
+
can_compute_hull,
|
|
162
|
+
energy_mode,
|
|
163
|
+
unary_refs,
|
|
164
|
+
} = $derived(
|
|
165
|
+
helpers.compute_energy_mode_info(
|
|
166
|
+
gas_corrected_entries,
|
|
167
|
+
thermo.find_lowest_energy_unary_refs,
|
|
168
|
+
energy_source_mode,
|
|
169
|
+
),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
const effective_entries = $derived(
|
|
173
|
+
helpers.get_effective_entries(
|
|
174
|
+
gas_corrected_entries,
|
|
175
|
+
energy_mode,
|
|
176
|
+
unary_refs,
|
|
177
|
+
thermo.compute_e_form_per_atom,
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
// Process convex hull data with unified PhaseData interface using effective entries
|
|
182
|
+
const pd_data = $derived(thermo.process_hull_entries(effective_entries))
|
|
183
|
+
|
|
184
|
+
// Pre-compute polymorph stats once for O(1) tooltip lookups
|
|
185
|
+
const polymorph_stats_map = $derived(
|
|
186
|
+
helpers.compute_all_polymorph_stats(effective_entries),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
const elements = $derived.by(() => {
|
|
57
190
|
if (pd_data.elements.length > 4) {
|
|
58
|
-
|
|
59
|
-
|
|
191
|
+
console.error(
|
|
192
|
+
`ConvexHull4D: Dataset contains ${pd_data.elements.length} elements, but quaternary diagrams require exactly 4. Found: [${
|
|
193
|
+
pd_data.elements.join(`, `)
|
|
194
|
+
}]`,
|
|
195
|
+
)
|
|
196
|
+
return []
|
|
60
197
|
}
|
|
61
|
-
return pd_data.elements
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
198
|
+
return pd_data.elements
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
// Compute 4D hull for visualization (always compute when we have formation energies)
|
|
202
|
+
const hull_4d = $derived.by(() => {
|
|
203
|
+
if (elements.length !== 4) return []
|
|
204
|
+
|
|
67
205
|
try {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
206
|
+
// Get coords with formation energies, excluding entries that don't participate in hull
|
|
207
|
+
const coords = compute_4d_coords(pd_data.entries, elements)
|
|
208
|
+
.filter((ent) => !ent.exclude_from_hull)
|
|
209
|
+
|
|
210
|
+
// Convert to 4D points for hull computation using barycentric coordinates (composition fractions)
|
|
211
|
+
const points_4d: Point4D[] = coords
|
|
212
|
+
.filter(
|
|
213
|
+
(ent) =>
|
|
214
|
+
Number.isFinite(ent.e_form_per_atom) &&
|
|
215
|
+
[ent.x, ent.y, ent.z].every(Number.isFinite),
|
|
216
|
+
)
|
|
217
|
+
.map((ent) => {
|
|
218
|
+
const amounts = elements.map((el) => ent.composition[el] || 0)
|
|
219
|
+
const total = amounts.reduce((sum, amt) => sum + amt, 0)
|
|
220
|
+
if (!(total > 0)) return { x: NaN, y: NaN, z: NaN, w: NaN }
|
|
221
|
+
const [x, y, z] = amounts.map((amt) => amt / total)
|
|
222
|
+
return { x, y, z, w: ent.e_form_per_atom ?? NaN }
|
|
81
223
|
})
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
catch (error) {
|
|
88
|
-
|
|
89
|
-
|
|
224
|
+
.filter((point) => [point.x, point.y, point.z, point.w].every(Number.isFinite))
|
|
225
|
+
|
|
226
|
+
if (points_4d.length < 5) return [] // Need at least 5 points for 4D hull
|
|
227
|
+
|
|
228
|
+
return thermo.compute_lower_hull_4d(points_4d)
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`Error computing 4D hull:`, error)
|
|
231
|
+
return []
|
|
90
232
|
}
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
// Enrich coords with e_above_hull (before filtering)
|
|
236
|
+
const all_enriched_entries = $derived.by(() => {
|
|
237
|
+
if (elements.length !== 4) return []
|
|
96
238
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
catch (err) {
|
|
122
|
-
|
|
123
|
-
|
|
239
|
+
const coords = compute_4d_coords(pd_data.entries, elements)
|
|
240
|
+
if (energy_mode !== `on-the-fly` || hull_4d.length === 0) return coords
|
|
241
|
+
|
|
242
|
+
// Build 4D points, tracking original indices for mapping hull distances back
|
|
243
|
+
const valid = coords.flatMap((entry, idx) => {
|
|
244
|
+
if (
|
|
245
|
+
!Number.isFinite(entry.e_form_per_atom) ||
|
|
246
|
+
![entry.x, entry.y, entry.z].every(Number.isFinite)
|
|
247
|
+
) return []
|
|
248
|
+
const amounts = elements.map((el) => entry.composition[el] || 0)
|
|
249
|
+
const total = amounts.reduce((sum, amt) => sum + amt, 0)
|
|
250
|
+
if (!(total > 0)) return []
|
|
251
|
+
const [x, y, z] = amounts.map((amt) => amt / total)
|
|
252
|
+
return [x, y, z].every(Number.isFinite)
|
|
253
|
+
? [{ idx, pt: { x, y, z, w: entry.e_form_per_atom ?? NaN } }]
|
|
254
|
+
: []
|
|
255
|
+
})
|
|
256
|
+
const raw_dists = thermo.compute_e_above_hull_4d(valid.map((item) => item.pt), hull_4d)
|
|
257
|
+
const hull_map = new Map(valid.map((item, hull_idx) => [item.idx, raw_dists[hull_idx]]))
|
|
258
|
+
return coords.map((entry, idx) => {
|
|
259
|
+
const raw = hull_map.get(idx)
|
|
260
|
+
if (raw === undefined) return { ...entry, e_above_hull: raw, is_stable: false }
|
|
261
|
+
return { ...entry, ...compute_hull_stability(raw, entry.exclude_from_hull) }
|
|
262
|
+
})
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(`Error computing quaternary coordinates:`, err)
|
|
265
|
+
return []
|
|
124
266
|
}
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
$
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Auto threshold: show all for few entries, use default for many, interpolate between
|
|
270
|
+
const max_hull_dist_in_data = $derived(
|
|
271
|
+
helpers.calc_max_hull_dist_in_data(all_enriched_entries),
|
|
272
|
+
)
|
|
273
|
+
const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
|
|
274
|
+
all_enriched_entries.length,
|
|
275
|
+
max_hull_dist_in_data,
|
|
276
|
+
DEFAULTS.convex_hull.quaternary.max_hull_dist_show_phases,
|
|
277
|
+
))
|
|
278
|
+
|
|
279
|
+
// Initialize threshold to auto value on first load
|
|
280
|
+
let initialized = $state(false)
|
|
281
|
+
$effect(() => {
|
|
132
282
|
if (!initialized && all_enriched_entries.length > 0) {
|
|
133
|
-
|
|
134
|
-
|
|
283
|
+
initialized = true
|
|
284
|
+
max_hull_dist_show_phases = auto_default_threshold
|
|
135
285
|
}
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
// Filter by threshold and compute visibility
|
|
289
|
+
const plot_entries = $derived(
|
|
290
|
+
all_enriched_entries.filter((entry) => {
|
|
291
|
+
// Always include stable entries and elemental reference points
|
|
292
|
+
if (entry.is_stable || (entry.e_above_hull ?? Infinity) <= 1e-6) return true
|
|
293
|
+
return typeof entry.e_above_hull === `number` &&
|
|
294
|
+
entry.e_above_hull <= max_hull_dist_show_phases
|
|
295
|
+
}).map((entry) => {
|
|
296
|
+
const is_stable = entry.is_stable || entry.e_above_hull === 0
|
|
297
|
+
return {
|
|
147
298
|
...entry,
|
|
148
299
|
visible: (is_stable && show_stable) || (!is_stable && show_unstable),
|
|
149
|
-
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
300
|
+
}
|
|
301
|
+
}),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
// Stable and unstable entries exposed as bindable props
|
|
305
|
+
$effect(() => {
|
|
306
|
+
stable_entries = plot_entries.filter((entry: ConvexHullEntry) =>
|
|
307
|
+
entry.is_stable || entry.e_above_hull === 0
|
|
308
|
+
)
|
|
309
|
+
unstable_entries = plot_entries.filter((entry: ConvexHullEntry) =>
|
|
310
|
+
(entry.e_above_hull ?? 0) > 0 && !entry.is_stable
|
|
311
|
+
)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
let canvas: HTMLCanvasElement
|
|
315
|
+
let ctx: CanvasRenderingContext2D | null = null
|
|
316
|
+
let frame_id = 0 // Performance optimization
|
|
317
|
+
|
|
318
|
+
// Camera state - following Materials Project's 3D camera setup
|
|
319
|
+
let camera = $state({
|
|
161
320
|
rotation_x: DEFAULTS.convex_hull.quaternary.camera_rotation_x,
|
|
162
321
|
rotation_y: DEFAULTS.convex_hull.quaternary.camera_rotation_y,
|
|
163
322
|
zoom: DEFAULTS.convex_hull.quaternary.camera_zoom,
|
|
164
323
|
center_x: 0,
|
|
165
324
|
center_y: 20, // Slight offset to avoid legend overlap
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
let
|
|
170
|
-
let
|
|
171
|
-
let
|
|
172
|
-
let
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
let
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
// Interaction state
|
|
328
|
+
let is_dragging = $state(false)
|
|
329
|
+
let drag_started = $state(false)
|
|
330
|
+
let last_mouse = $state({ x: 0, y: 0 })
|
|
331
|
+
let hover_data = $state<HoverData3D<ConvexHullEntry> | null>(null)
|
|
332
|
+
let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
|
|
333
|
+
|
|
334
|
+
// Drag and drop state
|
|
335
|
+
let drag_over = $state(false)
|
|
336
|
+
|
|
337
|
+
// Structure popup state
|
|
338
|
+
let modal_open = $state(false)
|
|
339
|
+
let selected_structure = $state<AnyStructure | null>(null)
|
|
340
|
+
let modal_place_right = $state(true)
|
|
341
|
+
|
|
342
|
+
// Hull face color (customizable via controls)
|
|
343
|
+
let hull_face_color = $state(`#4caf50`)
|
|
344
|
+
|
|
345
|
+
// Pulsating highlight for selected point and highlighted entries
|
|
346
|
+
let pulse_time = $state(0)
|
|
347
|
+
let pulse_opacity = $derived(0.3 + 0.4 * Math.sin(pulse_time * 4))
|
|
348
|
+
let pulse_frame_id = 0
|
|
349
|
+
|
|
350
|
+
// Merge highlight style with defaults
|
|
351
|
+
const merged_highlight_style = $derived(
|
|
352
|
+
helpers.merge_highlight_style(highlight_style),
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
// Helper to check if entry is highlighted
|
|
356
|
+
const is_highlighted = (entry: ConvexHullEntry): boolean =>
|
|
357
|
+
helpers.is_entry_highlighted(entry, highlighted_entries)
|
|
358
|
+
|
|
359
|
+
$effect(() => {
|
|
360
|
+
if (!selected_entry && !highlighted_entries.length) return
|
|
361
|
+
const reduce = globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches
|
|
362
|
+
if (reduce) return
|
|
195
363
|
const animate = () => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
pulse_frame_id = requestAnimationFrame(animate)
|
|
364
|
+
pulse_time += 0.02
|
|
365
|
+
render_once()
|
|
366
|
+
pulse_frame_id = requestAnimationFrame(animate)
|
|
367
|
+
}
|
|
368
|
+
pulse_frame_id = requestAnimationFrame(animate)
|
|
201
369
|
return () => {
|
|
202
|
-
|
|
203
|
-
cancelAnimationFrame(pulse_frame_id);
|
|
204
|
-
};
|
|
205
|
-
});
|
|
206
|
-
// Re-render when important state changes
|
|
207
|
-
$effect(() => {
|
|
208
|
-
// deno-fmt-ignore
|
|
209
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
210
|
-
[show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements];
|
|
211
|
-
render_once();
|
|
212
|
-
});
|
|
213
|
-
// Visibility toggles are now bindable props
|
|
214
|
-
// Smart label defaults - hide labels if too many entries
|
|
215
|
-
$effect(() => {
|
|
216
|
-
const total_entries = effective_entries.length;
|
|
217
|
-
if (total_entries > label_threshold) {
|
|
218
|
-
show_stable_labels = false;
|
|
219
|
-
show_unstable_labels = false;
|
|
370
|
+
if (pulse_frame_id) cancelAnimationFrame(pulse_frame_id)
|
|
220
371
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
// Re-render when important state changes
|
|
375
|
+
$effect(() => {
|
|
376
|
+
// oxfmt-ignore
|
|
377
|
+
void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
|
|
378
|
+
|
|
379
|
+
render_once()
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// Visibility toggles are now bindable props
|
|
383
|
+
|
|
384
|
+
// Smart label defaults - hide labels if too many entries
|
|
385
|
+
$effect(() => {
|
|
386
|
+
const total_entries = effective_entries.length
|
|
387
|
+
if (total_entries > label_threshold) {
|
|
388
|
+
show_stable_labels = false
|
|
389
|
+
show_unstable_labels = false
|
|
390
|
+
} else {
|
|
391
|
+
// For smaller datasets, show stable labels by default
|
|
392
|
+
show_stable_labels = true
|
|
393
|
+
show_unstable_labels = false
|
|
225
394
|
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
camera.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// Function to extract structure data from a convex hull entry
|
|
398
|
+
function extract_structure_from_entry(
|
|
399
|
+
entry: ConvexHullEntry,
|
|
400
|
+
): AnyStructure | null {
|
|
401
|
+
const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
|
|
402
|
+
return orig_entry?.structure as AnyStructure || null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const reset_camera = () => {
|
|
406
|
+
camera.rotation_x = DEFAULTS.convex_hull.quaternary.camera_rotation_x
|
|
407
|
+
camera.rotation_y = DEFAULTS.convex_hull.quaternary.camera_rotation_y
|
|
408
|
+
camera.zoom = DEFAULTS.convex_hull.quaternary.camera_zoom
|
|
409
|
+
camera.center_x = 0
|
|
410
|
+
camera.center_y = 20 // Slight offset to avoid legend overlap
|
|
411
|
+
}
|
|
412
|
+
function reset_all() {
|
|
413
|
+
reset_camera()
|
|
414
|
+
fullscreen = DEFAULTS.convex_hull.quaternary.fullscreen
|
|
415
|
+
info_pane_open = DEFAULTS.convex_hull.quaternary.info_pane_open
|
|
416
|
+
legend_pane_open = DEFAULTS.convex_hull.quaternary.legend_pane_open
|
|
417
|
+
color_mode = DEFAULTS.convex_hull.quaternary.color_mode
|
|
418
|
+
color_scale = DEFAULTS.convex_hull.quaternary.color_scale as D3InterpolateName
|
|
419
|
+
show_stable = DEFAULTS.convex_hull.quaternary.show_stable
|
|
420
|
+
show_unstable = DEFAULTS.convex_hull.quaternary.show_unstable
|
|
421
|
+
show_stable_labels = DEFAULTS.convex_hull.quaternary.show_stable_labels
|
|
422
|
+
show_unstable_labels = DEFAULTS.convex_hull.quaternary.show_unstable_labels
|
|
250
423
|
// Use auto-computed threshold based on entry count instead of static default
|
|
251
|
-
max_hull_dist_show_phases = auto_default_threshold
|
|
424
|
+
max_hull_dist_show_phases = auto_default_threshold
|
|
252
425
|
max_hull_dist_show_labels =
|
|
253
|
-
|
|
254
|
-
show_hull_faces = DEFAULTS.convex_hull.quaternary.show_hull_faces
|
|
255
|
-
hull_face_color = DEFAULTS.convex_hull.quaternary.hull_face_color
|
|
256
|
-
hull_face_opacity = DEFAULTS.convex_hull.quaternary.hull_face_opacity
|
|
426
|
+
DEFAULTS.convex_hull.quaternary.max_hull_dist_show_labels
|
|
427
|
+
show_hull_faces = DEFAULTS.convex_hull.quaternary.show_hull_faces
|
|
428
|
+
hull_face_color = DEFAULTS.convex_hull.quaternary.hull_face_color
|
|
429
|
+
hull_face_opacity = DEFAULTS.convex_hull.quaternary.hull_face_opacity
|
|
257
430
|
hull_face_color_mode = DEFAULTS.convex_hull.quaternary
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
|
|
431
|
+
.hull_face_color_mode as HullFaceColorMode
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const handle_keydown = (event: KeyboardEvent) => {
|
|
435
|
+
const target = event.target as HTMLElement
|
|
262
436
|
// Skip if focus is on an interactive element that handles Enter natively
|
|
263
|
-
const interactive_selector =
|
|
264
|
-
|
|
265
|
-
|
|
437
|
+
const interactive_selector =
|
|
438
|
+
`input,textarea,select,button,a,[contenteditable="true"],[role="button"],[tabindex]:not([tabindex="-1"])`
|
|
439
|
+
if (target.matches(interactive_selector) && target !== canvas) return
|
|
440
|
+
|
|
266
441
|
// Prevent double handling from canvas + wrapper bubbling
|
|
267
|
-
if (event.target !== event.currentTarget && event.currentTarget !== canvas)
|
|
268
|
-
|
|
442
|
+
if (event.target !== event.currentTarget && event.currentTarget !== canvas) return
|
|
443
|
+
|
|
269
444
|
// Handle Enter for keyboard accessibility - select hovered entry
|
|
270
445
|
if (event.key === `Enter`) {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}
|
|
283
|
-
}
|
|
446
|
+
const entry = hover_data?.entry
|
|
447
|
+
if (entry) {
|
|
448
|
+
on_point_click?.(entry)
|
|
449
|
+
if (enable_click_selection) {
|
|
450
|
+
selected_entry = entry
|
|
451
|
+
if (enable_structure_preview) {
|
|
452
|
+
const structure = extract_structure_from_entry(entry)
|
|
453
|
+
if (structure) {
|
|
454
|
+
selected_structure = structure
|
|
455
|
+
modal_place_right = helpers.calculate_modal_side(wrapper)
|
|
456
|
+
modal_open = true
|
|
284
457
|
}
|
|
458
|
+
}
|
|
285
459
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
460
|
+
} else if (modal_open) {
|
|
461
|
+
close_structure_popup()
|
|
462
|
+
}
|
|
463
|
+
return
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const actions: Record<string, () => void> = {
|
|
467
|
+
r: reset_camera,
|
|
468
|
+
b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
|
|
469
|
+
s: () => show_stable = !show_stable,
|
|
470
|
+
u: () => show_unstable = !show_unstable,
|
|
471
|
+
h: () => show_hull_faces = !show_hull_faces,
|
|
472
|
+
l: () => show_stable_labels = !show_stable_labels,
|
|
290
473
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
if (data)
|
|
305
|
-
on_file_drop?.(data);
|
|
306
|
-
}
|
|
307
|
-
async function copy_entry_data(entry, position) {
|
|
474
|
+
actions[event.key.toLowerCase()]?.()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async function handle_file_drop(event: DragEvent): Promise<void> {
|
|
478
|
+
drag_over = false
|
|
479
|
+
const data = await helpers.parse_hull_entries_from_drop(event)
|
|
480
|
+
if (data) on_file_drop?.(data)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function copy_entry_data(
|
|
484
|
+
entry: ConvexHullEntry,
|
|
485
|
+
position: { x: number; y: number },
|
|
486
|
+
) {
|
|
308
487
|
await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
488
|
+
copy_feedback.visible = visible
|
|
489
|
+
copy_feedback.position = pos
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const get_point_color = (entry: ConvexHullEntry): string =>
|
|
494
|
+
helpers.get_point_color_for_entry(
|
|
495
|
+
entry,
|
|
496
|
+
color_mode,
|
|
497
|
+
merged_config.colors,
|
|
498
|
+
energy_color_scale,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
// Cache energy color scale per frame/setting
|
|
502
|
+
const energy_color_scale = $derived.by(() =>
|
|
503
|
+
helpers.get_energy_color_scale(color_mode, color_scale, plot_entries)
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
// Convex hull statistics - compute internally and expose via bindable prop
|
|
507
|
+
$effect(() => {
|
|
508
|
+
phase_stats = thermo.get_convex_hull_stats(plot_entries, elements, 4)
|
|
509
|
+
})
|
|
510
|
+
|
|
511
|
+
// 3D to 2D projection following Materials Project approach
|
|
512
|
+
function project_3d_point(
|
|
513
|
+
x: number,
|
|
514
|
+
y: number,
|
|
515
|
+
z: number,
|
|
516
|
+
): { x: number; y: number; depth: number } {
|
|
517
|
+
if (!canvas) return { x: 0, y: 0, depth: 0 }
|
|
518
|
+
|
|
324
519
|
// Center coordinates around tetrahedron/triangle centroid
|
|
325
|
-
let centered_x = x
|
|
326
|
-
|
|
327
|
-
let centered_z = z;
|
|
520
|
+
let [centered_x, centered_y, centered_z] = [x, y, z]
|
|
521
|
+
|
|
328
522
|
// Tetrahedron centroid: average of vertices (1,0,0), (0.5,√3/2,0), (0.5,√3/6,√6/3), (0,0,0)
|
|
329
|
-
const centroid_x = (1 + 0.5 + 0.5 + 0) / 4
|
|
330
|
-
const centroid_y = (0 + Math.sqrt(3) / 2 + Math.sqrt(3) / 6 + 0) / 4
|
|
331
|
-
const centroid_z = (0 + 0 + Math.sqrt(6) / 3 + 0) / 4
|
|
332
|
-
centered_x = x - centroid_x
|
|
333
|
-
centered_y = y - centroid_y
|
|
334
|
-
centered_z = z - centroid_z
|
|
523
|
+
const centroid_x = (1 + 0.5 + 0.5 + 0) / 4 // = 0.5
|
|
524
|
+
const centroid_y = (0 + Math.sqrt(3) / 2 + Math.sqrt(3) / 6 + 0) / 4 // = √3/6
|
|
525
|
+
const centroid_z = (0 + 0 + Math.sqrt(6) / 3 + 0) / 4 // = √6/12
|
|
526
|
+
centered_x = x - centroid_x
|
|
527
|
+
centered_y = y - centroid_y
|
|
528
|
+
centered_z = z - centroid_z
|
|
529
|
+
|
|
335
530
|
// Apply 3D transformations around the centered coordinates
|
|
336
|
-
const cos_x = Math.cos(camera.rotation_x)
|
|
337
|
-
const sin_x = Math.sin(camera.rotation_x)
|
|
338
|
-
const cos_y = Math.cos(camera.rotation_y)
|
|
339
|
-
const sin_y = Math.sin(camera.rotation_y)
|
|
531
|
+
const cos_x = Math.cos(camera.rotation_x)
|
|
532
|
+
const sin_x = Math.sin(camera.rotation_x)
|
|
533
|
+
const cos_y = Math.cos(camera.rotation_y)
|
|
534
|
+
const sin_y = Math.sin(camera.rotation_y)
|
|
535
|
+
|
|
340
536
|
// Rotate around Y axis first
|
|
341
|
-
const x1 = centered_x * cos_y - centered_z * sin_y
|
|
342
|
-
const z1 = centered_x * sin_y + centered_z * cos_y
|
|
537
|
+
const x1 = centered_x * cos_y - centered_z * sin_y
|
|
538
|
+
const z1 = centered_x * sin_y + centered_z * cos_y
|
|
539
|
+
|
|
343
540
|
// Then rotate around X axis
|
|
344
|
-
const y2 = centered_y * cos_x - z1 * sin_x
|
|
345
|
-
const z2 = centered_y * sin_x + z1 * cos_x
|
|
541
|
+
const y2 = centered_y * cos_x - z1 * sin_x
|
|
542
|
+
const z2 = centered_y * sin_x + z1 * cos_x
|
|
543
|
+
|
|
346
544
|
// Apply perspective projection using cached canvas dimensions for consistency
|
|
347
|
-
const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
|
|
348
|
-
const center_x = canvas_dims.width / 2 + camera.center_x
|
|
349
|
-
const center_y = canvas_dims.height / 2 + camera.center_y
|
|
545
|
+
const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
|
|
546
|
+
const center_x = canvas_dims.width / 2 + camera.center_x
|
|
547
|
+
const center_y = canvas_dims.height / 2 + camera.center_y
|
|
548
|
+
|
|
350
549
|
return {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
550
|
+
x: center_x + x1 * scale,
|
|
551
|
+
y: center_y - y2 * scale, // Flip Y for canvas coordinates
|
|
552
|
+
depth: z2, // For depth sorting
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function draw_structure_outline(): void {
|
|
557
|
+
if (!ctx) return
|
|
558
|
+
|
|
559
|
+
const styles = getComputedStyle(canvas)
|
|
360
560
|
// Match gray dashed structure lines used in 3D
|
|
361
|
-
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
362
|
-
ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
|
|
363
|
-
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
|
|
561
|
+
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
562
|
+
ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
|
|
563
|
+
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
|
|
564
|
+
|
|
364
565
|
// Draw tetrahedron edges
|
|
365
|
-
draw_tetrahedron()
|
|
566
|
+
draw_tetrahedron()
|
|
567
|
+
|
|
366
568
|
// Reset dash and stroke for subsequent drawings
|
|
367
|
-
ctx.setLineDash([])
|
|
368
|
-
ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
569
|
+
ctx.setLineDash([])
|
|
570
|
+
ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121`
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function draw_tetrahedron(): void {
|
|
574
|
+
if (!ctx) return
|
|
575
|
+
|
|
373
576
|
// Convert vertices to Point3D objects
|
|
374
|
-
const vertices = TETRAHEDRON_VERTICES.map(([x, y, z]) => ({ x, y, z }))
|
|
577
|
+
const vertices = TETRAHEDRON_VERTICES.map(([x, y, z]) => ({ x, y, z }))
|
|
578
|
+
|
|
375
579
|
// Tetrahedron edges (connecting vertices)
|
|
376
580
|
const edges = [
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
]
|
|
581
|
+
[0, 1],
|
|
582
|
+
[0, 2],
|
|
583
|
+
[0, 3], // From vertex 0
|
|
584
|
+
[1, 2],
|
|
585
|
+
[1, 3], // From vertex 1
|
|
586
|
+
[2, 3], // From vertex 2
|
|
587
|
+
]
|
|
588
|
+
|
|
384
589
|
// Draw edges
|
|
385
|
-
ctx.beginPath()
|
|
386
|
-
for (const [
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
590
|
+
ctx.beginPath()
|
|
591
|
+
for (const [start, end] of edges) {
|
|
592
|
+
const v1 = vertices[start]
|
|
593
|
+
const v2 = vertices[end]
|
|
594
|
+
|
|
595
|
+
const proj1 = project_3d_point(v1.x, v1.y, v1.z)
|
|
596
|
+
const proj2 = project_3d_point(v2.x, v2.y, v2.z)
|
|
597
|
+
|
|
598
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
599
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
393
600
|
}
|
|
394
|
-
ctx.stroke()
|
|
601
|
+
ctx.stroke()
|
|
602
|
+
|
|
395
603
|
// Corner element labels: place just outside along line towards tetrahedron centroid
|
|
396
604
|
if (elements.length === 4) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
ctx.fillText(elements[idx], proj.x, proj.y);
|
|
605
|
+
// Tetrahedron centroid in barycentric space maps to average of vertices
|
|
606
|
+
const centroid = {
|
|
607
|
+
x: (vertices[0].x + vertices[1].x + vertices[2].x + vertices[3].x) / 4,
|
|
608
|
+
y: (vertices[0].y + vertices[1].y + vertices[2].y + vertices[3].y) / 4,
|
|
609
|
+
z: (vertices[0].z + vertices[1].z + vertices[2].z + vertices[3].z) / 4,
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
ctx.fillStyle = text_color
|
|
613
|
+
ctx.font = `bold 18px Arial`
|
|
614
|
+
ctx.textAlign = `center`
|
|
615
|
+
ctx.textBaseline = `middle`
|
|
616
|
+
|
|
617
|
+
const distance = 0.06
|
|
618
|
+
for (let idx = 0; idx < 4; idx++) {
|
|
619
|
+
const vx = vertices[idx]
|
|
620
|
+
// Direction from centroid to vertex
|
|
621
|
+
const dir = {
|
|
622
|
+
x: vx.x - centroid.x,
|
|
623
|
+
y: vx.y - centroid.y,
|
|
624
|
+
z: vx.z - centroid.z,
|
|
625
|
+
}
|
|
626
|
+
const len = Math.hypot(dir.x, dir.y, dir.z) || 1
|
|
627
|
+
const label_pos = {
|
|
628
|
+
x: vx.x + (dir.x / len) * distance,
|
|
629
|
+
y: vx.y + (dir.y / len) * distance,
|
|
630
|
+
z: vx.z + (dir.z / len) * distance,
|
|
424
631
|
}
|
|
632
|
+
const proj = project_3d_point(label_pos.x, label_pos.y, label_pos.z)
|
|
633
|
+
ctx.fillText(elements[idx], proj.x, proj.y)
|
|
634
|
+
}
|
|
425
635
|
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Draw convex hull faces connecting stable points
|
|
639
|
+
function draw_convex_hull_faces(): void {
|
|
640
|
+
if (!ctx || !show_hull_faces || hull_4d.length === 0) return
|
|
641
|
+
|
|
431
642
|
// Get stable points to determine which hull facets to draw
|
|
432
|
-
const stable_points = plot_entries.filter((
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
643
|
+
const stable_points = plot_entries.filter((entry) =>
|
|
644
|
+
entry.is_stable || entry.e_above_hull === 0
|
|
645
|
+
)
|
|
646
|
+
if (stable_points.length === 0) return
|
|
647
|
+
|
|
648
|
+
// Each tetrahedral facet has 4 triangular faces - we need to draw these
|
|
649
|
+
// Collect all triangular faces with depth for sorting
|
|
650
|
+
type TriangleFace = {
|
|
651
|
+
vertices: [
|
|
652
|
+
{ x: number; y: number; depth: number },
|
|
653
|
+
{ x: number; y: number; depth: number },
|
|
654
|
+
{ x: number; y: number; depth: number },
|
|
655
|
+
]
|
|
656
|
+
avg_depth: number
|
|
657
|
+
avg_w: number // Average formation energy for coloring
|
|
658
|
+
tet_idx: number // Tetrahedron index for facet_index mode
|
|
659
|
+
centroid_bary: number[] // Barycentric centroid [el0, el1, el2, el3] for dominant_element mode
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const triangles: TriangleFace[] = []
|
|
663
|
+
|
|
436
664
|
for (let tet_idx = 0; tet_idx < hull_4d.length; tet_idx++) {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
]
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
665
|
+
const tet = hull_4d[tet_idx]
|
|
666
|
+
const [p0, p1, p2, p3] = tet.vertices
|
|
667
|
+
|
|
668
|
+
// Convert barycentric coordinates to tetrahedral 3D coordinates
|
|
669
|
+
const tet0 = barycentric_to_tetrahedral([
|
|
670
|
+
p0.x,
|
|
671
|
+
p0.y,
|
|
672
|
+
p0.z,
|
|
673
|
+
1 - p0.x - p0.y - p0.z,
|
|
674
|
+
])
|
|
675
|
+
const tet1 = barycentric_to_tetrahedral([
|
|
676
|
+
p1.x,
|
|
677
|
+
p1.y,
|
|
678
|
+
p1.z,
|
|
679
|
+
1 - p1.x - p1.y - p1.z,
|
|
680
|
+
])
|
|
681
|
+
const tet2 = barycentric_to_tetrahedral([
|
|
682
|
+
p2.x,
|
|
683
|
+
p2.y,
|
|
684
|
+
p2.z,
|
|
685
|
+
1 - p2.x - p2.y - p2.z,
|
|
686
|
+
])
|
|
687
|
+
const tet3 = barycentric_to_tetrahedral([
|
|
688
|
+
p3.x,
|
|
689
|
+
p3.y,
|
|
690
|
+
p3.z,
|
|
691
|
+
1 - p3.x - p3.y - p3.z,
|
|
692
|
+
])
|
|
693
|
+
|
|
694
|
+
// Project to 2D screen space
|
|
695
|
+
const proj0 = project_3d_point(tet0.x, tet0.y, tet0.z)
|
|
696
|
+
const proj1 = project_3d_point(tet1.x, tet1.y, tet1.z)
|
|
697
|
+
const proj2 = project_3d_point(tet2.x, tet2.y, tet2.z)
|
|
698
|
+
const proj3 = project_3d_point(tet3.x, tet3.y, tet3.z)
|
|
699
|
+
|
|
700
|
+
// Compute tetrahedron centroid in barycentric coords (for dominant_element mode)
|
|
701
|
+
// All 4 faces share the same tetrahedron, so they get the same color for facet_index
|
|
702
|
+
const tet_centroid_bary = [
|
|
703
|
+
(p0.x + p1.x + p2.x + p3.x) / 4,
|
|
704
|
+
(p0.y + p1.y + p2.y + p3.y) / 4,
|
|
705
|
+
(p0.z + p1.z + p2.z + p3.z) / 4,
|
|
706
|
+
((1 - p0.x - p0.y - p0.z) + (1 - p1.x - p1.y - p1.z) +
|
|
707
|
+
(1 - p2.x - p2.y - p2.z) + (1 - p3.x - p3.y - p3.z)) / 4,
|
|
708
|
+
]
|
|
709
|
+
|
|
710
|
+
// Each tetrahedron has 4 triangular faces
|
|
711
|
+
const faces: [typeof proj0, typeof proj1, typeof proj2, number][] = [
|
|
712
|
+
[proj0, proj1, proj2, (p0.w + p1.w + p2.w) / 3],
|
|
713
|
+
[proj0, proj1, proj3, (p0.w + p1.w + p3.w) / 3],
|
|
714
|
+
[proj0, proj2, proj3, (p0.w + p2.w + p3.w) / 3],
|
|
715
|
+
[proj1, proj2, proj3, (p1.w + p2.w + p3.w) / 3],
|
|
716
|
+
]
|
|
717
|
+
|
|
718
|
+
for (const [v0, v1, v2, avg_w] of faces) {
|
|
719
|
+
triangles.push({
|
|
720
|
+
vertices: [v0, v1, v2],
|
|
721
|
+
avg_depth: (v0.depth + v1.depth + v2.depth) / 3,
|
|
722
|
+
avg_w,
|
|
723
|
+
tet_idx,
|
|
724
|
+
centroid_bary: tet_centroid_bary,
|
|
725
|
+
})
|
|
726
|
+
}
|
|
494
727
|
}
|
|
728
|
+
|
|
495
729
|
// Sort by depth (back to front)
|
|
496
|
-
triangles.sort((a, b) => a.avg_depth - b.avg_depth)
|
|
730
|
+
triangles.sort((a, b) => a.avg_depth - b.avg_depth)
|
|
731
|
+
|
|
497
732
|
// Lazy computation for uniform mode: normalize alpha by formation energy
|
|
498
|
-
let norm_alpha = null
|
|
733
|
+
let norm_alpha: ((w: number) => number) | null = null
|
|
499
734
|
if (hull_face_color_mode === `uniform`) {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
735
|
+
const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
|
|
736
|
+
const min_fe = Math.min(0, ...formation_energies)
|
|
737
|
+
norm_alpha = (energy: number) => {
|
|
738
|
+
const t = Math.max(0, Math.min(1, (0 - energy) / Math.max(1e-6, 0 - min_fe)))
|
|
739
|
+
return t * hull_face_opacity
|
|
740
|
+
}
|
|
506
741
|
}
|
|
742
|
+
|
|
507
743
|
// Lazy computation for formation_energy mode
|
|
508
|
-
let energy_face_scale = null
|
|
509
|
-
let min_w = 0
|
|
744
|
+
let energy_face_scale: ((val: number) => string) | null = null
|
|
745
|
+
let min_w = 0
|
|
510
746
|
if (hull_face_color_mode === `formation_energy`) {
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
747
|
+
const all_avg_w = triangles.map((tri) => tri.avg_w)
|
|
748
|
+
min_w = Math.min(...all_avg_w)
|
|
749
|
+
energy_face_scale = helpers.get_energy_color_scale(
|
|
750
|
+
`energy`,
|
|
751
|
+
color_scale,
|
|
752
|
+
all_avg_w.map((energy) => ({ e_above_hull: energy - min_w })), // Normalize to 0-based
|
|
753
|
+
)
|
|
514
754
|
}
|
|
755
|
+
|
|
515
756
|
// Helper to get face color based on mode
|
|
516
|
-
const get_face_color = (tri) => {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
}
|
|
757
|
+
const get_face_color = (tri: TriangleFace): string => {
|
|
758
|
+
if (hull_face_color_mode === `uniform`) {
|
|
759
|
+
return hull_face_color
|
|
760
|
+
}
|
|
761
|
+
if (hull_face_color_mode === `formation_energy`) {
|
|
762
|
+
return energy_face_scale?.(tri.avg_w - min_w) ?? hull_face_color
|
|
763
|
+
}
|
|
764
|
+
if (hull_face_color_mode === `dominant_element`) {
|
|
765
|
+
// Find element with highest fraction
|
|
766
|
+
const max_idx = tri.centroid_bary.indexOf(Math.max(...tri.centroid_bary))
|
|
767
|
+
const el = elements[max_idx]
|
|
768
|
+
return element_colors[el] ?? `#888888`
|
|
769
|
+
}
|
|
770
|
+
if (hull_face_color_mode === `facet_index`) {
|
|
771
|
+
return PLOT_COLORS[tri.tet_idx % PLOT_COLORS.length]
|
|
772
|
+
}
|
|
773
|
+
return hull_face_color
|
|
774
|
+
}
|
|
775
|
+
|
|
534
776
|
// Draw each triangle
|
|
535
777
|
for (const tri of triangles) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
778
|
+
const [v0, v1, v2] = tri.vertices
|
|
779
|
+
// Uniform mode uses variable opacity; other modes use fixed opacity
|
|
780
|
+
const alpha = hull_face_color_mode === `uniform`
|
|
781
|
+
? (norm_alpha?.(tri.avg_w) ?? hull_face_opacity)
|
|
782
|
+
: hull_face_opacity
|
|
783
|
+
const face_color = get_face_color(tri)
|
|
784
|
+
|
|
785
|
+
ctx.save()
|
|
786
|
+
ctx.beginPath()
|
|
787
|
+
ctx.moveTo(v0.x, v0.y)
|
|
788
|
+
ctx.lineTo(v1.x, v1.y)
|
|
789
|
+
ctx.lineTo(v2.x, v2.y)
|
|
790
|
+
ctx.closePath()
|
|
791
|
+
|
|
792
|
+
ctx.fillStyle = add_alpha(face_color, alpha)
|
|
793
|
+
ctx.fill()
|
|
794
|
+
|
|
795
|
+
// Edge lines more pronounced with higher opacity
|
|
796
|
+
ctx.strokeStyle = add_alpha(face_color, Math.min(0.4, alpha * 4))
|
|
797
|
+
ctx.lineWidth = 1
|
|
798
|
+
ctx.stroke()
|
|
799
|
+
ctx.restore()
|
|
555
800
|
}
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function draw_data_points(): void {
|
|
804
|
+
if (!ctx || sorted_points_cache.length === 0) return
|
|
805
|
+
|
|
560
806
|
for (const { entry, projected } of sorted_points_cache) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
807
|
+
const is_stable = entry.is_stable || entry.e_above_hull === 0
|
|
808
|
+
const is_entry_highlighted = is_highlighted(entry)
|
|
809
|
+
const color = get_point_color(entry)
|
|
810
|
+
const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
|
|
811
|
+
const marker = entry.marker || `circle`
|
|
812
|
+
|
|
813
|
+
// Shadow
|
|
814
|
+
const shadow_offset = Math.abs(entry.z) * 2 * canvas_dims.scale
|
|
815
|
+
ctx.fillStyle = `rgba(0, 0, 0, 0.2)`
|
|
816
|
+
const shadow_path = helpers.create_marker_path(size * 0.8, marker)
|
|
817
|
+
ctx.save()
|
|
818
|
+
ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset)
|
|
819
|
+
ctx.fill(shadow_path)
|
|
820
|
+
ctx.restore()
|
|
821
|
+
|
|
822
|
+
// Highlights
|
|
823
|
+
if (selected_entry && entry.entry_id === selected_entry.entry_id) {
|
|
824
|
+
helpers.draw_selection_highlight(
|
|
825
|
+
ctx,
|
|
826
|
+
projected,
|
|
827
|
+
size,
|
|
828
|
+
canvas_dims.scale,
|
|
829
|
+
pulse_time,
|
|
830
|
+
pulse_opacity,
|
|
831
|
+
)
|
|
832
|
+
}
|
|
833
|
+
if (is_entry_highlighted) {
|
|
834
|
+
helpers.draw_highlight_effect(
|
|
835
|
+
ctx,
|
|
836
|
+
projected,
|
|
837
|
+
size,
|
|
838
|
+
canvas_dims.scale,
|
|
839
|
+
pulse_time,
|
|
840
|
+
merged_highlight_style,
|
|
841
|
+
)
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Main point with marker symbol
|
|
845
|
+
ctx.fillStyle =
|
|
846
|
+
is_entry_highlighted && merged_highlight_style.effect === `color`
|
|
847
|
+
? merged_highlight_style.color
|
|
848
|
+
: color
|
|
849
|
+
ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`
|
|
850
|
+
ctx.lineWidth = 0.5 * canvas_dims.scale
|
|
851
|
+
const marker_path = helpers.create_marker_path(size, marker)
|
|
852
|
+
ctx.save()
|
|
853
|
+
ctx.translate(projected.x, projected.y)
|
|
854
|
+
ctx.fill(marker_path)
|
|
855
|
+
ctx.stroke(marker_path)
|
|
856
|
+
ctx.restore()
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
if (!merged_config.show_labels) return
|
|
860
|
+
|
|
861
|
+
const label_entries = helpers.get_composition_label_entries(
|
|
862
|
+
sorted_points_cache
|
|
863
|
+
.map(({ entry }) => entry)
|
|
864
|
+
.filter((entry) => {
|
|
865
|
+
if (entry.is_element) return false
|
|
866
|
+
const is_stable = entry.is_stable || entry.e_above_hull === 0
|
|
867
|
+
return (is_stable && show_stable_labels) ||
|
|
596
868
|
(!is_stable && show_unstable_labels &&
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
869
|
+
(entry.e_above_hull ?? 0) <= max_hull_dist_show_labels)
|
|
870
|
+
}),
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
ctx.fillStyle = text_color
|
|
874
|
+
ctx.font = `${Math.round(12 * canvas_dims.scale)}px Arial`
|
|
875
|
+
ctx.textAlign = `center`
|
|
876
|
+
ctx.textBaseline = `middle`
|
|
877
|
+
|
|
878
|
+
for (const entry of label_entries) {
|
|
879
|
+
const is_stable = entry.is_stable || entry.e_above_hull === 0
|
|
880
|
+
const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
|
|
881
|
+
const projected = project_3d_point(entry.x, entry.y, entry.z)
|
|
882
|
+
const label = helpers.get_entry_label(entry, elements)
|
|
883
|
+
ctx.fillText(label, projected.x, projected.y + size + 6 * canvas_dims.scale)
|
|
607
884
|
}
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function render_frame(): void {
|
|
888
|
+
if (!ctx || !canvas) return
|
|
889
|
+
|
|
612
890
|
// Use CSS dimensions for rendering (already scaled by DPR in context)
|
|
613
|
-
const display_width = canvas.clientWidth || 600
|
|
614
|
-
const display_height = canvas.clientHeight || 600
|
|
615
|
-
|
|
616
|
-
ctx.
|
|
617
|
-
|
|
891
|
+
const display_width = canvas.clientWidth || 600
|
|
892
|
+
const display_height = canvas.clientHeight || 600
|
|
893
|
+
|
|
894
|
+
ctx.clearRect(0, 0, display_width, display_height) // Clear canvas
|
|
895
|
+
|
|
896
|
+
ctx.fillStyle = `transparent` // Set background - use transparent to inherit from container
|
|
897
|
+
ctx.fillRect(0, 0, display_width, display_height)
|
|
898
|
+
|
|
618
899
|
if (elements.length !== 4) {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
900
|
+
if (elements.length > 0) {
|
|
901
|
+
ctx.fillStyle = text_color
|
|
902
|
+
ctx.font = `16px Arial`
|
|
903
|
+
ctx.textAlign = `center`
|
|
904
|
+
ctx.textBaseline = `middle`
|
|
905
|
+
ctx.fillText(
|
|
906
|
+
`Quaternary convex hull requires exactly 4 elements (got ${elements.length})`,
|
|
907
|
+
display_width / 2,
|
|
908
|
+
display_height / 2,
|
|
909
|
+
)
|
|
910
|
+
}
|
|
911
|
+
return
|
|
627
912
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
913
|
+
|
|
914
|
+
draw_structure_outline() // Draw tetrahedron outline
|
|
915
|
+
|
|
916
|
+
draw_convex_hull_faces() // Draw convex hull faces (before points so they appear behind)
|
|
917
|
+
|
|
918
|
+
draw_data_points() // Draw data points (on top)
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function handle_mouse_down(event: MouseEvent) {
|
|
922
|
+
is_dragging = true
|
|
923
|
+
drag_started = false
|
|
924
|
+
hover_data = null
|
|
925
|
+
on_point_hover?.(null)
|
|
926
|
+
last_mouse = { x: event.clientX, y: event.clientY }
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const handle_mouse_move = (event: MouseEvent) => {
|
|
930
|
+
if (!is_dragging) return
|
|
931
|
+
const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y]
|
|
932
|
+
|
|
643
933
|
// Mark as drag if any movement occurred
|
|
644
|
-
if (dx !== 0 || dy !== 0)
|
|
645
|
-
|
|
934
|
+
if (dx !== 0 || dy !== 0) drag_started = true
|
|
935
|
+
|
|
646
936
|
// With Cmd/Ctrl held: pan the view instead of rotating
|
|
647
937
|
if (event.metaKey || event.ctrlKey) {
|
|
648
|
-
|
|
649
|
-
|
|
938
|
+
camera.center_x += dx
|
|
939
|
+
camera.center_y += dy
|
|
940
|
+
} else {
|
|
941
|
+
camera.rotation_y += dx * 0.005
|
|
942
|
+
camera.rotation_x = Math.max(
|
|
943
|
+
-Math.PI / 3,
|
|
944
|
+
Math.min(Math.PI / 3, camera.rotation_x - dy * 0.005),
|
|
945
|
+
)
|
|
650
946
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const entry = find_entry_at_mouse(event)
|
|
947
|
+
last_mouse = { x: event.clientX, y: event.clientY }
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
const handle_wheel = (event: WheelEvent) => {
|
|
951
|
+
event.preventDefault()
|
|
952
|
+
camera.zoom = Math.max(
|
|
953
|
+
1.0,
|
|
954
|
+
Math.min(15, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)),
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const handle_hover = (event: MouseEvent) => {
|
|
959
|
+
if (is_dragging) return
|
|
960
|
+
const entry = find_entry_at_mouse(event)
|
|
665
961
|
hover_data = entry
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
on_point_hover?.(hover_data)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
962
|
+
? { entry, position: { x: event.clientX, y: event.clientY } }
|
|
963
|
+
: null
|
|
964
|
+
on_point_hover?.(hover_data)
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
const find_entry_at_mouse = (event: MouseEvent): ConvexHullEntry | null =>
|
|
968
|
+
helpers.find_hull_entry_at_mouse(
|
|
969
|
+
canvas,
|
|
970
|
+
event,
|
|
971
|
+
plot_entries,
|
|
972
|
+
(x: number, y: number, z: number) => {
|
|
973
|
+
const projected = project_3d_point(x, y, z)
|
|
974
|
+
return { x: projected.x, y: projected.y }
|
|
975
|
+
},
|
|
976
|
+
)
|
|
977
|
+
|
|
978
|
+
const handle_click = (event: MouseEvent) => {
|
|
979
|
+
event.stopPropagation()
|
|
980
|
+
|
|
676
981
|
// Check if this was a drag operation (any mouse movement during drag)
|
|
677
|
-
const was_drag = drag_started
|
|
678
|
-
drag_started = false
|
|
679
|
-
if (was_drag)
|
|
680
|
-
|
|
681
|
-
const entry = find_entry_at_mouse(event)
|
|
982
|
+
const was_drag = drag_started
|
|
983
|
+
drag_started = false // Reset for next interaction
|
|
984
|
+
if (was_drag) return // Don't trigger click if this was a drag
|
|
985
|
+
|
|
986
|
+
const entry = find_entry_at_mouse(event)
|
|
682
987
|
if (entry) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
}
|
|
988
|
+
on_point_click?.(entry)
|
|
989
|
+
if (enable_click_selection) {
|
|
990
|
+
selected_entry = entry
|
|
991
|
+
if (enable_structure_preview) {
|
|
992
|
+
const structure = extract_structure_from_entry(entry)
|
|
993
|
+
if (structure) {
|
|
994
|
+
selected_structure = structure
|
|
995
|
+
modal_place_right = helpers.calculate_modal_side(wrapper)
|
|
996
|
+
modal_open = true
|
|
997
|
+
}
|
|
694
998
|
}
|
|
695
|
-
|
|
696
|
-
else if (modal_open)
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
function close_structure_popup() {
|
|
700
|
-
modal_open = false
|
|
701
|
-
selected_structure = null
|
|
702
|
-
selected_entry = null
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
|
|
999
|
+
}
|
|
1000
|
+
} else if (modal_open) close_structure_popup()
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function close_structure_popup() {
|
|
1004
|
+
modal_open = false
|
|
1005
|
+
selected_structure = null
|
|
1006
|
+
selected_entry = null
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const handle_double_click = (event: MouseEvent) => {
|
|
1010
|
+
const entry = find_entry_at_mouse(event)
|
|
706
1011
|
if (entry) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
1012
|
+
copy_entry_data(entry, {
|
|
1013
|
+
x: event.clientX,
|
|
1014
|
+
y: event.clientY,
|
|
1015
|
+
})
|
|
711
1016
|
}
|
|
712
|
-
}
|
|
713
|
-
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const render_once = () => {
|
|
714
1020
|
if (!frame_id) {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1021
|
+
frame_id = requestAnimationFrame(() => {
|
|
1022
|
+
render_frame()
|
|
1023
|
+
frame_id = 0
|
|
1024
|
+
})
|
|
719
1025
|
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
const dpr = globalThis.devicePixelRatio || 1
|
|
725
|
-
const container = canvas.parentElement
|
|
726
|
-
const rect = container?.getBoundingClientRect()
|
|
727
|
-
const [
|
|
728
|
-
|
|
729
|
-
canvas.
|
|
730
|
-
|
|
731
|
-
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function update_canvas_size() {
|
|
1029
|
+
if (!canvas) return
|
|
1030
|
+
const dpr = globalThis.devicePixelRatio || 1
|
|
1031
|
+
const container = canvas.parentElement
|
|
1032
|
+
const rect = container?.getBoundingClientRect()
|
|
1033
|
+
const [width, height] = rect ? [rect.width, rect.height] : [400, 400]
|
|
1034
|
+
|
|
1035
|
+
canvas.width = Math.max(0, Math.round(width * dpr))
|
|
1036
|
+
canvas.height = Math.max(0, Math.round(height * dpr))
|
|
1037
|
+
canvas_dims = { width, height, scale: Math.min(width, height) / 600 }
|
|
1038
|
+
|
|
1039
|
+
ctx = canvas.getContext(`2d`)
|
|
732
1040
|
if (ctx) {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1041
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
1042
|
+
ctx.imageSmoothingEnabled = true
|
|
1043
|
+
ctx.imageSmoothingQuality = `high`
|
|
736
1044
|
}
|
|
737
|
-
render_once()
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1045
|
+
render_once()
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
$effect(() => {
|
|
1049
|
+
if (!canvas) return
|
|
1050
|
+
|
|
742
1051
|
// Initial setup
|
|
743
|
-
update_canvas_size()
|
|
1052
|
+
update_canvas_size()
|
|
1053
|
+
|
|
744
1054
|
// Watch for resize events - only update canvas, don't reset camera
|
|
745
|
-
const resize_observer = new ResizeObserver(update_canvas_size)
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
})
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
$
|
|
1055
|
+
const resize_observer = new ResizeObserver(update_canvas_size)
|
|
1056
|
+
|
|
1057
|
+
const container = canvas.parentElement
|
|
1058
|
+
if (container) resize_observer.observe(container)
|
|
1059
|
+
|
|
1060
|
+
return () => { // Cleanup on unmount
|
|
1061
|
+
if (frame_id) cancelAnimationFrame(frame_id)
|
|
1062
|
+
resize_observer.disconnect()
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
// Fullscreen handling with camera reset
|
|
1067
|
+
let was_fullscreen = $state(fullscreen)
|
|
1068
|
+
$effect(() => {
|
|
758
1069
|
setup_fullscreen_effect(fullscreen, wrapper, (entering_fullscreen) => {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
})
|
|
765
|
-
set_fullscreen_bg(wrapper, fullscreen, `--hull-4d-bg-fullscreen`)
|
|
766
|
-
})
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1070
|
+
if (entering_fullscreen !== was_fullscreen) {
|
|
1071
|
+
camera.center_x = 0
|
|
1072
|
+
camera.center_y = 20
|
|
1073
|
+
was_fullscreen = entering_fullscreen
|
|
1074
|
+
}
|
|
1075
|
+
})
|
|
1076
|
+
set_fullscreen_bg(wrapper, fullscreen, `--hull-4d-bg-fullscreen`)
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
// Performance: Cache canvas dimensions and pre-compute sorted point projections
|
|
1080
|
+
let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
|
|
1081
|
+
const sorted_points_cache = $derived.by(() => {
|
|
1082
|
+
if (!canvas || plot_entries.length === 0) return []
|
|
772
1083
|
return plot_entries
|
|
773
|
-
|
|
774
|
-
|
|
1084
|
+
.filter((entry) => entry.visible)
|
|
1085
|
+
.map((entry) => ({
|
|
775
1086
|
entry,
|
|
776
1087
|
projected: project_3d_point(entry.x, entry.y, entry.z),
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
})
|
|
780
|
-
|
|
1088
|
+
}))
|
|
1089
|
+
.sort((a, b) => a.projected.depth - b.projected.depth)
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
let style = $derived(
|
|
1093
|
+
`--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
|
|
781
1094
|
--hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
|
|
782
1095
|
--hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
|
|
783
|
-
--hull-text-color:${
|
|
1096
|
+
--hull-text-color:${
|
|
1097
|
+
merged_config.colors?.annotation || `var(--text-color, #212121)`
|
|
1098
|
+
}`,
|
|
1099
|
+
)
|
|
784
1100
|
</script>
|
|
785
1101
|
|
|
786
1102
|
<svelte:document
|
|
@@ -823,7 +1139,7 @@ let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#00
|
|
|
823
1139
|
selected_entry,
|
|
824
1140
|
})}
|
|
825
1141
|
<h3 style="position: absolute; left: 1em; top: 1ex; margin: 0">
|
|
826
|
-
{@html merged_controls.title || phase_stats?.chemical_system || ``}
|
|
1142
|
+
{@html sanitize_html(merged_controls.title || phase_stats?.chemical_system || ``)}
|
|
827
1143
|
</h3>
|
|
828
1144
|
|
|
829
1145
|
<canvas
|
|
@@ -848,8 +1164,8 @@ let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#00
|
|
|
848
1164
|
<!-- Energy above hull Color Bar -->
|
|
849
1165
|
{#if color_mode === `energy` && plot_entries.length > 0}
|
|
850
1166
|
{@const hull_distances = plot_entries
|
|
851
|
-
.map((
|
|
852
|
-
.filter((
|
|
1167
|
+
.map((entry) => entry.e_above_hull)
|
|
1168
|
+
.filter((val): val is number => typeof val === `number`)}
|
|
853
1169
|
{@const min_energy = hull_distances.length > 0 ? Math.min(...hull_distances) : 0}
|
|
854
1170
|
{@const max_energy = hull_distances.length > 0 ? Math.max(...hull_distances, 0.1) : 0.1}
|
|
855
1171
|
<ColorBar
|