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,949 +1,1418 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { D3InterpolateName } from '../colors'
|
|
3
|
+
import {
|
|
4
|
+
add_alpha,
|
|
5
|
+
AXIS_COLORS,
|
|
6
|
+
is_dark_mode,
|
|
7
|
+
NEG_AXIS_COLORS,
|
|
8
|
+
PLOT_COLORS,
|
|
9
|
+
vesta_hex,
|
|
10
|
+
watch_dark_mode,
|
|
11
|
+
} from '../colors'
|
|
12
|
+
import {
|
|
13
|
+
get_formula_label_segments,
|
|
14
|
+
type FormulaLabelSegment,
|
|
15
|
+
} from '../composition/format'
|
|
16
|
+
import { normalize_show_controls } from '../controls'
|
|
17
|
+
import { sanitize_html } from '../sanitize'
|
|
18
|
+
import { ClickFeedback, DragOverlay, Spinner } from '../feedback'
|
|
19
|
+
import Icon from '../Icon.svelte'
|
|
20
|
+
import { format_num } from '../labels'
|
|
21
|
+
import {
|
|
22
|
+
set_fullscreen_bg,
|
|
23
|
+
setup_fullscreen_effect,
|
|
24
|
+
toggle_fullscreen,
|
|
25
|
+
} from '../layout'
|
|
26
|
+
import { to_radians, type Vec3 } from '../math'
|
|
27
|
+
import { ColorBar, PlotTooltip } from '../plot'
|
|
28
|
+
import {
|
|
29
|
+
centered_rect,
|
|
30
|
+
pad_rect,
|
|
31
|
+
rects_overlap,
|
|
32
|
+
rect_within_rect,
|
|
33
|
+
type Rect,
|
|
34
|
+
} from '../plot/layout'
|
|
35
|
+
import { DEFAULTS } from '../settings'
|
|
36
|
+
import type { AnyStructure } from '../structure'
|
|
37
|
+
import { Canvas, T } from '@threlte/core'
|
|
38
|
+
import * as extras from '@threlte/extras'
|
|
39
|
+
import { ticks } from 'd3-array'
|
|
40
|
+
import { PerspectiveCamera, WebGLRenderer } from 'three'
|
|
41
|
+
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
42
|
+
import {
|
|
43
|
+
get_ternary_3d_coordinates,
|
|
44
|
+
get_triangle_centroid,
|
|
45
|
+
get_triangle_edges,
|
|
46
|
+
get_triangle_vertical_edges,
|
|
47
|
+
TRIANGLE_VERTICES,
|
|
48
|
+
} from './barycentric-coords'
|
|
49
|
+
import ConvexHullControls from './ConvexHullControls.svelte'
|
|
50
|
+
import ConvexHullInfoPane from './ConvexHullInfoPane.svelte'
|
|
51
|
+
import ConvexHullTooltip from './ConvexHullTooltip.svelte'
|
|
52
|
+
import GasPressureControls from './GasPressureControls.svelte'
|
|
53
|
+
import * as helpers from './helpers'
|
|
54
|
+
import type { BaseConvexHullProps, Hull3DProps } from './index'
|
|
55
|
+
import { CONVEX_HULL_STYLE, default_controls, default_hull_config } from './index'
|
|
56
|
+
import StructurePopup from './StructurePopup.svelte'
|
|
57
|
+
import TemperatureSlider from './TemperatureSlider.svelte'
|
|
58
|
+
import * as thermo from './thermodynamics'
|
|
59
|
+
import type {
|
|
60
|
+
ConvexHullEntry,
|
|
61
|
+
ConvexHullTriangle,
|
|
62
|
+
HighlightStyle,
|
|
63
|
+
HoverData3D,
|
|
64
|
+
HullFaceColorMode,
|
|
65
|
+
LabelPlacement,
|
|
66
|
+
Point3D,
|
|
67
|
+
} from './types'
|
|
68
|
+
import { compute_hull_stability } from './helpers'
|
|
69
|
+
|
|
70
|
+
let {
|
|
71
|
+
entries = [],
|
|
72
|
+
controls = {},
|
|
73
|
+
config = {},
|
|
74
|
+
on_point_click,
|
|
75
|
+
on_point_hover,
|
|
76
|
+
fullscreen = $bindable(DEFAULTS.convex_hull.ternary.fullscreen),
|
|
77
|
+
enable_fullscreen = true,
|
|
78
|
+
enable_info_pane = true,
|
|
79
|
+
wrapper = $bindable(),
|
|
80
|
+
label_threshold = 50,
|
|
81
|
+
show_stable = $bindable(DEFAULTS.convex_hull.ternary.show_stable),
|
|
82
|
+
show_unstable = $bindable(DEFAULTS.convex_hull.ternary.show_unstable),
|
|
83
|
+
show_hull_faces = $bindable(DEFAULTS.convex_hull.ternary.show_hull_faces),
|
|
84
|
+
hull_face_opacity = $bindable(DEFAULTS.convex_hull.ternary.hull_face_opacity),
|
|
85
|
+
hull_face_color_mode = $bindable(
|
|
86
|
+
DEFAULTS.convex_hull.ternary.hull_face_color_mode as HullFaceColorMode,
|
|
87
|
+
),
|
|
88
|
+
element_colors = vesta_hex,
|
|
89
|
+
color_mode = $bindable(DEFAULTS.convex_hull.ternary.color_mode),
|
|
90
|
+
color_scale = $bindable(
|
|
91
|
+
DEFAULTS.convex_hull.ternary.color_scale as D3InterpolateName,
|
|
92
|
+
),
|
|
93
|
+
info_pane_open = $bindable(DEFAULTS.convex_hull.ternary.info_pane_open),
|
|
94
|
+
legend_pane_open = $bindable(DEFAULTS.convex_hull.ternary.legend_pane_open),
|
|
95
|
+
max_hull_dist_show_phases = $bindable(
|
|
96
|
+
DEFAULTS.convex_hull.ternary.max_hull_dist_show_phases,
|
|
97
|
+
),
|
|
98
|
+
max_hull_dist_show_labels = $bindable(
|
|
99
|
+
DEFAULTS.convex_hull.ternary.max_hull_dist_show_labels,
|
|
100
|
+
),
|
|
101
|
+
show_stable_labels = $bindable(DEFAULTS.convex_hull.ternary.show_stable_labels),
|
|
102
|
+
show_unstable_labels = $bindable(
|
|
103
|
+
DEFAULTS.convex_hull.ternary.show_unstable_labels,
|
|
104
|
+
),
|
|
105
|
+
on_file_drop,
|
|
106
|
+
enable_click_selection = true,
|
|
107
|
+
enable_structure_preview = true,
|
|
108
|
+
energy_source_mode = $bindable(`precomputed`),
|
|
109
|
+
phase_stats = $bindable(null),
|
|
110
|
+
stable_entries = $bindable([]),
|
|
111
|
+
unstable_entries = $bindable([]),
|
|
112
|
+
highlighted_entries = $bindable([]),
|
|
113
|
+
highlight_style = {},
|
|
114
|
+
selected_entry = $bindable(null),
|
|
115
|
+
temperature = $bindable(),
|
|
116
|
+
interpolate_temperature = true,
|
|
117
|
+
max_interpolation_gap = 500,
|
|
118
|
+
gizmo = true,
|
|
119
|
+
gas_config,
|
|
120
|
+
gas_pressures = $bindable({}),
|
|
121
|
+
children,
|
|
122
|
+
tooltip,
|
|
123
|
+
...rest
|
|
124
|
+
}: BaseConvexHullProps<ConvexHullEntry> & Hull3DProps & {
|
|
125
|
+
highlight_style?: HighlightStyle
|
|
126
|
+
} = $props()
|
|
127
|
+
|
|
128
|
+
const merged_controls = $derived({ ...default_controls, ...controls })
|
|
129
|
+
const controls_config = $derived(normalize_show_controls(merged_controls.show))
|
|
130
|
+
const merged_config = $derived({
|
|
29
131
|
...default_hull_config,
|
|
30
132
|
...config,
|
|
31
133
|
colors: { ...default_hull_config.colors, ...(config.colors || {}) },
|
|
32
134
|
margin: { t: 40, r: 40, b: 60, l: 60, ...(config.margin || {}) },
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
// Temperature-dependent free energy support
|
|
138
|
+
const { has_temp_data, available_temperatures } = $derived(
|
|
139
|
+
helpers.analyze_temperature_data(entries),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// Initialize or reset temperature when it's undefined or no longer valid
|
|
143
|
+
$effect(() => {
|
|
144
|
+
if (
|
|
145
|
+
has_temp_data &&
|
|
146
|
+
available_temperatures.length > 0 &&
|
|
147
|
+
(temperature === undefined || !available_temperatures.includes(temperature))
|
|
148
|
+
) temperature = available_temperatures[0]
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// Filter entries by temperature when in temperature mode
|
|
152
|
+
const temp_filtered_entries = $derived(
|
|
153
|
+
has_temp_data && temperature !== undefined
|
|
154
|
+
? helpers.filter_entries_at_temperature(entries, temperature, {
|
|
46
155
|
interpolate: interpolate_temperature,
|
|
47
156
|
max_interpolation_gap,
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
157
|
+
})
|
|
158
|
+
: entries,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// Gas-dependent chemical potential support (corrections based on T, P)
|
|
162
|
+
// Default to DEFAULT_GAS_TEMP (room temperature) when no temperature specified
|
|
163
|
+
const {
|
|
164
|
+
entries: gas_corrected_entries,
|
|
165
|
+
analysis: gas_analysis,
|
|
166
|
+
merged_config: merged_gas_config,
|
|
167
|
+
} = $derived(
|
|
168
|
+
helpers.get_gas_corrected_entries(
|
|
169
|
+
temp_filtered_entries,
|
|
170
|
+
gas_config,
|
|
171
|
+
gas_pressures,
|
|
172
|
+
temperature ?? helpers.DEFAULT_GAS_TEMP,
|
|
173
|
+
),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
let { // Compute energy mode information
|
|
177
|
+
has_precomputed_e_form,
|
|
178
|
+
has_precomputed_hull,
|
|
179
|
+
can_compute_e_form,
|
|
180
|
+
can_compute_hull,
|
|
181
|
+
energy_mode,
|
|
182
|
+
unary_refs,
|
|
183
|
+
} = $derived(
|
|
184
|
+
helpers.compute_energy_mode_info(
|
|
185
|
+
gas_corrected_entries,
|
|
186
|
+
thermo.find_lowest_energy_unary_refs,
|
|
187
|
+
energy_source_mode,
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
const effective_entries = $derived(
|
|
192
|
+
helpers.get_effective_entries(
|
|
193
|
+
gas_corrected_entries,
|
|
194
|
+
energy_mode,
|
|
195
|
+
unary_refs,
|
|
196
|
+
thermo.compute_e_form_per_atom,
|
|
197
|
+
),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
// Process convex hull data with unified PhaseData interface using effective entries
|
|
201
|
+
const pd_data = $derived(thermo.process_hull_entries(effective_entries))
|
|
202
|
+
|
|
203
|
+
// Pre-compute polymorph stats once for O(1) tooltip lookups
|
|
204
|
+
const polymorph_stats_map = $derived(
|
|
205
|
+
helpers.compute_all_polymorph_stats(effective_entries),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
const elements = $derived.by(() => {
|
|
61
209
|
if (pd_data.elements.length > 3) {
|
|
62
|
-
|
|
63
|
-
|
|
210
|
+
console.error(
|
|
211
|
+
`ConvexHull3D: Dataset contains ${pd_data.elements.length} elements, but ternary diagrams require exactly 3. Found: [${
|
|
212
|
+
pd_data.elements.join(`, `)
|
|
213
|
+
}]`,
|
|
214
|
+
)
|
|
215
|
+
return []
|
|
64
216
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
217
|
+
|
|
218
|
+
return pd_data.elements
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
// 1) Raw 3D coordinates (formation-energy z), independent of hull state
|
|
222
|
+
const coords_entries = $derived.by(() => {
|
|
223
|
+
if (elements.length !== 3) return []
|
|
71
224
|
try {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
225
|
+
// Pass precomputed el_refs to avoid recomputing in error diagnostics
|
|
226
|
+
const coords = get_ternary_3d_coordinates(
|
|
227
|
+
pd_data.entries,
|
|
228
|
+
elements,
|
|
229
|
+
pd_data.el_refs,
|
|
230
|
+
)
|
|
231
|
+
return coords
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.error(`Error computing ternary coordinates:`, error)
|
|
234
|
+
return []
|
|
75
235
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
// Compute lower convex hull faces (triangles) for 3D rendering (low energy hull only)
|
|
239
|
+
// Must be defined before all_enriched_entries which uses hull_model
|
|
240
|
+
const hull_faces = $derived.by((): ConvexHullTriangle[] => {
|
|
241
|
+
if (coords_entries.length === 0) return []
|
|
242
|
+
// Excluded entries don't participate in hull construction
|
|
243
|
+
const hull_entries = coords_entries.filter((e) => !e.exclude_from_hull)
|
|
244
|
+
if (hull_entries.length === 0) return []
|
|
245
|
+
const points = hull_entries.map((e) => ({ x: e.x, y: e.y, z: e.z }))
|
|
85
246
|
try {
|
|
86
|
-
|
|
247
|
+
return thermo.compute_lower_hull_triangles(points)
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.error(`Error computing convex hull:`, error)
|
|
250
|
+
return []
|
|
87
251
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Cached hull model for e_above_hull queries; recompute only when faces change
|
|
255
|
+
let hull_model = $derived.by(() => thermo.build_lower_hull_model(hull_faces))
|
|
256
|
+
|
|
257
|
+
// Enrich coords with e_above_hull from cached hull model (before filtering)
|
|
258
|
+
const all_enriched_entries = $derived.by(() => {
|
|
259
|
+
if (coords_entries.length === 0) return []
|
|
260
|
+
if (energy_mode !== `on-the-fly`) return coords_entries
|
|
261
|
+
const pts = coords_entries.map((e) => ({ x: e.x, y: e.y, z: e.z }))
|
|
262
|
+
const raw_dists = thermo.compute_e_above_hull_for_points(pts, hull_model)
|
|
263
|
+
return coords_entries.map((entry, idx) => ({
|
|
264
|
+
...entry, ...compute_hull_stability(raw_dists[idx], entry.exclude_from_hull),
|
|
265
|
+
}))
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
// Auto threshold: show all for few entries, use default for many, interpolate between
|
|
269
|
+
const max_hull_dist_in_data = $derived(
|
|
270
|
+
helpers.calc_max_hull_dist_in_data(all_enriched_entries),
|
|
271
|
+
)
|
|
272
|
+
const auto_default_threshold = $derived(helpers.compute_auto_hull_dist_threshold(
|
|
273
|
+
all_enriched_entries.length,
|
|
274
|
+
max_hull_dist_in_data,
|
|
275
|
+
DEFAULTS.convex_hull.ternary.max_hull_dist_show_phases,
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
// Initialize threshold to auto value on first load
|
|
279
|
+
let initialized = $state(false)
|
|
280
|
+
$effect(() => {
|
|
111
281
|
if (!initialized && all_enriched_entries.length > 0) {
|
|
112
|
-
|
|
113
|
-
|
|
282
|
+
initialized = true
|
|
283
|
+
max_hull_dist_show_phases = auto_default_threshold
|
|
114
284
|
}
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
// Filter by threshold and compute visibility
|
|
288
|
+
const plot_entries = $derived(
|
|
289
|
+
all_enriched_entries
|
|
290
|
+
.filter((e) => (e.e_above_hull ?? 0) <= max_hull_dist_show_phases)
|
|
291
|
+
.map((e) => ({
|
|
292
|
+
...e,
|
|
293
|
+
visible: ((e.is_stable || e.e_above_hull === 0) && show_stable) ||
|
|
294
|
+
(!(e.is_stable || e.e_above_hull === 0) && show_unstable),
|
|
295
|
+
})),
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
$effect(() => {
|
|
299
|
+
stable_entries = plot_entries.filter((entry: ConvexHullEntry) =>
|
|
300
|
+
entry.is_stable || entry.e_above_hull === 0
|
|
301
|
+
)
|
|
302
|
+
unstable_entries = plot_entries.filter((entry: ConvexHullEntry) =>
|
|
303
|
+
typeof entry.e_above_hull === `number` && entry.e_above_hull > 0 &&
|
|
304
|
+
!entry.is_stable
|
|
305
|
+
)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
// Canvas rendering
|
|
309
|
+
let canvas: HTMLCanvasElement
|
|
310
|
+
let ctx: CanvasRenderingContext2D | null = null
|
|
311
|
+
|
|
312
|
+
// Performance optimization
|
|
313
|
+
let frame_id = 0
|
|
314
|
+
let pulse_frame_id = 0
|
|
315
|
+
|
|
316
|
+
const camera_default = {
|
|
136
317
|
elevation: DEFAULTS.convex_hull.ternary.camera_elevation,
|
|
137
318
|
azimuth: DEFAULTS.convex_hull.ternary.camera_azimuth,
|
|
138
319
|
zoom: DEFAULTS.convex_hull.ternary.camera_zoom,
|
|
139
320
|
center_x: 0,
|
|
140
321
|
center_y: -50, // Shift up to better show the formation energy funnel
|
|
141
|
-
}
|
|
142
|
-
let camera = $state({ ...camera_default })
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
let
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
322
|
+
}
|
|
323
|
+
let camera = $state({ ...camera_default })
|
|
324
|
+
|
|
325
|
+
// === Gizmo state & coordinate mapping ===
|
|
326
|
+
// ConvexHull3D uses Rz(azimuth) then Rx(-elevation), viewing along -z_rotated.
|
|
327
|
+
// These helpers convert between that system and Three.js camera position/up.
|
|
328
|
+
const GIZMO_CAM_DIST = 5
|
|
329
|
+
const MIN_ELEV_FOR_Z_AXIS = 5 // degrees — below this, z-axis ticks collapse to a point
|
|
330
|
+
let gizmo_cam_ref = $state<PerspectiveCamera>()
|
|
331
|
+
let gizmo_orbit_ref = $state<OrbitControls | undefined>(undefined)
|
|
332
|
+
let gizmo_active = $state(false)
|
|
333
|
+
|
|
334
|
+
// Convert elevation/azimuth (degrees) to Three.js camera position + up vector.
|
|
335
|
+
function gizmo_camera(
|
|
336
|
+
elev_deg: number,
|
|
337
|
+
azim_deg: number,
|
|
338
|
+
): { position: Vec3; up: Vec3 } {
|
|
339
|
+
const [elev, azim] = [to_radians(elev_deg), to_radians(azim_deg)]
|
|
154
340
|
const [se, ce, sa, ca] = [
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
]
|
|
341
|
+
Math.sin(elev),
|
|
342
|
+
Math.cos(elev),
|
|
343
|
+
Math.sin(azim),
|
|
344
|
+
Math.cos(azim),
|
|
345
|
+
]
|
|
160
346
|
return {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
347
|
+
position: [
|
|
348
|
+
-sa * se * GIZMO_CAM_DIST,
|
|
349
|
+
-ca * se * GIZMO_CAM_DIST,
|
|
350
|
+
ce * GIZMO_CAM_DIST,
|
|
351
|
+
],
|
|
352
|
+
up: [sa * ce, ca * ce, se],
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Derived gizmo camera state, avoids recomputing in the template
|
|
357
|
+
const gizmo_cam_state = $derived(gizmo_camera(camera.elevation, camera.azimuth))
|
|
358
|
+
|
|
359
|
+
// Center camera on the triangle's visual center for a given elevation.
|
|
360
|
+
// The centroid (rotation center) sits at 1/3 height while the bbox
|
|
361
|
+
// center is at 1/2 height — a difference of sqrt(3)/12 in data units.
|
|
362
|
+
// Scale by cos(elevation) so offset only applies in near-top-down views.
|
|
363
|
+
function center_camera(elev_deg: number): void {
|
|
364
|
+
camera.center_x = 0
|
|
177
365
|
// 0.6 matches the draw_data_points() scale factor that maps data coords to canvas pixels
|
|
178
|
-
const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
|
|
179
|
-
camera.center_y = Math.sqrt(3) / 12 * scale * Math.cos(to_radians(elev_deg))
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const cam = gizmo_cam_ref
|
|
186
|
-
if (!cam)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
cam.
|
|
190
|
-
cam.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Sync: gizmo → ConvexHull3D (during and after gizmo animation)
|
|
195
|
-
function sync_gizmo_to_camera() {
|
|
196
|
-
const cam = gizmo_cam_ref
|
|
197
|
-
if (!cam)
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const elev_rad = Math.acos(Math.max(-1, Math.min(1, cz / dist)));
|
|
204
|
-
const sin_elev = Math.sin(elev_rad);
|
|
366
|
+
const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
|
|
367
|
+
camera.center_y = Math.sqrt(3) / 12 * scale * Math.cos(to_radians(elev_deg))
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Sync: ConvexHull3D → Three.js gizmo camera (on main canvas drag)
|
|
371
|
+
$effect(() => {
|
|
372
|
+
if (gizmo_active) return
|
|
373
|
+
const cam = gizmo_cam_ref
|
|
374
|
+
if (!cam) return
|
|
375
|
+
const { position, up } = gizmo_camera(camera.elevation, camera.azimuth)
|
|
376
|
+
cam.position.set(...position)
|
|
377
|
+
cam.up.set(...up)
|
|
378
|
+
cam.lookAt(0, 0, 0)
|
|
379
|
+
gizmo_orbit_ref?.update?.()
|
|
380
|
+
})
|
|
381
|
+
|
|
382
|
+
// Sync: gizmo → ConvexHull3D (during and after gizmo animation)
|
|
383
|
+
function sync_gizmo_to_camera(): void {
|
|
384
|
+
const cam = gizmo_cam_ref
|
|
385
|
+
if (!cam) return
|
|
386
|
+
const { x: cx, y: cy, z: cz } = cam.position
|
|
387
|
+
const dist = Math.sqrt(cx * cx + cy * cy + cz * cz)
|
|
388
|
+
if (dist < 1e-6) return
|
|
389
|
+
const elev_rad = Math.acos(Math.max(-1, Math.min(1, cz / dist)))
|
|
390
|
+
const sin_elev = Math.sin(elev_rad)
|
|
205
391
|
const azim_deg = Math.abs(sin_elev) > 1e-6
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const elev_deg = elev_rad * 180 / Math.PI
|
|
209
|
-
camera.elevation = elev_deg
|
|
210
|
-
camera.azimuth = azim_deg
|
|
211
|
-
center_camera(elev_deg)
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
392
|
+
? Math.atan2(-cx / (dist * sin_elev), -cy / (dist * sin_elev)) * 180 / Math.PI
|
|
393
|
+
: 0
|
|
394
|
+
const elev_deg = elev_rad * 180 / Math.PI
|
|
395
|
+
camera.elevation = elev_deg
|
|
396
|
+
camera.azimuth = azim_deg
|
|
397
|
+
center_camera(elev_deg)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Gizmo axis colors (constant — AXIS_COLORS/NEG_AXIS_COLORS never change)
|
|
401
|
+
const gizmo_axis_options = Object.fromEntries(
|
|
402
|
+
[...AXIS_COLORS, ...NEG_AXIS_COLORS].map((
|
|
403
|
+
[axis, color, hover_color],
|
|
404
|
+
) => [axis, {
|
|
405
|
+
color,
|
|
406
|
+
labelColor: `#111`,
|
|
407
|
+
opacity: 0.85,
|
|
408
|
+
hover: { color: hover_color, labelColor: `#222`, opacity: 1 },
|
|
409
|
+
}]),
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
// Extract placement from gizmo options (not a Threlte Gizmo prop)
|
|
413
|
+
const gizmo_placement = $derived(
|
|
414
|
+
typeof gizmo === `object` && gizmo?.placement ? gizmo.placement : `top-right`,
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
// Merge constant axis options with consumer overrides (exclude our custom placement)
|
|
418
|
+
const gizmo_props = $derived.by(() => {
|
|
224
419
|
if (typeof gizmo !== `object` || !gizmo) {
|
|
225
|
-
|
|
420
|
+
return { background: { enabled: false }, size: 80, ...gizmo_axis_options }
|
|
226
421
|
}
|
|
227
|
-
const { placement: _, ...threlte_opts } = gizmo
|
|
422
|
+
const { placement: _, ...threlte_opts } = gizmo
|
|
228
423
|
return {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
let
|
|
238
|
-
let
|
|
239
|
-
let
|
|
240
|
-
let
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
let
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
let
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
//
|
|
255
|
-
|
|
256
|
-
$
|
|
257
|
-
|
|
258
|
-
|
|
424
|
+
background: { enabled: false },
|
|
425
|
+
size: 80,
|
|
426
|
+
...gizmo_axis_options,
|
|
427
|
+
...threlte_opts,
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
// Interaction state
|
|
432
|
+
let is_dragging = $state(false)
|
|
433
|
+
let drag_started = $state(false)
|
|
434
|
+
let last_mouse = $state({ x: 0, y: 0 })
|
|
435
|
+
let hover_data = $state<HoverData3D<ConvexHullEntry> | null>(null)
|
|
436
|
+
let copy_feedback = $state({ visible: false, position: { x: 0, y: 0 } })
|
|
437
|
+
|
|
438
|
+
// Drag and drop state
|
|
439
|
+
let drag_over = $state(false)
|
|
440
|
+
|
|
441
|
+
// Structure popup state
|
|
442
|
+
let modal_open = $state(false)
|
|
443
|
+
let selected_structure = $state<AnyStructure | null>(null)
|
|
444
|
+
let modal_place_right = $state(true)
|
|
445
|
+
|
|
446
|
+
// Hull face color (customizable via controls)
|
|
447
|
+
let hull_face_color = $state(`#4caf50`)
|
|
448
|
+
|
|
449
|
+
// Pulsating highlight for selected point
|
|
450
|
+
let pulse_time = $state(0)
|
|
451
|
+
let pulse_opacity = $derived(0.3 + 0.4 * Math.sin(pulse_time * 4))
|
|
452
|
+
|
|
453
|
+
// Merge highlight style with defaults
|
|
454
|
+
const merged_highlight_style = $derived(
|
|
455
|
+
helpers.merge_highlight_style(highlight_style),
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
// Helper to check if entry is highlighted
|
|
459
|
+
const is_highlighted = (entry: ConvexHullEntry): boolean =>
|
|
460
|
+
helpers.is_entry_highlighted(entry, highlighted_entries)
|
|
461
|
+
|
|
462
|
+
$effect(() => {
|
|
463
|
+
if (!selected_entry && !highlighted_entries.length) return
|
|
259
464
|
const animate = () => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
264
|
-
pulse_frame_id = requestAnimationFrame(animate)
|
|
465
|
+
pulse_time += 0.02
|
|
466
|
+
render_once()
|
|
467
|
+
pulse_frame_id = requestAnimationFrame(animate)
|
|
468
|
+
}
|
|
469
|
+
pulse_frame_id = requestAnimationFrame(animate)
|
|
265
470
|
return () => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
// Re-render when important state changes
|
|
271
|
-
$effect(() => {
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
render_once()
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
471
|
+
if (pulse_frame_id) cancelAnimationFrame(pulse_frame_id)
|
|
472
|
+
}
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// Re-render when important state changes
|
|
476
|
+
$effect(() => {
|
|
477
|
+
// oxfmt-ignore
|
|
478
|
+
void [show_hull_faces, color_mode, color_scale, show_stable_labels, show_unstable_labels, max_hull_dist_show_labels, camera.elevation, camera.azimuth, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, highlighted_entries, text_color] // track reactively
|
|
479
|
+
|
|
480
|
+
render_once()
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
// Function to extract structure data from a convex hull entry
|
|
484
|
+
function extract_structure_from_entry(
|
|
485
|
+
entry: ConvexHullEntry,
|
|
486
|
+
): AnyStructure | null {
|
|
487
|
+
const orig_entry = entries.find((ent) => ent.entry_id === entry.entry_id)
|
|
488
|
+
return orig_entry?.structure as AnyStructure || null
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const reset_camera = () => Object.assign(camera, camera_default)
|
|
492
|
+
function reset_all() {
|
|
493
|
+
reset_camera()
|
|
494
|
+
fullscreen = DEFAULTS.convex_hull.ternary.fullscreen
|
|
495
|
+
info_pane_open = DEFAULTS.convex_hull.ternary.info_pane_open
|
|
496
|
+
legend_pane_open = DEFAULTS.convex_hull.ternary.legend_pane_open
|
|
497
|
+
color_mode = DEFAULTS.convex_hull.ternary.color_mode
|
|
498
|
+
color_scale = DEFAULTS.convex_hull.ternary.color_scale as D3InterpolateName
|
|
499
|
+
show_stable = DEFAULTS.convex_hull.ternary.show_stable
|
|
500
|
+
show_unstable = DEFAULTS.convex_hull.ternary.show_unstable
|
|
501
|
+
show_stable_labels = DEFAULTS.convex_hull.ternary.show_stable_labels
|
|
502
|
+
show_unstable_labels = DEFAULTS.convex_hull.ternary.show_unstable_labels
|
|
503
|
+
max_hull_dist_show_labels = DEFAULTS.convex_hull.ternary.max_hull_dist_show_labels
|
|
295
504
|
// Use auto-computed threshold based on entry count instead of static default
|
|
296
|
-
max_hull_dist_show_phases = auto_default_threshold
|
|
297
|
-
show_hull_faces = DEFAULTS.convex_hull.ternary.show_hull_faces
|
|
298
|
-
hull_face_color = DEFAULTS.convex_hull.ternary.hull_face_color
|
|
299
|
-
hull_face_opacity = DEFAULTS.convex_hull.ternary.hull_face_opacity
|
|
505
|
+
max_hull_dist_show_phases = auto_default_threshold
|
|
506
|
+
show_hull_faces = DEFAULTS.convex_hull.ternary.show_hull_faces
|
|
507
|
+
hull_face_color = DEFAULTS.convex_hull.ternary.hull_face_color
|
|
508
|
+
hull_face_opacity = DEFAULTS.convex_hull.ternary.hull_face_opacity
|
|
300
509
|
hull_face_color_mode = DEFAULTS.convex_hull.ternary
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
510
|
+
.hull_face_color_mode as HullFaceColorMode
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const handle_keydown = (event: KeyboardEvent) => {
|
|
514
|
+
const target = event.target as HTMLElement
|
|
515
|
+
if (target.tagName.match(/INPUT|TEXTAREA/)) return
|
|
516
|
+
|
|
307
517
|
// Stop propagation if event came from canvas to prevent wrapper's handler
|
|
308
518
|
// from running again (both have onkeydown, causing duplicate handling)
|
|
309
519
|
if (target === canvas) {
|
|
310
|
-
|
|
520
|
+
event.stopPropagation()
|
|
311
521
|
}
|
|
522
|
+
|
|
312
523
|
if (event.key === `Escape` && modal_open) {
|
|
313
|
-
|
|
314
|
-
|
|
524
|
+
close_structure_popup()
|
|
525
|
+
return
|
|
315
526
|
}
|
|
527
|
+
|
|
316
528
|
// Handle Enter for keyboard accessibility - select hovered entry
|
|
317
529
|
if (event.key === `Enter`) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
330
|
-
}
|
|
530
|
+
const entry = hover_data?.entry
|
|
531
|
+
if (entry) {
|
|
532
|
+
on_point_click?.(entry)
|
|
533
|
+
if (enable_click_selection) {
|
|
534
|
+
selected_entry = entry
|
|
535
|
+
if (enable_structure_preview) {
|
|
536
|
+
const structure = extract_structure_from_entry(entry)
|
|
537
|
+
if (structure) {
|
|
538
|
+
selected_structure = structure
|
|
539
|
+
modal_place_right = helpers.calculate_modal_side(wrapper)
|
|
540
|
+
modal_open = true
|
|
331
541
|
}
|
|
542
|
+
}
|
|
332
543
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
544
|
+
} else if (modal_open) {
|
|
545
|
+
close_structure_popup()
|
|
546
|
+
}
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const actions: Record<string, () => void> = {
|
|
551
|
+
r: reset_camera,
|
|
552
|
+
t: () => {
|
|
553
|
+
camera.elevation = 0
|
|
554
|
+
camera.azimuth = 0
|
|
555
|
+
center_camera(0)
|
|
556
|
+
},
|
|
557
|
+
b: () => color_mode = color_mode === `stability` ? `energy` : `stability`,
|
|
558
|
+
s: () => show_stable = !show_stable,
|
|
559
|
+
u: () => show_unstable = !show_unstable,
|
|
560
|
+
h: () => show_hull_faces = !show_hull_faces,
|
|
561
|
+
l: () => show_stable_labels = !show_stable_labels,
|
|
337
562
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
actions[event.key.toLowerCase()]?.();
|
|
352
|
-
};
|
|
353
|
-
async function handle_file_drop(event) {
|
|
354
|
-
drag_over = false;
|
|
355
|
-
const data = await helpers.parse_hull_entries_from_drop(event);
|
|
356
|
-
if (data)
|
|
357
|
-
on_file_drop?.(data);
|
|
358
|
-
}
|
|
359
|
-
async function copy_entry_data(entry, position) {
|
|
563
|
+
actions[event.key.toLowerCase()]?.()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
async function handle_file_drop(event: DragEvent): Promise<void> {
|
|
567
|
+
drag_over = false
|
|
568
|
+
const data = await helpers.parse_hull_entries_from_drop(event)
|
|
569
|
+
if (data) on_file_drop?.(data)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async function copy_entry_data(
|
|
573
|
+
entry: ConvexHullEntry,
|
|
574
|
+
position: { x: number; y: number },
|
|
575
|
+
) {
|
|
360
576
|
await helpers.copy_entry_to_clipboard(entry, position, (visible, pos) => {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
})
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
577
|
+
copy_feedback.visible = visible
|
|
578
|
+
copy_feedback.position = pos
|
|
579
|
+
})
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const get_point_color = (entry: ConvexHullEntry): string =>
|
|
583
|
+
helpers.get_point_color_for_entry(
|
|
584
|
+
entry,
|
|
585
|
+
color_mode,
|
|
586
|
+
merged_config.colors,
|
|
587
|
+
energy_color_scale,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
// Cache energy color scale per frame/setting
|
|
591
|
+
const energy_color_scale = $derived.by(() =>
|
|
592
|
+
helpers.get_energy_color_scale(color_mode, color_scale, plot_entries)
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
// Convex hull statistics - compute internally and expose via bindable prop
|
|
596
|
+
$effect(() => {
|
|
597
|
+
phase_stats = thermo.get_convex_hull_stats(plot_entries, elements, 3)
|
|
598
|
+
})
|
|
599
|
+
|
|
600
|
+
// 3D to 2D projection for ternary diagrams
|
|
601
|
+
function project_3d_point(
|
|
602
|
+
x: number,
|
|
603
|
+
y: number,
|
|
604
|
+
z: number,
|
|
605
|
+
): { x: number; y: number; depth: number } {
|
|
606
|
+
if (!canvas) return { x: 0, y: 0, depth: 0 }
|
|
607
|
+
|
|
376
608
|
const [elev, azim] = [
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
]
|
|
609
|
+
(camera.elevation * Math.PI) / 180,
|
|
610
|
+
(camera.azimuth * Math.PI) / 180,
|
|
611
|
+
]
|
|
380
612
|
const [cos_az, sin_az, cos_el, sin_el] = [
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
]
|
|
386
|
-
const centroid = get_triangle_centroid()
|
|
387
|
-
const { center: e_ctr, z_scale } = energy_range
|
|
388
|
-
|
|
389
|
-
const [
|
|
390
|
-
const [
|
|
613
|
+
Math.cos(azim),
|
|
614
|
+
Math.sin(azim),
|
|
615
|
+
Math.cos(-elev),
|
|
616
|
+
Math.sin(-elev),
|
|
617
|
+
]
|
|
618
|
+
const centroid = get_triangle_centroid()
|
|
619
|
+
const { center: e_ctr, z_scale } = energy_range
|
|
620
|
+
|
|
621
|
+
const [dx, dy, dz] = [x - centroid.x, y - centroid.y, (z - e_ctr) * z_scale]
|
|
622
|
+
const [x1, y1] = [dx * cos_az - dy * sin_az, dx * sin_az + dy * cos_az]
|
|
623
|
+
const [y2, z2] = [y1 * cos_el - dz * sin_el, y1 * sin_el + dz * cos_el]
|
|
624
|
+
|
|
391
625
|
// Use Math.min for consistent scaling with cached canvas dimensions
|
|
392
|
-
const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
|
|
626
|
+
const scale = Math.min(canvas_dims.width, canvas_dims.height) * 0.6 * camera.zoom
|
|
393
627
|
return {
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
628
|
+
x: canvas_dims.width / 2 + camera.center_x + x1 * scale,
|
|
629
|
+
y: canvas_dims.height / 2 + camera.center_y - y2 * scale,
|
|
630
|
+
depth: z2,
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function draw_structure_outline(): void {
|
|
635
|
+
if (!ctx) return
|
|
636
|
+
|
|
402
637
|
// Set consistent style for all triangle structure lines
|
|
403
|
-
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
404
|
-
ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
|
|
405
|
-
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
|
|
638
|
+
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
639
|
+
ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
|
|
640
|
+
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash) // Dashed lines for all structure lines
|
|
641
|
+
|
|
406
642
|
// Draw triangle base and vertical edges
|
|
407
|
-
draw_triangle_structure()
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
643
|
+
draw_triangle_structure()
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function draw_triangle_structure(): void {
|
|
647
|
+
if (!ctx) return
|
|
648
|
+
|
|
412
649
|
// Get formation energy range for vertical edges
|
|
413
|
-
const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
|
|
414
|
-
const e_form_min = Math.min(0, ...formation_energies)
|
|
415
|
-
const e_form_max = Math.max(0, ...formation_energies)
|
|
650
|
+
const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
|
|
651
|
+
const e_form_min = Math.min(0, ...formation_energies) // Include 0 for elemental references
|
|
652
|
+
const e_form_max = Math.max(0, ...formation_energies) // Include 0 for elemental references
|
|
653
|
+
|
|
416
654
|
// Draw base triangle edges (top triangle at formation energy = 0)
|
|
417
|
-
const triangle_edges = get_triangle_edges()
|
|
418
|
-
ctx.beginPath()
|
|
655
|
+
const triangle_edges = get_triangle_edges()
|
|
656
|
+
ctx.beginPath()
|
|
419
657
|
for (const [v1, v2] of triangle_edges) {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
658
|
+
const proj1 = project_3d_point(v1.x, v1.y, 0) // Base triangle at formation energy = 0
|
|
659
|
+
const proj2 = project_3d_point(v2.x, v2.y, 0)
|
|
660
|
+
|
|
661
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
662
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
424
663
|
}
|
|
425
|
-
ctx.stroke()
|
|
664
|
+
ctx.stroke()
|
|
665
|
+
|
|
426
666
|
// Draw vertical edges from corners (from most negative to 0 formation energy)
|
|
427
|
-
const vertical_edges = get_triangle_vertical_edges(
|
|
428
|
-
|
|
667
|
+
const vertical_edges = get_triangle_vertical_edges(
|
|
668
|
+
e_form_min,
|
|
669
|
+
e_form_max,
|
|
670
|
+
)
|
|
671
|
+
ctx.beginPath()
|
|
429
672
|
for (const [v1, v2] of vertical_edges) {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
673
|
+
const proj1 = project_3d_point(v1.x, v1.y, v1.z)
|
|
674
|
+
const proj2 = project_3d_point(v2.x, v2.y, v2.z)
|
|
675
|
+
|
|
676
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
677
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
434
678
|
}
|
|
435
|
-
ctx.stroke()
|
|
679
|
+
ctx.stroke()
|
|
680
|
+
|
|
436
681
|
// Draw bottom triangle (connecting the bottom tips of vertical lines)
|
|
437
|
-
const bottom_triangle_edges = get_triangle_edges()
|
|
438
|
-
ctx.beginPath()
|
|
682
|
+
const bottom_triangle_edges = get_triangle_edges()
|
|
683
|
+
ctx.beginPath()
|
|
439
684
|
for (const [v1, v2] of bottom_triangle_edges) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
685
|
+
const proj1 = project_3d_point(v1.x, v1.y, e_form_min) // Bottom triangle at most negative energy
|
|
686
|
+
const proj2 = project_3d_point(v2.x, v2.y, e_form_min)
|
|
687
|
+
|
|
688
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
689
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
444
690
|
}
|
|
445
|
-
ctx.stroke()
|
|
691
|
+
ctx.stroke()
|
|
692
|
+
|
|
446
693
|
// Reset stroke style to default for other elements
|
|
447
|
-
const styles = getComputedStyle(canvas)
|
|
448
|
-
ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121
|
|
449
|
-
ctx.setLineDash([])
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
694
|
+
const styles = getComputedStyle(canvas)
|
|
695
|
+
ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121`
|
|
696
|
+
ctx.setLineDash([]) // Reset line dash for other drawing operations
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function draw_element_labels(): void {
|
|
700
|
+
if (!ctx || elements.length !== 3) return
|
|
701
|
+
|
|
702
|
+
ctx.save()
|
|
703
|
+
|
|
455
704
|
// Draw element labels outside triangle corners
|
|
456
|
-
const centroid = get_triangle_centroid()
|
|
457
|
-
ctx.fillStyle = text_color
|
|
458
|
-
ctx.font = `bold 16px Arial
|
|
459
|
-
ctx.textAlign = `center
|
|
460
|
-
ctx.textBaseline = `middle
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
705
|
+
const centroid = get_triangle_centroid()
|
|
706
|
+
ctx.fillStyle = text_color
|
|
707
|
+
ctx.font = `bold 16px Arial`
|
|
708
|
+
ctx.textAlign = `center`
|
|
709
|
+
ctx.textBaseline = `middle`
|
|
710
|
+
|
|
711
|
+
for (
|
|
712
|
+
let idx = 0;
|
|
713
|
+
idx < TRIANGLE_VERTICES.length && idx < elements.length;
|
|
714
|
+
idx++
|
|
715
|
+
) {
|
|
716
|
+
const [x, y] = TRIANGLE_VERTICES[idx]
|
|
717
|
+
const dx = x - centroid.x
|
|
718
|
+
const dy = y - centroid.y
|
|
719
|
+
const length = Math.sqrt(dx * dx + dy * dy)
|
|
720
|
+
const distance = 0.05
|
|
721
|
+
|
|
722
|
+
const label_pos = {
|
|
723
|
+
x: x + (dx / length) * distance,
|
|
724
|
+
y: y + (dy / length) * distance,
|
|
725
|
+
z: 0,
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const proj = project_3d_point(label_pos.x, label_pos.y, label_pos.z)
|
|
729
|
+
ctx.fillText(elements[idx], proj.x, proj.y)
|
|
474
730
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
731
|
+
|
|
732
|
+
ctx.restore()
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function draw_z_axis_ticks(): void {
|
|
736
|
+
if (!ctx || elements.length !== 3) return
|
|
480
737
|
// Hide z-axis in near-top-down views where ticks collapse to a point
|
|
481
|
-
if (Math.abs(camera.elevation) < MIN_ELEV_FOR_Z_AXIS)
|
|
482
|
-
|
|
483
|
-
const { min: e_min, max: e_max, center: e_mid } = energy_range
|
|
484
|
-
if (Math.abs(e_max - e_min) < 1e-6)
|
|
485
|
-
|
|
738
|
+
if (Math.abs(camera.elevation) < MIN_ELEV_FOR_Z_AXIS) return
|
|
739
|
+
|
|
740
|
+
const { min: e_min, max: e_max, center: e_mid } = energy_range
|
|
741
|
+
if (Math.abs(e_max - e_min) < 1e-6) return
|
|
742
|
+
|
|
486
743
|
// Find the vertex that projects to the leftmost x-position (changes with rotation)
|
|
487
|
-
const projected_vertices = TRIANGLE_VERTICES.map(([vx, vy]) =>
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
744
|
+
const projected_vertices = TRIANGLE_VERTICES.map(([vx, vy]) =>
|
|
745
|
+
project_3d_point(vx, vy, e_mid)
|
|
746
|
+
)
|
|
747
|
+
const leftmost_idx = projected_vertices.reduce(
|
|
748
|
+
(
|
|
749
|
+
min_idx,
|
|
750
|
+
proj,
|
|
751
|
+
idx,
|
|
752
|
+
) => (proj.x < projected_vertices[min_idx].x ? idx : min_idx),
|
|
753
|
+
0,
|
|
754
|
+
)
|
|
755
|
+
const [axis_x, axis_y] = TRIANGLE_VERTICES[leftmost_idx]
|
|
756
|
+
const tick_len = 6 * canvas_dims.scale
|
|
757
|
+
|
|
758
|
+
ctx.save()
|
|
759
|
+
ctx.fillStyle = text_color
|
|
760
|
+
ctx.textAlign = `right`
|
|
761
|
+
ctx.textBaseline = `middle`
|
|
762
|
+
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
763
|
+
ctx.font = `${merged_config.font_size}px Arial`
|
|
764
|
+
|
|
497
765
|
for (const tick of ticks(e_min, e_max, 5)) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
766
|
+
const { x, y } = project_3d_point(axis_x, axis_y, tick)
|
|
767
|
+
ctx.beginPath()
|
|
768
|
+
ctx.moveTo(x - tick_len, y)
|
|
769
|
+
ctx.lineTo(x, y)
|
|
770
|
+
ctx.stroke()
|
|
771
|
+
ctx.fillText(format_num(tick, `.2~`), x - tick_len - 4, y)
|
|
504
772
|
}
|
|
773
|
+
|
|
505
774
|
// Rotated axis label: Eform (eV/atom) with "form" as subscript
|
|
506
|
-
const { x: lx, y: ly } = project_3d_point(axis_x, axis_y, e_mid)
|
|
507
|
-
const fs = merged_config.font_size ?? 12
|
|
508
|
-
const sub_fs = Math.round(fs * 0.75)
|
|
509
|
-
ctx.translate(lx - 50 * canvas_dims.scale, ly)
|
|
510
|
-
ctx.rotate(-Math.PI / 2)
|
|
511
|
-
ctx.textAlign = `left
|
|
775
|
+
const { x: lx, y: ly } = project_3d_point(axis_x, axis_y, e_mid)
|
|
776
|
+
const fs = merged_config.font_size ?? 12
|
|
777
|
+
const sub_fs = Math.round(fs * 0.75)
|
|
778
|
+
ctx.translate(lx - 50 * canvas_dims.scale, ly)
|
|
779
|
+
ctx.rotate(-Math.PI / 2)
|
|
780
|
+
ctx.textAlign = `left`
|
|
512
781
|
// Measure widths in each font, then draw — reordered to minimize font switches
|
|
513
|
-
ctx.font = `bold ${fs}px Arial
|
|
514
|
-
const e_width = ctx.measureText(`E`).width
|
|
515
|
-
const suffix_width = ctx.measureText(` (eV/atom)`).width
|
|
516
|
-
ctx.font = `${sub_fs}px Arial
|
|
517
|
-
const form_width = ctx.measureText(`form`).width
|
|
518
|
-
const offset = -(e_width + form_width + suffix_width) / 2
|
|
782
|
+
ctx.font = `bold ${fs}px Arial`
|
|
783
|
+
const e_width = ctx.measureText(`E`).width
|
|
784
|
+
const suffix_width = ctx.measureText(` (eV/atom)`).width
|
|
785
|
+
ctx.font = `${sub_fs}px Arial`
|
|
786
|
+
const form_width = ctx.measureText(`form`).width
|
|
787
|
+
const offset = -(e_width + form_width + suffix_width) / 2
|
|
519
788
|
// Draw subscript while sub-font is still active
|
|
520
|
-
ctx.fillText(`form`, offset + e_width, fs * 0.3)
|
|
521
|
-
ctx.font = `bold ${fs}px Arial
|
|
522
|
-
ctx.fillText(`E`, offset, 0)
|
|
523
|
-
ctx.fillText(` (eV/atom)`, offset + e_width + form_width, 0)
|
|
524
|
-
ctx.restore()
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
789
|
+
ctx.fillText(`form`, offset + e_width, fs * 0.3)
|
|
790
|
+
ctx.font = `bold ${fs}px Arial`
|
|
791
|
+
ctx.fillText(`E`, offset, 0)
|
|
792
|
+
ctx.fillText(` (eV/atom)`, offset + e_width + form_width, 0)
|
|
793
|
+
ctx.restore()
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
function draw_convex_hull_faces(): void {
|
|
797
|
+
if (!ctx || !show_hull_faces || hull_faces.length === 0) return
|
|
798
|
+
|
|
529
799
|
// Lazy computation for uniform mode: normalize alpha by formation energy
|
|
530
|
-
let norm_alpha = null
|
|
800
|
+
let norm_alpha: ((z: number) => number) | null = null
|
|
531
801
|
if (hull_face_color_mode === `uniform`) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
802
|
+
const formation_energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
|
|
803
|
+
const min_fe = Math.min(0, ...formation_energies)
|
|
804
|
+
norm_alpha = (z: number) => {
|
|
805
|
+
const t = Math.max(0, Math.min(1, (0 - z) / Math.max(1e-6, 0 - min_fe)))
|
|
806
|
+
return t * hull_face_opacity
|
|
807
|
+
}
|
|
538
808
|
}
|
|
809
|
+
|
|
539
810
|
// Lazy computation for formation_energy mode
|
|
540
|
-
let energy_face_scale = null
|
|
541
|
-
let min_z = 0
|
|
811
|
+
let energy_face_scale: ((val: number) => string) | null = null
|
|
812
|
+
let min_z = 0
|
|
542
813
|
if (hull_face_color_mode === `formation_energy`) {
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
814
|
+
const all_z = hull_faces.flatMap((tri) => tri.vertices.map((v) => v.z))
|
|
815
|
+
min_z = Math.min(...all_z)
|
|
816
|
+
energy_face_scale = helpers.get_energy_color_scale(
|
|
817
|
+
`energy`,
|
|
818
|
+
color_scale,
|
|
819
|
+
all_z.map((z) => ({ e_above_hull: z - min_z })), // Normalize to 0-based
|
|
820
|
+
)
|
|
546
821
|
}
|
|
822
|
+
|
|
547
823
|
// Helper to get face color based on mode
|
|
548
|
-
const get_face_color = (
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
return
|
|
567
|
-
|
|
824
|
+
const get_face_color = (
|
|
825
|
+
tri: typeof hull_faces[0],
|
|
826
|
+
tri_idx: number,
|
|
827
|
+
): string => {
|
|
828
|
+
if (hull_face_color_mode === `uniform`) {
|
|
829
|
+
return hull_face_color
|
|
830
|
+
}
|
|
831
|
+
if (hull_face_color_mode === `formation_energy`) {
|
|
832
|
+
const avg_z = (tri.vertices[0].z + tri.vertices[1].z + tri.vertices[2].z) / 3
|
|
833
|
+
return energy_face_scale?.(avg_z - min_z) ?? hull_face_color
|
|
834
|
+
}
|
|
835
|
+
if (hull_face_color_mode === `dominant_element`) {
|
|
836
|
+
// Find element vertex closest to face centroid in 2D ternary space
|
|
837
|
+
const { x: cx, y: cy } = tri.centroid
|
|
838
|
+
const dists = TRIANGLE_VERTICES.map(([tx, ty]) =>
|
|
839
|
+
Math.hypot(cx - tx, cy - ty)
|
|
840
|
+
)
|
|
841
|
+
const el = elements[dists.indexOf(Math.min(...dists))]
|
|
842
|
+
return element_colors[el] ?? `#888888`
|
|
843
|
+
}
|
|
844
|
+
if (hull_face_color_mode === `facet_index`) {
|
|
845
|
+
return PLOT_COLORS[tri_idx % PLOT_COLORS.length]
|
|
846
|
+
}
|
|
847
|
+
return hull_face_color
|
|
848
|
+
}
|
|
849
|
+
|
|
568
850
|
// Sort faces by depth for proper rendering
|
|
569
851
|
const faces_with_depth = hull_faces.map((tri, tri_idx) => {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
852
|
+
const centroid_proj = project_3d_point(
|
|
853
|
+
tri.centroid.x,
|
|
854
|
+
tri.centroid.y,
|
|
855
|
+
tri.centroid.z,
|
|
856
|
+
)
|
|
857
|
+
return { tri, tri_idx, depth: centroid_proj.depth }
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
faces_with_depth.sort((a, b) => a.depth - b.depth) // Back to front
|
|
861
|
+
|
|
574
862
|
// Draw each face (lower hull only)
|
|
575
863
|
for (const { tri, tri_idx } of faces_with_depth) {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
draw_tri(add_alpha(face_color, avg_alpha), avg_alpha * 3);
|
|
622
|
-
}
|
|
623
|
-
else {
|
|
624
|
-
const vx = coef_a / mag;
|
|
625
|
-
const vy = coef_b / mag;
|
|
626
|
-
const cx = (x1 + x2 + x3) / 3;
|
|
627
|
-
const cy = (y1 + y2 + y3) / 3;
|
|
628
|
-
const alpha_c = coef_a * cx + coef_b * cy + coef_c;
|
|
629
|
-
const alpha_min = Math.min(a1, a2, a3);
|
|
630
|
-
const alpha_max = Math.max(a1, a2, a3);
|
|
631
|
-
const s_min = (alpha_min - alpha_c) / mag;
|
|
632
|
-
const s_max = (alpha_max - alpha_c) / mag;
|
|
633
|
-
const grad = ctx.createLinearGradient(cx + vx * s_min, cy + vy * s_min, cx + vx * s_max, cy + vy * s_max);
|
|
634
|
-
grad.addColorStop(0, add_alpha(face_color, alpha_min));
|
|
635
|
-
grad.addColorStop(1, add_alpha(face_color, alpha_max));
|
|
636
|
-
draw_tri(grad, alpha_max * 3);
|
|
637
|
-
}
|
|
864
|
+
const [p1, p2, p3] = tri.vertices
|
|
865
|
+
|
|
866
|
+
const proj1 = project_3d_point(p1.x, p1.y, p1.z)
|
|
867
|
+
const proj2 = project_3d_point(p2.x, p2.y, p2.z)
|
|
868
|
+
const proj3 = project_3d_point(p3.x, p3.y, p3.z)
|
|
869
|
+
|
|
870
|
+
const face_color = get_face_color(tri, tri_idx)
|
|
871
|
+
|
|
872
|
+
// For uniform mode, use gradient with variable opacity
|
|
873
|
+
// For other modes, use fixed opacity
|
|
874
|
+
if (hull_face_color_mode === `uniform`) {
|
|
875
|
+
// Build per-face linear gradient in screen space matching linear variation of formation energy
|
|
876
|
+
const a1 = norm_alpha?.(p1.z) ?? 0
|
|
877
|
+
const a2 = norm_alpha?.(p2.z) ?? 0
|
|
878
|
+
const a3 = norm_alpha?.(p3.z) ?? 0
|
|
879
|
+
|
|
880
|
+
// Solve a*x + b*y + c = alpha at the three projected vertices
|
|
881
|
+
const x1 = proj1.x, y1 = proj1.y
|
|
882
|
+
const x2 = proj2.x, y2 = proj2.y
|
|
883
|
+
const x3 = proj3.x, y3 = proj3.y
|
|
884
|
+
const det = x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2)
|
|
885
|
+
let coef_a = 0, coef_b = 0, coef_c = (a1 + a2 + a3) / 3
|
|
886
|
+
if (Math.abs(det) > 1e-9) {
|
|
887
|
+
coef_a = (a1 * (y2 - y3) + a2 * (y3 - y1) + a3 * (y1 - y2)) / det
|
|
888
|
+
coef_b = (a1 * (x3 - x2) + a2 * (x1 - x3) + a3 * (x2 - x1)) / det
|
|
889
|
+
coef_c = (a1 * (x2 * y3 - x3 * y2) + a2 * (x3 * y1 - x1 * y3) +
|
|
890
|
+
a3 * (x1 * y2 - x2 * y1)) /
|
|
891
|
+
det
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// Helper to draw filled+stroked triangle
|
|
895
|
+
const draw_tri = (fill: string | CanvasGradient, stroke_alpha: number) => {
|
|
896
|
+
if (!ctx) return
|
|
897
|
+
ctx.save()
|
|
898
|
+
ctx.beginPath()
|
|
899
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
900
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
901
|
+
ctx.lineTo(proj3.x, proj3.y)
|
|
902
|
+
ctx.closePath()
|
|
903
|
+
ctx.fillStyle = fill
|
|
904
|
+
ctx.fill()
|
|
905
|
+
ctx.strokeStyle = add_alpha(face_color, Math.min(0.6, stroke_alpha))
|
|
906
|
+
ctx.lineWidth = 1
|
|
907
|
+
ctx.stroke()
|
|
908
|
+
ctx.restore()
|
|
638
909
|
}
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
910
|
+
|
|
911
|
+
// Gradient direction is the screen-space gradient of alpha
|
|
912
|
+
const mag = Math.hypot(coef_a, coef_b)
|
|
913
|
+
if (mag < 1e-9) {
|
|
914
|
+
// Fallback: uniform fill if nearly flat
|
|
915
|
+
const avg_alpha = (a1 + a2 + a3) / 3
|
|
916
|
+
draw_tri(add_alpha(face_color, avg_alpha), avg_alpha * 3)
|
|
917
|
+
} else {
|
|
918
|
+
const vx = coef_a / mag
|
|
919
|
+
const vy = coef_b / mag
|
|
920
|
+
const cx = (x1 + x2 + x3) / 3
|
|
921
|
+
const cy = (y1 + y2 + y3) / 3
|
|
922
|
+
const alpha_c = coef_a * cx + coef_b * cy + coef_c
|
|
923
|
+
const alpha_min = Math.min(a1, a2, a3)
|
|
924
|
+
const alpha_max = Math.max(a1, a2, a3)
|
|
925
|
+
const s_min = (alpha_min - alpha_c) / mag
|
|
926
|
+
const s_max = (alpha_max - alpha_c) / mag
|
|
927
|
+
|
|
928
|
+
const grad = ctx.createLinearGradient(
|
|
929
|
+
cx + vx * s_min,
|
|
930
|
+
cy + vy * s_min,
|
|
931
|
+
cx + vx * s_max,
|
|
932
|
+
cy + vy * s_max,
|
|
933
|
+
)
|
|
934
|
+
grad.addColorStop(0, add_alpha(face_color, alpha_min))
|
|
935
|
+
grad.addColorStop(1, add_alpha(face_color, alpha_max))
|
|
936
|
+
draw_tri(grad, alpha_max * 3)
|
|
653
937
|
}
|
|
938
|
+
} else {
|
|
939
|
+
// Non-uniform modes: solid color with fixed opacity
|
|
940
|
+
ctx.save()
|
|
941
|
+
ctx.beginPath()
|
|
942
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
943
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
944
|
+
ctx.lineTo(proj3.x, proj3.y)
|
|
945
|
+
ctx.closePath()
|
|
946
|
+
ctx.fillStyle = add_alpha(face_color, hull_face_opacity)
|
|
947
|
+
ctx.fill()
|
|
948
|
+
ctx.strokeStyle = add_alpha(face_color, Math.min(0.6, hull_face_opacity * 3))
|
|
949
|
+
ctx.lineWidth = 1
|
|
950
|
+
ctx.stroke()
|
|
951
|
+
ctx.restore()
|
|
952
|
+
}
|
|
654
953
|
}
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Formation energy color bar helpers
|
|
957
|
+
const e_form_range = $derived.by((): [number, number] => {
|
|
958
|
+
const energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
|
|
959
|
+
const min_fe = energies.length ? Math.min(0, ...energies) : -1
|
|
960
|
+
return [min_fe, 0]
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
const e_form_color_scale_fn = $derived.by(() => {
|
|
964
|
+
const [min_fe, max_fe] = e_form_range
|
|
965
|
+
const denom = Math.max(1e-6, max_fe - min_fe)
|
|
966
|
+
return (value: number) => {
|
|
967
|
+
// alpha 0 at 0 eV, goes to hull_face_opacity at most negative energy
|
|
968
|
+
const t = Math.max(0, Math.min(1, (value - min_fe) / denom))
|
|
969
|
+
const alpha = (1 - t) * hull_face_opacity
|
|
970
|
+
return add_alpha(hull_face_color, alpha)
|
|
971
|
+
}
|
|
972
|
+
})
|
|
973
|
+
|
|
974
|
+
function draw_data_points(): void {
|
|
975
|
+
if (!ctx || sorted_points_cache.length === 0) return
|
|
976
|
+
|
|
675
977
|
for (const { entry, projected } of sorted_points_cache) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
978
|
+
const is_stable = entry.is_stable || entry.e_above_hull === 0
|
|
979
|
+
const is_entry_highlighted = is_highlighted(entry)
|
|
980
|
+
const color = get_point_color(entry)
|
|
981
|
+
const size = (entry.size || (is_stable ? 6 : 4)) * canvas_dims.scale
|
|
982
|
+
const marker = entry.marker || `circle`
|
|
983
|
+
|
|
984
|
+
// Shadow
|
|
985
|
+
const shadow_offset = Math.abs(entry.z) * 0.1 * canvas_dims.scale
|
|
986
|
+
ctx.fillStyle = `rgba(0, 0, 0, 0.2)`
|
|
987
|
+
const shadow_path = helpers.create_marker_path(size * 0.8, marker)
|
|
988
|
+
ctx.save()
|
|
989
|
+
ctx.translate(projected.x + shadow_offset, projected.y + shadow_offset)
|
|
990
|
+
ctx.fill(shadow_path)
|
|
991
|
+
ctx.restore()
|
|
992
|
+
|
|
993
|
+
// Highlights
|
|
994
|
+
if (selected_entry && entry.entry_id === selected_entry.entry_id) {
|
|
995
|
+
helpers.draw_selection_highlight(
|
|
996
|
+
ctx,
|
|
997
|
+
projected,
|
|
998
|
+
size,
|
|
999
|
+
canvas_dims.scale,
|
|
1000
|
+
pulse_time,
|
|
1001
|
+
pulse_opacity,
|
|
1002
|
+
)
|
|
1003
|
+
}
|
|
1004
|
+
if (is_entry_highlighted) {
|
|
1005
|
+
helpers.draw_highlight_effect(
|
|
1006
|
+
ctx,
|
|
1007
|
+
projected,
|
|
1008
|
+
size,
|
|
1009
|
+
canvas_dims.scale,
|
|
1010
|
+
pulse_time,
|
|
1011
|
+
merged_highlight_style,
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
// Main point with marker symbol
|
|
1016
|
+
ctx.fillStyle =
|
|
1017
|
+
is_entry_highlighted && merged_highlight_style.effect === `color`
|
|
1018
|
+
? merged_highlight_style.color
|
|
1019
|
+
: color
|
|
1020
|
+
ctx.strokeStyle = is_stable ? `#ffffff` : `#000000`
|
|
1021
|
+
ctx.lineWidth = 0.5 * canvas_dims.scale
|
|
1022
|
+
const marker_path = helpers.create_marker_path(size, marker)
|
|
1023
|
+
ctx.save()
|
|
1024
|
+
ctx.translate(projected.x, projected.y)
|
|
1025
|
+
ctx.fill(marker_path)
|
|
1026
|
+
ctx.stroke(marker_path)
|
|
1027
|
+
ctx.restore()
|
|
709
1028
|
}
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
const hull_label_font_size = 12
|
|
1032
|
+
const hull_label_subscript_font_size = 11
|
|
1033
|
+
const hull_label_font = `${hull_label_font_size}px Arial`
|
|
1034
|
+
const hull_label_subscript_font = `${hull_label_subscript_font_size}px Arial`
|
|
1035
|
+
|
|
1036
|
+
function label_priority_energy(entry: ConvexHullEntry): number {
|
|
1037
|
+
for (const value of [
|
|
1038
|
+
entry.e_form_per_atom,
|
|
1039
|
+
entry.z,
|
|
1040
|
+
entry.energy_per_atom,
|
|
1041
|
+
entry.energy,
|
|
1042
|
+
entry.e_above_hull,
|
|
1043
|
+
]) {
|
|
1044
|
+
if (typeof value === `number` && Number.isFinite(value)) return value
|
|
1045
|
+
}
|
|
1046
|
+
return Number.POSITIVE_INFINITY
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function get_label_placements(
|
|
1050
|
+
projected: { x: number; y: number },
|
|
1051
|
+
point_size: number,
|
|
1052
|
+
text_width: number,
|
|
1053
|
+
text_height: number,
|
|
1054
|
+
): LabelPlacement[] {
|
|
1055
|
+
const padding = Math.max(1, 2 * canvas_dims.scale)
|
|
1056
|
+
const gap = point_size + 4 * canvas_dims.scale
|
|
1057
|
+
const side_gap = point_size + 5 * canvas_dims.scale
|
|
1058
|
+
const placements = [
|
|
1059
|
+
{ x: projected.x, y: projected.y + gap },
|
|
1060
|
+
{ x: projected.x, y: projected.y - gap - text_height },
|
|
1061
|
+
{ x: projected.x + side_gap + text_width / 2, y: projected.y - text_height / 2 },
|
|
1062
|
+
{ x: projected.x - side_gap - text_width / 2, y: projected.y - text_height / 2 },
|
|
1063
|
+
{ x: projected.x + side_gap + text_width / 2, y: projected.y + gap },
|
|
1064
|
+
{ x: projected.x - side_gap - text_width / 2, y: projected.y + gap },
|
|
1065
|
+
{ x: projected.x + side_gap + text_width / 2, y: projected.y - gap - text_height },
|
|
1066
|
+
{ x: projected.x - side_gap - text_width / 2, y: projected.y - gap - text_height },
|
|
1067
|
+
]
|
|
1068
|
+
|
|
1069
|
+
return placements.map((placement) => ({
|
|
1070
|
+
...placement,
|
|
1071
|
+
rect: pad_rect(
|
|
1072
|
+
centered_rect(placement.x, placement.y, text_width, text_height),
|
|
1073
|
+
padding,
|
|
1074
|
+
),
|
|
1075
|
+
}))
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function measure_formula_segments(
|
|
1079
|
+
context: CanvasRenderingContext2D,
|
|
1080
|
+
segments: FormulaLabelSegment[],
|
|
1081
|
+
): number {
|
|
1082
|
+
context.save()
|
|
1083
|
+
const width = segments.reduce((sum, segment) => {
|
|
1084
|
+
context.font = segment.subscript ? hull_label_subscript_font : hull_label_font
|
|
1085
|
+
return sum + context.measureText(segment.text).width
|
|
1086
|
+
}, 0)
|
|
1087
|
+
context.restore()
|
|
1088
|
+
return width
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function draw_formula_segments(
|
|
1092
|
+
context: CanvasRenderingContext2D,
|
|
1093
|
+
segments: FormulaLabelSegment[],
|
|
1094
|
+
center_x: number,
|
|
1095
|
+
top_y: number,
|
|
1096
|
+
text_width: number,
|
|
1097
|
+
): void {
|
|
1098
|
+
const subscript_offset = hull_label_font_size * 0.28
|
|
1099
|
+
let text_x = center_x - text_width / 2
|
|
1100
|
+
|
|
1101
|
+
context.save()
|
|
1102
|
+
context.textAlign = `left`
|
|
1103
|
+
context.textBaseline = `top`
|
|
1104
|
+
for (const segment of segments) {
|
|
1105
|
+
context.font = segment.subscript ? hull_label_subscript_font : hull_label_font
|
|
1106
|
+
context.fillText(
|
|
1107
|
+
segment.text,
|
|
1108
|
+
text_x,
|
|
1109
|
+
top_y + (segment.subscript ? subscript_offset : 0),
|
|
1110
|
+
)
|
|
1111
|
+
text_x += context.measureText(segment.text).width
|
|
1112
|
+
}
|
|
1113
|
+
context.restore()
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function draw_hull_labels(): void {
|
|
1117
|
+
if (!ctx || !merged_config.show_labels) return
|
|
1118
|
+
|
|
1119
|
+
ctx.fillStyle = text_color
|
|
1120
|
+
ctx.font = hull_label_font
|
|
1121
|
+
ctx.textAlign = `center`
|
|
1122
|
+
ctx.textBaseline = `top`
|
|
1123
|
+
const label_height = hull_label_font_size + 2
|
|
1124
|
+
|
|
1125
|
+
const label_entries = helpers.get_composition_label_entries(
|
|
1126
|
+
plot_entries.filter((entry) => {
|
|
1127
|
+
if (!entry.visible || entry.is_element) return false
|
|
1128
|
+
const is_stable_point = entry.is_stable || (entry.e_above_hull ?? 0) <= 1e-6
|
|
1129
|
+
return (is_stable_point && show_stable_labels) ||
|
|
1130
|
+
(!is_stable_point && show_unstable_labels &&
|
|
1131
|
+
(entry.e_above_hull ?? 0) <= max_hull_dist_show_labels)
|
|
1132
|
+
}),
|
|
1133
|
+
)
|
|
1134
|
+
.sort((entry_1, entry_2) => {
|
|
1135
|
+
const energy_diff = label_priority_energy(entry_1) -
|
|
1136
|
+
label_priority_energy(entry_2)
|
|
1137
|
+
if (energy_diff !== 0) return energy_diff
|
|
1138
|
+
return (entry_1.e_above_hull ?? 0) - (entry_2.e_above_hull ?? 0)
|
|
1139
|
+
})
|
|
1140
|
+
|
|
1141
|
+
const occupied_rects: Rect[] = []
|
|
1142
|
+
const canvas_rect: Rect = {
|
|
1143
|
+
x: 0,
|
|
1144
|
+
y: 0,
|
|
1145
|
+
width: canvas_dims.width,
|
|
1146
|
+
height: canvas_dims.height,
|
|
727
1147
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
1148
|
+
for (const entry of label_entries) {
|
|
1149
|
+
const projected = project_3d_point(entry.x, entry.y, entry.z)
|
|
1150
|
+
const formula_segments = get_formula_label_segments(
|
|
1151
|
+
helpers.get_entry_label(entry, elements),
|
|
1152
|
+
)
|
|
1153
|
+
const is_stable_point = entry.is_stable || entry.e_above_hull === 0
|
|
1154
|
+
const point_size = (entry.size || (is_stable_point ? 6 : 4)) * canvas_dims.scale
|
|
1155
|
+
const text_width = measure_formula_segments(ctx, formula_segments)
|
|
1156
|
+
const placements = get_label_placements(
|
|
1157
|
+
projected,
|
|
1158
|
+
point_size,
|
|
1159
|
+
text_width,
|
|
1160
|
+
label_height,
|
|
1161
|
+
)
|
|
1162
|
+
const placement = placements.find((candidate) =>
|
|
1163
|
+
rect_within_rect(candidate.rect, canvas_rect) &&
|
|
1164
|
+
!occupied_rects.some((occupied_rect) =>
|
|
1165
|
+
rects_overlap(candidate.rect, occupied_rect)
|
|
1166
|
+
)
|
|
1167
|
+
)
|
|
1168
|
+
if (!placement) continue
|
|
1169
|
+
|
|
1170
|
+
occupied_rects.push(placement.rect)
|
|
1171
|
+
draw_formula_segments(ctx, formula_segments, placement.x, placement.y, text_width)
|
|
742
1172
|
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
function render_frame(): void {
|
|
1176
|
+
if (!ctx || !canvas) return
|
|
1177
|
+
|
|
747
1178
|
// Use CSS dimensions for rendering
|
|
748
|
-
const display_width = canvas.clientWidth || 600
|
|
749
|
-
const display_height = canvas.clientHeight || 600
|
|
1179
|
+
const display_width = canvas.clientWidth || 600
|
|
1180
|
+
const display_height = canvas.clientHeight || 600
|
|
1181
|
+
|
|
750
1182
|
// Clear canvas
|
|
751
|
-
ctx.clearRect(0, 0, display_width, display_height)
|
|
1183
|
+
ctx.clearRect(0, 0, display_width, display_height)
|
|
1184
|
+
|
|
752
1185
|
// Set background - use transparent to inherit from container
|
|
753
|
-
ctx.fillStyle = `transparent
|
|
754
|
-
ctx.fillRect(0, 0, display_width, display_height)
|
|
1186
|
+
ctx.fillStyle = `transparent`
|
|
1187
|
+
ctx.fillRect(0, 0, display_width, display_height)
|
|
1188
|
+
|
|
755
1189
|
if (elements.length !== 3) {
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
1190
|
+
if (elements.length > 0) {
|
|
1191
|
+
ctx.fillStyle = text_color
|
|
1192
|
+
ctx.font = `16px Arial`
|
|
1193
|
+
ctx.textAlign = `center`
|
|
1194
|
+
ctx.textBaseline = `middle`
|
|
1195
|
+
ctx.fillText(
|
|
1196
|
+
`Ternary convex hull requires exactly 3 elements (got ${elements.length})`,
|
|
1197
|
+
display_width / 2,
|
|
1198
|
+
display_height / 2,
|
|
1199
|
+
)
|
|
1200
|
+
}
|
|
1201
|
+
return
|
|
764
1202
|
}
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1203
|
+
|
|
1204
|
+
draw_structure_outline()
|
|
1205
|
+
draw_convex_hull_faces() // behind points
|
|
1206
|
+
draw_z_axis_ticks() // after faces for visibility at high opacity
|
|
1207
|
+
draw_data_points()
|
|
1208
|
+
draw_hull_labels()
|
|
1209
|
+
draw_element_labels()
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function handle_mouse_down(event: MouseEvent) {
|
|
1213
|
+
is_dragging = true
|
|
1214
|
+
drag_started = false
|
|
1215
|
+
hover_data = null
|
|
1216
|
+
on_point_hover?.(null)
|
|
1217
|
+
last_mouse = { x: event.clientX, y: event.clientY }
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const handle_mouse_move = (event: MouseEvent) => {
|
|
1221
|
+
if (!is_dragging) return
|
|
1222
|
+
const [dx, dy] = [event.clientX - last_mouse.x, event.clientY - last_mouse.y]
|
|
1223
|
+
|
|
783
1224
|
// Mark as drag if any movement occurred
|
|
784
|
-
if (dx !== 0 || dy !== 0)
|
|
785
|
-
|
|
1225
|
+
if (dx !== 0 || dy !== 0) drag_started = true
|
|
1226
|
+
|
|
786
1227
|
// With Cmd/Ctrl held: pan the view instead of rotating
|
|
787
1228
|
if (event.metaKey || event.ctrlKey) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1229
|
+
camera.center_x += dx
|
|
1230
|
+
camera.center_y += dy
|
|
1231
|
+
} else {
|
|
1232
|
+
// Horizontal drag -> azimuth rotation around z-axis
|
|
1233
|
+
camera.azimuth += dx * 0.3 // Positive dx (drag right) rotates clockwise
|
|
1234
|
+
|
|
1235
|
+
// Vertical drag -> elevation angle (full range)
|
|
1236
|
+
camera.elevation -= dy * 0.3 // Positive dy (drag down) tilts view down
|
|
796
1237
|
}
|
|
797
|
-
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
1238
|
+
|
|
1239
|
+
last_mouse = { x: event.clientX, y: event.clientY }
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const handle_wheel = (event: WheelEvent) => {
|
|
1243
|
+
event.preventDefault()
|
|
1244
|
+
camera.zoom = Math.max(
|
|
1245
|
+
0.5,
|
|
1246
|
+
Math.min(10, camera.zoom * (event.deltaY > 0 ? 0.98 : 1.02)),
|
|
1247
|
+
)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
const handle_hover = (event: MouseEvent) => {
|
|
1251
|
+
if (is_dragging) return
|
|
1252
|
+
const entry = find_entry_at_mouse(event)
|
|
807
1253
|
hover_data = entry
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
on_point_hover?.(hover_data)
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1254
|
+
? { entry, position: { x: event.clientX, y: event.clientY } }
|
|
1255
|
+
: null
|
|
1256
|
+
on_point_hover?.(hover_data)
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const find_entry_at_mouse = (event: MouseEvent): ConvexHullEntry | null =>
|
|
1260
|
+
helpers.find_hull_entry_at_mouse(
|
|
1261
|
+
canvas,
|
|
1262
|
+
event,
|
|
1263
|
+
plot_entries,
|
|
1264
|
+
(x: number, y: number, z: number) => {
|
|
1265
|
+
const pt = project_3d_point(x, y, z)
|
|
1266
|
+
return { x: pt.x, y: pt.y }
|
|
1267
|
+
},
|
|
1268
|
+
)
|
|
1269
|
+
|
|
1270
|
+
const handle_click = (event: MouseEvent) => {
|
|
1271
|
+
event.stopPropagation()
|
|
818
1272
|
// Check if this was a drag operation (any mouse movement during drag)
|
|
819
|
-
const was_drag = drag_started
|
|
820
|
-
drag_started = false
|
|
821
|
-
if (was_drag)
|
|
822
|
-
|
|
823
|
-
const entry = find_entry_at_mouse(event)
|
|
1273
|
+
const was_drag = drag_started
|
|
1274
|
+
drag_started = false // Reset for next interaction
|
|
1275
|
+
if (was_drag) return // Don't trigger click if this was a drag
|
|
1276
|
+
|
|
1277
|
+
const entry = find_entry_at_mouse(event)
|
|
824
1278
|
if (!entry) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return;
|
|
1279
|
+
if (modal_open) close_structure_popup()
|
|
1280
|
+
return
|
|
828
1281
|
}
|
|
829
|
-
|
|
1282
|
+
|
|
1283
|
+
on_point_click?.(entry)
|
|
1284
|
+
|
|
830
1285
|
if (enable_click_selection) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
}
|
|
1286
|
+
selected_entry = entry
|
|
1287
|
+
if (enable_structure_preview) {
|
|
1288
|
+
const structure = extract_structure_from_entry(entry)
|
|
1289
|
+
if (structure) {
|
|
1290
|
+
selected_structure = structure
|
|
1291
|
+
modal_place_right = helpers.calculate_modal_side(wrapper)
|
|
1292
|
+
modal_open = true
|
|
839
1293
|
}
|
|
1294
|
+
}
|
|
840
1295
|
}
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function close_structure_popup() {
|
|
1299
|
+
modal_open = false
|
|
1300
|
+
selected_structure = null
|
|
1301
|
+
selected_entry = null
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
const handle_double_click = (event: MouseEvent) => {
|
|
1305
|
+
const entry = find_entry_at_mouse(event)
|
|
849
1306
|
if (entry) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1307
|
+
copy_entry_data(entry, {
|
|
1308
|
+
x: event.clientX,
|
|
1309
|
+
y: event.clientY,
|
|
1310
|
+
})
|
|
854
1311
|
}
|
|
855
|
-
}
|
|
856
|
-
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const render_once = () => {
|
|
857
1315
|
if (!frame_id) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1316
|
+
frame_id = requestAnimationFrame(() => {
|
|
1317
|
+
render_frame()
|
|
1318
|
+
frame_id = 0
|
|
1319
|
+
})
|
|
862
1320
|
}
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const dpr = globalThis.devicePixelRatio || 1
|
|
868
|
-
const container = canvas.parentElement
|
|
869
|
-
const rect = container?.getBoundingClientRect()
|
|
870
|
-
const [w, h] = rect ? [rect.width, rect.height] : [400, 400]
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
function update_canvas_size() {
|
|
1324
|
+
if (!canvas) return
|
|
1325
|
+
const dpr = globalThis.devicePixelRatio || 1
|
|
1326
|
+
const container = canvas.parentElement
|
|
1327
|
+
const rect = container?.getBoundingClientRect()
|
|
1328
|
+
const [w, h] = rect ? [rect.width, rect.height] : [400, 400]
|
|
1329
|
+
|
|
871
1330
|
// Only update canvas dimensions if they actually changed
|
|
872
1331
|
// (assigning canvas.width/height clears the canvas even if values are the same)
|
|
873
|
-
const new_width = Math.max(0, Math.round(w * dpr))
|
|
874
|
-
const new_height = Math.max(0, Math.round(h * dpr))
|
|
1332
|
+
const new_width = Math.max(0, Math.round(w * dpr))
|
|
1333
|
+
const new_height = Math.max(0, Math.round(h * dpr))
|
|
875
1334
|
if (!ctx || canvas.width !== new_width || canvas.height !== new_height) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1335
|
+
canvas.width = new_width
|
|
1336
|
+
canvas.height = new_height
|
|
1337
|
+
ctx = canvas.getContext(`2d`)
|
|
1338
|
+
if (ctx) {
|
|
1339
|
+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
1340
|
+
ctx.imageSmoothingEnabled = true
|
|
1341
|
+
ctx.imageSmoothingQuality = `high`
|
|
1342
|
+
}
|
|
884
1343
|
}
|
|
885
|
-
canvas_dims = { width: w, height: h, scale: Math.min(w, h) / 600 }
|
|
886
|
-
render_once()
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
$
|
|
893
|
-
|
|
894
|
-
|
|
1344
|
+
canvas_dims = { width: w, height: h, scale: Math.min(w, h) / 600 }
|
|
1345
|
+
render_once()
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Reactive dark mode detection for canvas text color
|
|
1349
|
+
let dark_mode = $state(is_dark_mode())
|
|
1350
|
+
$effect(() => watch_dark_mode((dark) => dark_mode = dark))
|
|
1351
|
+
const text_color = $derived(helpers.get_canvas_text_color(dark_mode))
|
|
1352
|
+
|
|
1353
|
+
$effect(() => {
|
|
1354
|
+
if (!canvas) return
|
|
1355
|
+
|
|
895
1356
|
// Initial setup
|
|
896
|
-
update_canvas_size()
|
|
1357
|
+
update_canvas_size()
|
|
1358
|
+
|
|
897
1359
|
// Watch for resize events - only update canvas, don't reset camera
|
|
898
|
-
const resize_observer = new ResizeObserver(update_canvas_size)
|
|
899
|
-
|
|
1360
|
+
const resize_observer = new ResizeObserver(update_canvas_size)
|
|
1361
|
+
|
|
1362
|
+
const container = canvas.parentElement
|
|
900
1363
|
if (container) {
|
|
901
|
-
|
|
1364
|
+
resize_observer.observe(container)
|
|
902
1365
|
}
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
// Fullscreen handling with camera reset
|
|
912
|
-
let was_fullscreen = $state(fullscreen)
|
|
913
|
-
$effect(() => {
|
|
1366
|
+
|
|
1367
|
+
return () => { // Cleanup on unmount
|
|
1368
|
+
if (frame_id) cancelAnimationFrame(frame_id)
|
|
1369
|
+
if (pulse_frame_id) cancelAnimationFrame(pulse_frame_id)
|
|
1370
|
+
resize_observer.disconnect()
|
|
1371
|
+
}
|
|
1372
|
+
})
|
|
1373
|
+
|
|
1374
|
+
// Fullscreen handling with camera reset
|
|
1375
|
+
let was_fullscreen = $state(fullscreen)
|
|
1376
|
+
$effect(() => {
|
|
914
1377
|
setup_fullscreen_effect(fullscreen, wrapper, (entering_fullscreen) => {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
})
|
|
921
|
-
set_fullscreen_bg(wrapper, fullscreen, `--hull-3d-bg-fullscreen`)
|
|
922
|
-
})
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
const
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
1378
|
+
if (entering_fullscreen !== was_fullscreen) {
|
|
1379
|
+
camera.center_x = 0
|
|
1380
|
+
camera.center_y = -50
|
|
1381
|
+
was_fullscreen = entering_fullscreen
|
|
1382
|
+
}
|
|
1383
|
+
})
|
|
1384
|
+
set_fullscreen_bg(wrapper, fullscreen, `--hull-3d-bg-fullscreen`)
|
|
1385
|
+
})
|
|
1386
|
+
|
|
1387
|
+
// Performance: Cache canvas dimensions and formation energy range
|
|
1388
|
+
let canvas_dims = $state({ width: 600, height: 600, scale: 1 })
|
|
1389
|
+
const energy_range = $derived.by(() => {
|
|
1390
|
+
const energies = plot_entries.map((e) => e.e_form_per_atom ?? 0)
|
|
1391
|
+
const [min, max] = [Math.min(0, ...energies), Math.max(0, ...energies)]
|
|
1392
|
+
const z_scale = 0.75 / Math.max(max - min, 0.001)
|
|
1393
|
+
return { min, max, center: (min + max) / 2, z_scale }
|
|
1394
|
+
})
|
|
1395
|
+
|
|
1396
|
+
// Performance: Pre-compute and cache all point projections + depth sorting
|
|
1397
|
+
const sorted_points_cache = $derived.by(() => {
|
|
1398
|
+
if (!canvas || plot_entries.length === 0) return []
|
|
935
1399
|
return plot_entries
|
|
936
|
-
|
|
937
|
-
|
|
1400
|
+
.filter((entry) => entry.visible)
|
|
1401
|
+
.map((entry) => ({
|
|
938
1402
|
entry,
|
|
939
1403
|
projected: project_3d_point(entry.x, entry.y, entry.z),
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
})
|
|
943
|
-
|
|
1404
|
+
}))
|
|
1405
|
+
.sort((a, b) => a.projected.depth - b.projected.depth)
|
|
1406
|
+
})
|
|
1407
|
+
|
|
1408
|
+
let style = $derived(
|
|
1409
|
+
`--hull-stable-color:${merged_config.colors?.stable || `#0072B2`};
|
|
944
1410
|
--hull-unstable-color:${merged_config.colors?.unstable || `#E69F00`};
|
|
945
1411
|
--hull-edge-color:${merged_config.colors?.edge || `var(--text-color, #212121)`};
|
|
946
|
-
--hull-text-color:${
|
|
1412
|
+
--hull-text-color:${
|
|
1413
|
+
merged_config.colors?.annotation || `var(--text-color, #212121)`
|
|
1414
|
+
}`,
|
|
1415
|
+
)
|
|
947
1416
|
</script>
|
|
948
1417
|
|
|
949
1418
|
<svelte:document
|
|
@@ -984,7 +1453,7 @@ let style = $derived(`--hull-stable-color:${merged_config.colors?.stable || `#00
|
|
|
984
1453
|
selected_entry,
|
|
985
1454
|
})}
|
|
986
1455
|
<h3 style="position: absolute; left: 1em; top: 1ex; margin: 0; font-weight: 500">
|
|
987
|
-
{@html merged_controls.title || phase_stats?.chemical_system || ``}
|
|
1456
|
+
{@html sanitize_html(merged_controls.title || phase_stats?.chemical_system || ``)}
|
|
988
1457
|
</h3>
|
|
989
1458
|
<canvas
|
|
990
1459
|
bind:this={canvas}
|