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,100 +1,211 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { type D3InterpolateName } from '../colors'
|
|
3
|
+
import {
|
|
4
|
+
get_electro_neg_formula,
|
|
5
|
+
get_formula_label_segments,
|
|
6
|
+
type FormulaLabelSegment,
|
|
7
|
+
} from '../composition/format'
|
|
8
|
+
import { extract_formula_elements } from '../composition/parse'
|
|
9
|
+
import TemperatureSlider from '../convex-hull/TemperatureSlider.svelte'
|
|
10
|
+
import type { PhaseData } from '../convex-hull/types'
|
|
11
|
+
import Spinner from '../feedback/Spinner.svelte'
|
|
12
|
+
import Icon from '../Icon.svelte'
|
|
13
|
+
import { format_num } from '../labels'
|
|
14
|
+
import { set_fullscreen_bg, SettingsSection, toggle_fullscreen } from '../layout'
|
|
15
|
+
import type { Vec2, Vec3 } from '../math'
|
|
16
|
+
import {
|
|
17
|
+
convex_hull_2d,
|
|
18
|
+
cross_3d,
|
|
19
|
+
merge_coplanar_triangles,
|
|
20
|
+
normalize_vec3,
|
|
21
|
+
} from '../math'
|
|
22
|
+
import DraggablePane from '../overlays/DraggablePane.svelte'
|
|
23
|
+
import { ColorBar, ScatterPlot3DControls } from '../plot'
|
|
24
|
+
import {
|
|
25
|
+
constrain_tooltip_position,
|
|
26
|
+
pad_rect,
|
|
27
|
+
rects_overlap,
|
|
28
|
+
} from '../plot/layout'
|
|
29
|
+
import type {
|
|
30
|
+
AxisConfig3D,
|
|
31
|
+
CameraProjection3D,
|
|
32
|
+
DataSeries3D,
|
|
33
|
+
DisplayConfig3D,
|
|
34
|
+
} from '../plot/types'
|
|
35
|
+
import { Canvas, T } from '@threlte/core'
|
|
36
|
+
import * as extras from '@threlte/extras'
|
|
37
|
+
import { scaleLinear } from 'd3-scale'
|
|
38
|
+
import { onDestroy, onMount, untrack } from 'svelte'
|
|
39
|
+
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
40
|
+
import * as THREE from 'three'
|
|
41
|
+
import type { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
|
|
42
|
+
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js'
|
|
43
|
+
import { ConvexGeometry } from 'three/examples/jsm/geometries/ConvexGeometry.js'
|
|
44
|
+
import { compute_chempot_async } from './async-compute.svelte'
|
|
45
|
+
import ChemPotScene3D from './ChemPotScene3D.svelte'
|
|
46
|
+
import { get_chempot_color_bar_config, make_chempot_color_scale } from './color'
|
|
47
|
+
import {
|
|
48
|
+
apply_element_padding,
|
|
49
|
+
bbox_diagonal,
|
|
50
|
+
best_form_energy_for_formula,
|
|
51
|
+
build_axis_ranges,
|
|
52
|
+
dedup_points,
|
|
53
|
+
formula_key_from_composition,
|
|
54
|
+
get_3d_domain_simplexes_and_ann_loc,
|
|
55
|
+
get_energy_per_atom,
|
|
56
|
+
get_min_entries_and_el_refs,
|
|
57
|
+
get_ternary_combinations,
|
|
58
|
+
get_visible_domain_labels,
|
|
59
|
+
pad_domain_points,
|
|
60
|
+
scale_to_font_range,
|
|
61
|
+
} from './compute'
|
|
62
|
+
import { with_hover_pointer } from './pointer'
|
|
63
|
+
import {
|
|
64
|
+
get_projection_source_entries,
|
|
65
|
+
get_temp_filter_payload,
|
|
66
|
+
get_valid_temperature,
|
|
67
|
+
} from './temperature'
|
|
68
|
+
import type {
|
|
69
|
+
ChemPotColorMode,
|
|
70
|
+
ChemPotDiagramConfig,
|
|
71
|
+
ChemPotDiagramData,
|
|
72
|
+
ChemPotHoverInfo,
|
|
73
|
+
ChemPotHoverInfo3D,
|
|
74
|
+
} from './types'
|
|
75
|
+
import { CHEMPOT_DEFAULTS } from './types'
|
|
76
|
+
|
|
77
|
+
let {
|
|
78
|
+
entries = [],
|
|
79
|
+
config = {},
|
|
80
|
+
width = $bindable(800),
|
|
81
|
+
height = $bindable(600),
|
|
82
|
+
// Auto-corrected to a valid available temperature when needed.
|
|
83
|
+
temperature = $bindable<number | undefined>(undefined),
|
|
84
|
+
interpolate_temperature = CHEMPOT_DEFAULTS.interpolate_temperature,
|
|
85
|
+
max_interpolation_gap = CHEMPOT_DEFAULTS.max_interpolation_gap,
|
|
86
|
+
hover_info = $bindable<ChemPotHoverInfo | null>(null),
|
|
87
|
+
render_local_tooltip = true,
|
|
88
|
+
}: {
|
|
89
|
+
entries: PhaseData[]
|
|
90
|
+
config?: ChemPotDiagramConfig
|
|
91
|
+
width?: number
|
|
92
|
+
height?: number
|
|
93
|
+
temperature?: number
|
|
94
|
+
interpolate_temperature?: boolean
|
|
95
|
+
max_interpolation_gap?: number
|
|
96
|
+
hover_info?: ChemPotHoverInfo | null
|
|
97
|
+
render_local_tooltip?: boolean
|
|
98
|
+
} = $props()
|
|
99
|
+
|
|
100
|
+
let formal_chempots_override = $state<boolean | null>(null)
|
|
101
|
+
let label_stable_override = $state<boolean | null>(null)
|
|
102
|
+
let element_padding_override = $state<number | null>(null)
|
|
103
|
+
let default_min_limit_override = $state<number | null>(null)
|
|
104
|
+
let draw_formula_meshes_override = $state<boolean | null>(null)
|
|
105
|
+
let draw_formula_lines_override = $state<boolean | null>(null)
|
|
106
|
+
const formal_chempots = $derived(
|
|
107
|
+
formal_chempots_override ??
|
|
108
|
+
(config.formal_chempots ?? CHEMPOT_DEFAULTS.formal_chempots),
|
|
109
|
+
)
|
|
110
|
+
const label_stable = $derived(
|
|
111
|
+
label_stable_override ?? (config.label_stable ?? CHEMPOT_DEFAULTS.label_stable),
|
|
112
|
+
)
|
|
113
|
+
const element_padding = $derived(
|
|
114
|
+
element_padding_override ??
|
|
115
|
+
(config.element_padding ?? CHEMPOT_DEFAULTS.element_padding),
|
|
116
|
+
)
|
|
117
|
+
const default_min_limit = $derived(
|
|
118
|
+
default_min_limit_override ??
|
|
119
|
+
(config.default_min_limit ?? CHEMPOT_DEFAULTS.default_min_limit),
|
|
120
|
+
)
|
|
121
|
+
let formulas_to_draw_override = $state<string[] | null>(null)
|
|
122
|
+
const formulas_to_draw = $derived(
|
|
123
|
+
formulas_to_draw_override ?? (config.formulas_to_draw ?? []),
|
|
124
|
+
)
|
|
125
|
+
const draw_formula_meshes = $derived(
|
|
126
|
+
draw_formula_meshes_override ??
|
|
127
|
+
(config.draw_formula_meshes ?? CHEMPOT_DEFAULTS.draw_formula_meshes),
|
|
128
|
+
)
|
|
129
|
+
const draw_formula_lines = $derived(
|
|
130
|
+
draw_formula_lines_override ??
|
|
131
|
+
(config.draw_formula_lines ?? CHEMPOT_DEFAULTS.draw_formula_lines),
|
|
132
|
+
)
|
|
133
|
+
let color_mode_override = $state<ChemPotColorMode | null>(null)
|
|
134
|
+
let color_scale_override = $state<D3InterpolateName | null>(null)
|
|
135
|
+
let reverse_color_scale_override = $state<boolean | null>(null)
|
|
136
|
+
const color_mode = $derived(
|
|
137
|
+
color_mode_override ?? (config.color_mode ?? `arity`),
|
|
138
|
+
)
|
|
139
|
+
const color_scale = $derived(
|
|
140
|
+
color_scale_override ?? (config.color_scale ?? CHEMPOT_DEFAULTS.color_scale),
|
|
141
|
+
)
|
|
142
|
+
const reverse_color_scale = $derived(
|
|
143
|
+
reverse_color_scale_override ??
|
|
144
|
+
(config.reverse_color_scale ?? CHEMPOT_DEFAULTS.reverse_color_scale),
|
|
145
|
+
)
|
|
146
|
+
const show_tooltip = $derived(config.show_tooltip ?? CHEMPOT_DEFAULTS.show_tooltip)
|
|
147
|
+
const tooltip_detail_level = $derived(
|
|
148
|
+
config.tooltip_detail_level ?? CHEMPOT_DEFAULTS.tooltip_detail_level,
|
|
149
|
+
)
|
|
150
|
+
const formula_colors = $derived(
|
|
151
|
+
config.formula_colors?.length
|
|
152
|
+
? config.formula_colors
|
|
153
|
+
: CHEMPOT_DEFAULTS.formula_colors,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
function formula_label_segments(formula: string): FormulaLabelSegment[] {
|
|
157
|
+
return get_formula_label_segments(get_electro_neg_formula(formula, true, ``, `.3~s`))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function normalize_projection_triplet(
|
|
161
|
+
maybe_triplet: string[] | undefined,
|
|
162
|
+
available_elements: string[],
|
|
163
|
+
): string[] | null {
|
|
164
|
+
if (!maybe_triplet || maybe_triplet.length !== 3) return null
|
|
165
|
+
const deduped = Array.from(new Set(maybe_triplet))
|
|
166
|
+
if (deduped.length !== 3) return null
|
|
167
|
+
if (deduped.some((element) => !available_elements.includes(element))) return null
|
|
168
|
+
return deduped
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let wrapper = $state<HTMLDivElement>()
|
|
172
|
+
let fullscreen = $state(false)
|
|
173
|
+
let export_pane_open = $state(false)
|
|
174
|
+
let formula_picker_open = $state(false)
|
|
175
|
+
let controls_open = $state(false)
|
|
176
|
+
|
|
177
|
+
// Mutual exclusion: only one pane open at a time.
|
|
178
|
+
// Separate effects so each reacts to its own pane opening independently —
|
|
179
|
+
// a single $derived ternary would create priority ordering where opening
|
|
180
|
+
// a "lower" pane while a "higher" one is open fails silently.
|
|
181
|
+
$effect(() => { if (export_pane_open) { formula_picker_open = false; controls_open = false } })
|
|
182
|
+
$effect(() => { if (formula_picker_open) { export_pane_open = false; controls_open = false } })
|
|
183
|
+
$effect(() => { if (controls_open) { export_pane_open = false; formula_picker_open = false } })
|
|
184
|
+
let copy_status = $state(false)
|
|
185
|
+
let copy_timeout_id: ReturnType<typeof setTimeout> | null = null
|
|
186
|
+
let container_width = $state(0)
|
|
187
|
+
let container_height = $state(0)
|
|
188
|
+
const base_aspect_ratio = $derived(height > 0 && width > 0 ? height / width : 1)
|
|
189
|
+
const render_width = $derived(container_width > 0 ? container_width : width)
|
|
190
|
+
const render_height = $derived(
|
|
191
|
+
fullscreen
|
|
192
|
+
? (container_height > 0 ? container_height : height)
|
|
193
|
+
: Math.round(render_width * base_aspect_ratio),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
let mounted = $state(false)
|
|
197
|
+
onMount(() => mounted = true)
|
|
198
|
+
let orbit_controls_ref = $state<OrbitControls | undefined>(undefined)
|
|
199
|
+
// Backside tracking: axes/ticks/labels render on the far side from the camera
|
|
200
|
+
// back[i] = backside data coordinate value for data axis i
|
|
201
|
+
// Matches ScatterPlot3DScene pattern where pos tracks the opposite side from camera
|
|
202
|
+
let back = $state([0, 0, 0])
|
|
203
|
+
// Outward offset signs for tick/label placement (away from bounding box)
|
|
204
|
+
let out_x = $state(-1) // sign for Three.js X (data axis 1) direction
|
|
205
|
+
let out_y = $state(-1) // sign for Three.js Y (data axis 2) direction
|
|
206
|
+
let camera_projection = $state<CameraProjection3D>(`orthographic`)
|
|
207
|
+
let auto_rotate = $state(0)
|
|
208
|
+
let display = $state<DisplayConfig3D>({
|
|
98
209
|
show_axes: true,
|
|
99
210
|
show_grid: true,
|
|
100
211
|
show_axis_labels: true,
|
|
@@ -102,1700 +213,2019 @@ let display = $state({
|
|
|
102
213
|
projections: { xy: false, xz: false, yz: false },
|
|
103
214
|
projection_opacity: 0.15,
|
|
104
215
|
projection_scale: 0.5,
|
|
105
|
-
})
|
|
106
|
-
let x_axis = $state({ label: ``, range: [null, null] })
|
|
107
|
-
let y_axis = $state({ label: ``, range: [null, null] })
|
|
108
|
-
let z_axis = $state({ label: ``, range: [null, null] })
|
|
109
|
-
const projection_opacity = $derived(display.projection_opacity ?? 0.15)
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// data[
|
|
114
|
-
// data[
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
216
|
+
})
|
|
217
|
+
let x_axis = $state<AxisConfig3D>({ label: ``, range: [null, null] })
|
|
218
|
+
let y_axis = $state<AxisConfig3D>({ label: ``, range: [null, null] })
|
|
219
|
+
let z_axis = $state<AxisConfig3D>({ label: ``, range: [null, null] })
|
|
220
|
+
const projection_opacity = $derived(display.projection_opacity ?? 0.15)
|
|
221
|
+
|
|
222
|
+
// Plotly/pymatgen uses Z-up with x-axis projecting left in isometric view.
|
|
223
|
+
// Three.js uses Y-up with X projecting right. To match pymatgen's visual layout:
|
|
224
|
+
// data[0] (plotly x, projects left) → Three.js Z (projects left)
|
|
225
|
+
// data[1] (plotly y, projects right) → Three.js X (projects right)
|
|
226
|
+
// data[2] (plotly z, projects up) → Three.js Y (projects up)
|
|
227
|
+
function to_vec3(pt: number[]): THREE.Vector3 {
|
|
228
|
+
const [x_val, y_val, z_val] = to_render_xyz(pt)
|
|
229
|
+
return new THREE.Vector3(x_val, y_val, z_val)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Compute diagram data (requires >= 3 elements for 3D rendering)
|
|
233
|
+
const { has_temp_data, available_temperatures, temp_filtered_entries } = $derived(
|
|
234
|
+
get_temp_filter_payload(entries, temperature, config, {
|
|
235
|
+
interpolate_temperature,
|
|
236
|
+
max_interpolation_gap,
|
|
237
|
+
}),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
// Keep bound temperature aligned with available data points.
|
|
241
|
+
$effect(() => {
|
|
242
|
+
const next_temperature = get_valid_temperature(
|
|
243
|
+
temperature,
|
|
244
|
+
has_temp_data,
|
|
245
|
+
available_temperatures,
|
|
246
|
+
)
|
|
247
|
+
if (next_temperature !== temperature) temperature = next_temperature
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
const show_temperature_slider = $derived(
|
|
251
|
+
has_temp_data && available_temperatures.length > 0,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
const projection_source_entries = $derived(
|
|
255
|
+
get_projection_source_entries(entries, temp_filtered_entries),
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
const all_entry_elements = $derived.by(() =>
|
|
259
|
+
Array.from(
|
|
260
|
+
new SvelteSet(
|
|
261
|
+
projection_source_entries.flatMap((entry) =>
|
|
262
|
+
Object.entries(entry.composition)
|
|
263
|
+
.filter(([, amount]) => amount > 0)
|
|
264
|
+
.map(([element]) => element)
|
|
265
|
+
),
|
|
266
|
+
),
|
|
267
|
+
).sort()
|
|
268
|
+
)
|
|
269
|
+
const has_multinary_system = $derived(all_entry_elements.length > 3)
|
|
270
|
+
let projection_elements_override = $state<string[] | null>(null)
|
|
271
|
+
const config_projection_elements = $derived(
|
|
272
|
+
normalize_projection_triplet(config.elements, all_entry_elements),
|
|
273
|
+
)
|
|
274
|
+
const projection_elements = $derived.by(() => {
|
|
275
|
+
if (all_entry_elements.length < 3) return []
|
|
141
276
|
if (!has_multinary_system) {
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
const override_projection = normalize_projection_triplet(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
277
|
+
return config_projection_elements ?? all_entry_elements.slice(0, 3)
|
|
278
|
+
}
|
|
279
|
+
const override_projection = normalize_projection_triplet(
|
|
280
|
+
projection_elements_override ?? undefined,
|
|
281
|
+
all_entry_elements,
|
|
282
|
+
)
|
|
283
|
+
if (override_projection) return override_projection
|
|
284
|
+
if (config_projection_elements) return config_projection_elements
|
|
285
|
+
return all_entry_elements.slice(0, 3)
|
|
286
|
+
})
|
|
287
|
+
const effective_config = $derived({
|
|
152
288
|
...config,
|
|
153
289
|
elements: projection_elements.length === 3
|
|
154
|
-
|
|
155
|
-
|
|
290
|
+
? projection_elements
|
|
291
|
+
: config.elements,
|
|
156
292
|
formal_chempots,
|
|
157
293
|
label_stable,
|
|
158
294
|
element_padding,
|
|
159
295
|
default_min_limit,
|
|
160
296
|
draw_formula_meshes,
|
|
161
297
|
draw_formula_lines,
|
|
162
|
-
})
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
298
|
+
})
|
|
299
|
+
let diagram_data = $state<ChemPotDiagramData | null>(null)
|
|
300
|
+
let diagram_computing = $state(false)
|
|
301
|
+
$effect(() => {
|
|
302
|
+
if (temp_filtered_entries.length < 3) {
|
|
303
|
+
diagram_data = null
|
|
304
|
+
diagram_computing = false
|
|
305
|
+
return
|
|
306
|
+
}
|
|
307
|
+
let cancelled = false
|
|
308
|
+
diagram_computing = true
|
|
309
|
+
compute_chempot_async(temp_filtered_entries, effective_config)
|
|
310
|
+
.then((data) => {
|
|
311
|
+
if (cancelled) return
|
|
312
|
+
diagram_data = data.elements.length >= 3 ? data : null
|
|
313
|
+
diagram_computing = false
|
|
314
|
+
})
|
|
315
|
+
.catch((err) => {
|
|
316
|
+
if (cancelled) return
|
|
317
|
+
console.error(`ChemPotDiagram3D:`, err)
|
|
318
|
+
diagram_data = null
|
|
319
|
+
diagram_computing = false
|
|
320
|
+
})
|
|
321
|
+
return () => { cancelled = true }
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
const plot_elements = $derived(diagram_data?.elements ?? projection_elements)
|
|
325
|
+
const is_projection_mode = $derived(
|
|
326
|
+
plot_elements.length > 0 &&
|
|
327
|
+
plot_elements.length < all_entry_elements.length &&
|
|
328
|
+
plot_elements.every((element) => all_entry_elements.includes(element)),
|
|
329
|
+
)
|
|
330
|
+
const projection_presets = $derived.by(() => {
|
|
331
|
+
const presets: string[][] = []
|
|
332
|
+
const seen = new Set<string>()
|
|
333
|
+
const add_triplet = (candidate: string[] | null): void => {
|
|
334
|
+
if (!candidate) return
|
|
335
|
+
const key = candidate.join(`|`)
|
|
336
|
+
if (seen.has(key)) return
|
|
337
|
+
seen.add(key)
|
|
338
|
+
presets.push(candidate)
|
|
339
|
+
}
|
|
340
|
+
add_triplet(config_projection_elements)
|
|
341
|
+
add_triplet(plot_elements.length === 3 ? plot_elements : null)
|
|
342
|
+
for (const combo of get_ternary_combinations(all_entry_elements)) {
|
|
343
|
+
add_triplet(combo)
|
|
344
|
+
if (presets.length >= 12) break
|
|
345
|
+
}
|
|
346
|
+
return presets
|
|
347
|
+
})
|
|
348
|
+
const current_projection_key = $derived(plot_elements.join(`|`))
|
|
349
|
+
let formula_filter_query = $state(``)
|
|
350
|
+
const available_formulas = $derived.by(() =>
|
|
351
|
+
Object.keys(diagram_data?.domains ?? {}).sort()
|
|
352
|
+
)
|
|
353
|
+
const filtered_formulas = $derived.by(() => {
|
|
354
|
+
const query = formula_filter_query.trim().toLowerCase()
|
|
355
|
+
if (!query) return available_formulas
|
|
356
|
+
return available_formulas.filter((formula) =>
|
|
357
|
+
formula.toLowerCase().includes(query)
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Process domains for rendering
|
|
362
|
+
interface DomainRenderData {
|
|
363
|
+
formula: string
|
|
364
|
+
points_3d: number[][]
|
|
365
|
+
ann_loc: number[]
|
|
366
|
+
is_draw_formula: boolean
|
|
367
|
+
label_font_size: number
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
interface HoverMeshData {
|
|
371
|
+
formula: string
|
|
372
|
+
geometry: THREE.BufferGeometry
|
|
373
|
+
info: ChemPotHoverInfo3D
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
interface FormulaEnergyStats {
|
|
377
|
+
matching_entry_count: number
|
|
378
|
+
min_energy_per_atom: number | null
|
|
379
|
+
max_energy_per_atom: number | null
|
|
380
|
+
}
|
|
381
|
+
type NumericColorMode = Exclude<ChemPotColorMode, `none` | `arity`>
|
|
382
|
+
|
|
383
|
+
const render_domains = $derived.by((): DomainRenderData[] => {
|
|
384
|
+
if (!diagram_data || plot_elements.length < 2) return []
|
|
385
|
+
|
|
386
|
+
const dim = diagram_data.elements.length
|
|
387
|
+
const indices = Array.from({ length: dim }, (_, idx) => idx)
|
|
225
388
|
const new_lims = element_padding > 0
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
389
|
+
? apply_element_padding(
|
|
390
|
+
diagram_data.domains,
|
|
391
|
+
indices,
|
|
392
|
+
element_padding,
|
|
393
|
+
default_min_limit,
|
|
394
|
+
)
|
|
395
|
+
: null
|
|
396
|
+
|
|
397
|
+
const result: DomainRenderData[] = []
|
|
229
398
|
for (const [formula, pts] of Object.entries(diagram_data.domains)) {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return result
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
const
|
|
399
|
+
const padded = new_lims
|
|
400
|
+
? pad_domain_points(
|
|
401
|
+
pts,
|
|
402
|
+
indices,
|
|
403
|
+
new_lims,
|
|
404
|
+
default_min_limit,
|
|
405
|
+
element_padding,
|
|
406
|
+
)
|
|
407
|
+
: pts
|
|
408
|
+
if (padded.length < 2) continue
|
|
409
|
+
const is_draw = formulas_to_draw.includes(formula)
|
|
410
|
+
const centroid = padded[0].map((_, col_idx) =>
|
|
411
|
+
padded.reduce((sum, point) => sum + point[col_idx], 0) / padded.length
|
|
412
|
+
)
|
|
413
|
+
const ann_loc = padded.length >= 3
|
|
414
|
+
? get_3d_domain_simplexes_and_ann_loc(padded).ann_loc
|
|
415
|
+
: centroid
|
|
416
|
+
result.push({
|
|
417
|
+
formula,
|
|
418
|
+
points_3d: padded,
|
|
419
|
+
ann_loc,
|
|
420
|
+
is_draw_formula: is_draw,
|
|
421
|
+
label_font_size: bbox_diagonal(padded),
|
|
422
|
+
})
|
|
423
|
+
}
|
|
424
|
+
const fonts = scale_to_font_range(result.map((d) => d.label_font_size), 9, 15)
|
|
425
|
+
for (let idx = 0; idx < result.length; idx++) result[idx].label_font_size = fonts[idx]
|
|
426
|
+
return result
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
const entry_energy_stats_by_formula = $derived.by(
|
|
430
|
+
(): SvelteMap<string, FormulaEnergyStats> => {
|
|
431
|
+
const stats_by_formula = new SvelteMap<string, FormulaEnergyStats>()
|
|
432
|
+
for (const entry of temp_filtered_entries) {
|
|
433
|
+
const formula_key = formula_key_from_composition(entry.composition)
|
|
434
|
+
const energy_per_atom = get_energy_per_atom(entry)
|
|
435
|
+
const existing = stats_by_formula.get(formula_key)
|
|
265
436
|
if (!existing) {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
437
|
+
stats_by_formula.set(formula_key, {
|
|
438
|
+
matching_entry_count: 1,
|
|
439
|
+
min_energy_per_atom: energy_per_atom,
|
|
440
|
+
max_energy_per_atom: energy_per_atom,
|
|
441
|
+
})
|
|
442
|
+
continue
|
|
272
443
|
}
|
|
273
444
|
stats_by_formula.set(formula_key, {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
445
|
+
matching_entry_count: existing.matching_entry_count + 1,
|
|
446
|
+
min_energy_per_atom: Math.min(
|
|
447
|
+
existing.min_energy_per_atom ?? energy_per_atom,
|
|
448
|
+
energy_per_atom,
|
|
449
|
+
),
|
|
450
|
+
max_energy_per_atom: Math.max(
|
|
451
|
+
existing.max_energy_per_atom ?? energy_per_atom,
|
|
452
|
+
energy_per_atom,
|
|
453
|
+
),
|
|
454
|
+
})
|
|
455
|
+
}
|
|
456
|
+
return stats_by_formula
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
// === Region coloring ===
|
|
461
|
+
// Categorical palette for arity mode (element count)
|
|
462
|
+
const arity_colors = [`#3498db`, `#2ecc71`, `#e67e22`, `#9b59b6`] as const
|
|
463
|
+
|
|
464
|
+
// Original (non-renormalized) elemental references for formation energy computation.
|
|
465
|
+
// diagram_data.el_refs may be renormalized to zero when formal_chempots is true,
|
|
466
|
+
// so we compute our own from the raw entries to get true DFT reference energies.
|
|
467
|
+
const raw_el_refs = $derived(
|
|
468
|
+
get_min_entries_and_el_refs(temp_filtered_entries).el_refs,
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
const color_mode_labels: Record<NumericColorMode, string> = {
|
|
289
472
|
energy: `Energy per atom (eV)`,
|
|
290
473
|
formation_energy: `Formation energy (eV/atom)`,
|
|
291
474
|
entries: `Entry count`,
|
|
292
|
-
}
|
|
293
|
-
function get_numeric_color_value(
|
|
475
|
+
}
|
|
476
|
+
function get_numeric_color_value(
|
|
477
|
+
formula: string,
|
|
478
|
+
active_color_mode: NumericColorMode,
|
|
479
|
+
): number | null {
|
|
294
480
|
if (active_color_mode === `energy`) {
|
|
295
|
-
|
|
481
|
+
return entry_energy_stats_by_formula.get(formula)?.min_energy_per_atom ?? null
|
|
296
482
|
}
|
|
297
483
|
if (active_color_mode === `formation_energy`) {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
if (color_mode === `none` || color_mode === `arity`)
|
|
304
|
-
return null;
|
|
305
|
-
const active_color_mode = color_mode;
|
|
306
|
-
const value_by_formula = new SvelteMap();
|
|
307
|
-
const values = [];
|
|
308
|
-
for (const domain of render_domains) {
|
|
309
|
-
const value = get_numeric_color_value(domain.formula, active_color_mode);
|
|
310
|
-
if (value == null || !Number.isFinite(value))
|
|
311
|
-
continue;
|
|
312
|
-
values.push(value);
|
|
313
|
-
value_by_formula.set(domain.formula, value);
|
|
314
|
-
}
|
|
315
|
-
return { value_by_formula, values };
|
|
316
|
-
});
|
|
317
|
-
// Per-domain color map keyed by formula
|
|
318
|
-
const domain_colors = $derived.by(() => {
|
|
319
|
-
const colors = new SvelteMap();
|
|
320
|
-
if (color_mode === `none`)
|
|
321
|
-
return colors;
|
|
322
|
-
if (color_mode === `arity`) {
|
|
323
|
-
for (const domain of render_domains) {
|
|
324
|
-
const n_elements = extract_formula_elements(domain.formula).length;
|
|
325
|
-
const idx = Math.min(n_elements, arity_colors.length) - 1;
|
|
326
|
-
colors.set(domain.formula, arity_colors[Math.max(0, idx)]);
|
|
327
|
-
}
|
|
328
|
-
return colors;
|
|
484
|
+
return best_form_energy_for_formula(
|
|
485
|
+
temp_filtered_entries,
|
|
486
|
+
formula,
|
|
487
|
+
raw_el_refs,
|
|
488
|
+
) ?? null
|
|
329
489
|
}
|
|
330
|
-
|
|
331
|
-
|
|
490
|
+
return entry_energy_stats_by_formula.get(formula)?.matching_entry_count ?? 0
|
|
491
|
+
}
|
|
492
|
+
const domain_color_values = $derived.by(
|
|
493
|
+
(): { value_by_formula: SvelteMap<string, number>; values: number[] } | null => {
|
|
494
|
+
if (color_mode === `none` || color_mode === `arity`) return null
|
|
495
|
+
const active_color_mode = color_mode as NumericColorMode
|
|
496
|
+
const value_by_formula = new SvelteMap<string, number>()
|
|
497
|
+
const values: number[] = []
|
|
498
|
+
for (const domain of render_domains) {
|
|
499
|
+
const value = get_numeric_color_value(domain.formula, active_color_mode)
|
|
500
|
+
if (value == null || !Number.isFinite(value)) continue
|
|
501
|
+
values.push(value)
|
|
502
|
+
value_by_formula.set(domain.formula, value)
|
|
503
|
+
}
|
|
504
|
+
return { value_by_formula, values }
|
|
505
|
+
},
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
// Per-domain color map keyed by formula
|
|
509
|
+
const domain_colors = $derived.by((): SvelteMap<string, string> => {
|
|
510
|
+
const colors = new SvelteMap<string, string>()
|
|
511
|
+
if (color_mode === `none`) return colors
|
|
512
|
+
|
|
513
|
+
if (color_mode === `arity`) {
|
|
514
|
+
for (const domain of render_domains) {
|
|
515
|
+
const n_elements = extract_formula_elements(domain.formula).length
|
|
516
|
+
const idx = Math.min(n_elements, arity_colors.length) - 1
|
|
517
|
+
colors.set(domain.formula, arity_colors[Math.max(0, idx)])
|
|
518
|
+
}
|
|
519
|
+
return colors
|
|
520
|
+
}
|
|
521
|
+
const values_payload = domain_color_values
|
|
522
|
+
const scale = make_chempot_color_scale(
|
|
523
|
+
values_payload?.values ?? [],
|
|
524
|
+
color_scale,
|
|
525
|
+
reverse_color_scale,
|
|
526
|
+
)
|
|
332
527
|
for (const domain of render_domains) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
return colors;
|
|
337
|
-
});
|
|
338
|
-
// Range and label for the color bar (null for none/arity which are categorical)
|
|
339
|
-
const color_range = $derived.by(() => {
|
|
340
|
-
const values = domain_color_values?.values ?? [];
|
|
341
|
-
if (values.length === 0)
|
|
342
|
-
return null;
|
|
343
|
-
let lo = values[0], hi = values[0];
|
|
344
|
-
for (let idx = 1; idx < values.length; idx++) {
|
|
345
|
-
if (values[idx] < lo)
|
|
346
|
-
lo = values[idx];
|
|
347
|
-
if (values[idx] > hi)
|
|
348
|
-
hi = values[idx];
|
|
528
|
+
const value = values_payload?.value_by_formula.get(domain.formula)
|
|
529
|
+
colors.set(domain.formula, value != null && scale ? scale(value) : `#999`)
|
|
349
530
|
}
|
|
350
|
-
return
|
|
531
|
+
return colors
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
// Range and label for the color bar (null for none/arity which are categorical)
|
|
535
|
+
const color_range = $derived.by(
|
|
536
|
+
(): { min: number; max: number; label: string } | null => {
|
|
537
|
+
const values = domain_color_values?.values ?? []
|
|
538
|
+
if (values.length === 0) return null
|
|
539
|
+
let lo = values[0], hi = values[0]
|
|
540
|
+
for (let idx = 1; idx < values.length; idx++) {
|
|
541
|
+
if (values[idx] < lo) lo = values[idx]
|
|
542
|
+
if (values[idx] > hi) hi = values[idx]
|
|
543
|
+
}
|
|
544
|
+
return {
|
|
351
545
|
min: lo,
|
|
352
546
|
max: Math.max(hi, lo + 1e-6),
|
|
353
547
|
label: color_mode === `none` || color_mode === `arity`
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
|
|
548
|
+
? ``
|
|
549
|
+
: color_mode_labels[color_mode],
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
const arity_legend_labels = $derived.by((): string[] => {
|
|
555
|
+
let has_four_plus_regions = false
|
|
360
556
|
for (const domain of render_domains) {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
557
|
+
if (extract_formula_elements(domain.formula).length >= 4) {
|
|
558
|
+
has_four_plus_regions = true
|
|
559
|
+
break
|
|
560
|
+
}
|
|
365
561
|
}
|
|
366
562
|
return has_four_plus_regions
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
//
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
let min0 = Infinity, max0 = -Infinity
|
|
377
|
-
let min1 = Infinity, max1 = -Infinity
|
|
378
|
-
let min2 = Infinity, max2 = -Infinity
|
|
563
|
+
? [`Unary`, `Binary`, `Ternary`, `4+`]
|
|
564
|
+
: [`Unary`, `Binary`, `Ternary`]
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
// Stretch short axes to improve screen-space utilization for highly anisotropic systems.
|
|
568
|
+
// Mapping is in rendered axis order: X=data[1], Y=data[2], Z=data[0].
|
|
569
|
+
const render_axis_scale = $derived.by((): Vec3 => {
|
|
570
|
+
const points = render_domains.flatMap((domain) => domain.points_3d)
|
|
571
|
+
if (points.length === 0) return [1, 1, 1]
|
|
572
|
+
let min0 = Infinity, max0 = -Infinity
|
|
573
|
+
let min1 = Infinity, max1 = -Infinity
|
|
574
|
+
let min2 = Infinity, max2 = -Infinity
|
|
379
575
|
for (const point of points) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
max2 = point[2];
|
|
392
|
-
}
|
|
393
|
-
const span_x = Math.max(max1 - min1, 1e-6); // render X from data axis 1
|
|
394
|
-
const span_y = Math.max(max2 - min2, 1e-6); // render Y from data axis 2
|
|
395
|
-
const span_z = Math.max(max0 - min0, 1e-6); // render Z from data axis 0
|
|
396
|
-
const max_span = Math.max(span_x, span_y, span_z);
|
|
576
|
+
if (point[0] < min0) min0 = point[0]
|
|
577
|
+
if (point[0] > max0) max0 = point[0]
|
|
578
|
+
if (point[1] < min1) min1 = point[1]
|
|
579
|
+
if (point[1] > max1) max1 = point[1]
|
|
580
|
+
if (point[2] < min2) min2 = point[2]
|
|
581
|
+
if (point[2] > max2) max2 = point[2]
|
|
582
|
+
}
|
|
583
|
+
const span_x = Math.max(max1 - min1, 1e-6) // render X from data axis 1
|
|
584
|
+
const span_y = Math.max(max2 - min2, 1e-6) // render Y from data axis 2
|
|
585
|
+
const span_z = Math.max(max0 - min0, 1e-6) // render Z from data axis 0
|
|
586
|
+
const max_span = Math.max(span_x, span_y, span_z)
|
|
397
587
|
return [
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
]
|
|
402
|
-
})
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
588
|
+
Math.min(Math.max(max_span / span_x, 1), 4),
|
|
589
|
+
Math.min(Math.max(max_span / span_y, 1), 4),
|
|
590
|
+
Math.min(Math.max(max_span / span_z, 1), 4),
|
|
591
|
+
]
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
function to_render_xyz(point: number[]): Vec3 {
|
|
595
|
+
const [scale_x, scale_y, scale_z] = render_axis_scale
|
|
596
|
+
return [point[1] * scale_x, point[2] * scale_y, point[0] * scale_z]
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Compute data center and extent for camera positioning (in swizzled coords)
|
|
600
|
+
const { data_center, data_extent } = $derived.by(() => {
|
|
601
|
+
const points = render_domains.flatMap((domain) => domain.points_3d)
|
|
410
602
|
if (points.length === 0) {
|
|
411
|
-
|
|
603
|
+
return { data_center: new THREE.Vector3(0, 0, 0), data_extent: 10 }
|
|
412
604
|
}
|
|
413
605
|
// Compute center in rendered coordinates (swizzled + axis scaling).
|
|
414
|
-
let [sum_x, sum_y, sum_z] = [0, 0, 0]
|
|
606
|
+
let [sum_x, sum_y, sum_z] = [0, 0, 0]
|
|
415
607
|
for (const point_3d of points) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
421
|
-
const n_points = points.length
|
|
422
|
-
const center = new THREE.Vector3(
|
|
608
|
+
const [x_val, y_val, z_val] = to_render_xyz(point_3d)
|
|
609
|
+
sum_x += x_val
|
|
610
|
+
sum_y += y_val
|
|
611
|
+
sum_z += z_val
|
|
612
|
+
}
|
|
613
|
+
const n_points = points.length
|
|
614
|
+
const center = new THREE.Vector3(
|
|
615
|
+
sum_x / n_points,
|
|
616
|
+
sum_y / n_points,
|
|
617
|
+
sum_z / n_points,
|
|
618
|
+
)
|
|
423
619
|
// Compute max distance from center
|
|
424
|
-
let max_dist = 0
|
|
620
|
+
let max_dist = 0
|
|
425
621
|
for (const point of points) {
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const default_camera_position = $derived([
|
|
622
|
+
const [x_val, y_val, z_val] = to_render_xyz(point)
|
|
623
|
+
const dist = Math.hypot(x_val - center.x, y_val - center.y, z_val - center.z)
|
|
624
|
+
if (dist > max_dist) max_dist = dist
|
|
625
|
+
}
|
|
626
|
+
return { data_center: center, data_extent: Math.max(max_dist * 1.3, 1) }
|
|
627
|
+
})
|
|
628
|
+
const default_camera_position = $derived<Vec3>([
|
|
434
629
|
data_center.x + data_extent,
|
|
435
630
|
data_center.y + data_extent,
|
|
436
631
|
data_center.z + data_extent,
|
|
437
|
-
])
|
|
438
|
-
const default_camera_target = $derived([
|
|
632
|
+
])
|
|
633
|
+
const default_camera_target = $derived<Vec3>([
|
|
439
634
|
data_center.x,
|
|
440
635
|
data_center.y,
|
|
441
636
|
data_center.z,
|
|
442
|
-
])
|
|
443
|
-
const default_orthographic_zoom = $derived(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
let
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
637
|
+
])
|
|
638
|
+
const default_orthographic_zoom = $derived(
|
|
639
|
+
Math.min(render_width, render_height) / (data_extent * 1.6),
|
|
640
|
+
)
|
|
641
|
+
let camera_position_override = $state<Vec3 | null>(null)
|
|
642
|
+
let camera_target_override = $state<Vec3 | null>(null)
|
|
643
|
+
let orthographic_zoom_override = $state<number | null>(null)
|
|
644
|
+
const camera_position = $derived(
|
|
645
|
+
camera_position_override ?? default_camera_position,
|
|
646
|
+
)
|
|
647
|
+
const camera_target = $derived(
|
|
648
|
+
camera_target_override ?? default_camera_target,
|
|
649
|
+
)
|
|
650
|
+
const orthographic_zoom = $derived(
|
|
651
|
+
orthographic_zoom_override ?? default_orthographic_zoom,
|
|
652
|
+
)
|
|
653
|
+
// Label scale factor: zoom relative to default, so labels grow/shrink with zoom
|
|
654
|
+
// Labels scale sub-linearly with zoom so they grow but don't dominate when zoomed in
|
|
655
|
+
const zoom_scale = $derived(
|
|
656
|
+
default_orthographic_zoom > 0 ? Math.sqrt(orthographic_zoom / default_orthographic_zoom) : 1,
|
|
657
|
+
)
|
|
658
|
+
let last_data_center: Vec3 | null = null
|
|
659
|
+
let last_data_extent: number | null = null
|
|
660
|
+
|
|
661
|
+
// Compute domain boundary edges via axis-aligned 2D convex hull projection.
|
|
662
|
+
// Each domain in a chem pot diagram is a convex polygon/polyhedron. We project
|
|
663
|
+
// to 2D (trying all 3 axis-aligned planes) and use the best projection's
|
|
664
|
+
// convex hull boundary. This reliably handles both flat and 3D domains.
|
|
665
|
+
function get_domain_edges(
|
|
666
|
+
pts: number[][],
|
|
667
|
+
): [number[], number[]][] {
|
|
668
|
+
const unique = dedup_3d(pts)
|
|
669
|
+
if (unique.length < 2) return []
|
|
670
|
+
if (unique.length === 2) return [[unique[0], unique[1]]]
|
|
462
671
|
if (unique.length === 3) {
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
return get_2d_hull_edges(unique)
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
let area_twice = 0
|
|
672
|
+
return [[unique[0], unique[1]], [unique[1], unique[2]], [unique[0], unique[2]]]
|
|
673
|
+
}
|
|
674
|
+
return get_2d_hull_edges(unique)
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function polygon_area_2d(points_2d: Vec2[]): number {
|
|
678
|
+
if (points_2d.length < 3) return 0
|
|
679
|
+
let area_twice = 0
|
|
471
680
|
for (let idx = 0; idx < points_2d.length; idx++) {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
}
|
|
476
|
-
return Math.abs(area_twice) / 2
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
//
|
|
480
|
-
// non-
|
|
481
|
-
//
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
681
|
+
const current = points_2d[idx]
|
|
682
|
+
const next = points_2d[(idx + 1) % points_2d.length]
|
|
683
|
+
area_twice += current[0] * next[1] - next[0] * current[1]
|
|
684
|
+
}
|
|
685
|
+
return Math.abs(area_twice) / 2
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// Compute domain edges from the single best axis-aligned projection
|
|
689
|
+
// (largest non-degenerate hull area). Unioning multiple projections can add
|
|
690
|
+
// non-physical diagonals for nearly coplanar domains.
|
|
691
|
+
// Called only from get_domain_edges with 4+ unique points
|
|
692
|
+
function get_2d_hull_edges(
|
|
693
|
+
pts: number[][],
|
|
694
|
+
): [number[], number[]][] {
|
|
695
|
+
let selected_hull: Vec2[] = []
|
|
696
|
+
let selected_coord_to_idx: SvelteMap<string, number> | null = null
|
|
697
|
+
let selected_hull_area = -1
|
|
698
|
+
|
|
486
699
|
for (const drop of [0, 1, 2]) {
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
if (!selected_coord_to_idx || selected_hull.length < 3)
|
|
528
|
-
return [];
|
|
529
|
-
const edges = [];
|
|
700
|
+
const axes = [0, 1, 2].filter((ax) => ax !== drop)
|
|
701
|
+
|
|
702
|
+
// Skip this projection if points collapse to a line (near-zero range in
|
|
703
|
+
// either projected axis). This avoids spurious edges from edge-on views.
|
|
704
|
+
let min0 = Infinity, max0 = -Infinity, min1 = Infinity, max1 = -Infinity
|
|
705
|
+
for (const pt of pts) {
|
|
706
|
+
const v0 = pt[axes[0]], v1 = pt[axes[1]]
|
|
707
|
+
if (v0 < min0) min0 = v0
|
|
708
|
+
if (v0 > max0) max0 = v0
|
|
709
|
+
if (v1 < min1) min1 = v1
|
|
710
|
+
if (v1 > max1) max1 = v1
|
|
711
|
+
}
|
|
712
|
+
const range0 = max0 - min0, range1 = max1 - min1
|
|
713
|
+
const max_2d_range = Math.max(range0, range1)
|
|
714
|
+
if (max_2d_range < 1e-6 || Math.min(range0, range1) < max_2d_range * 0.01) {
|
|
715
|
+
continue
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Build coordinate lookup for this projection
|
|
719
|
+
const coord_to_idx = new SvelteMap<string, number>()
|
|
720
|
+
const pts_2d: Vec2[] = []
|
|
721
|
+
for (let idx = 0; idx < pts.length; idx++) {
|
|
722
|
+
const p2 = [pts[idx][axes[0]], pts[idx][axes[1]]] as Vec2
|
|
723
|
+
pts_2d.push(p2)
|
|
724
|
+
const key = `${p2[0].toFixed(6)},${p2[1].toFixed(6)}`
|
|
725
|
+
if (!coord_to_idx.has(key)) coord_to_idx.set(key, idx)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const hull = convex_hull_2d(pts_2d)
|
|
729
|
+
if (hull.length < 3) continue
|
|
730
|
+
const hull_area = polygon_area_2d(hull)
|
|
731
|
+
if (hull_area <= selected_hull_area) continue
|
|
732
|
+
selected_hull = hull
|
|
733
|
+
selected_coord_to_idx = coord_to_idx
|
|
734
|
+
selected_hull_area = hull_area
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if (!selected_coord_to_idx || selected_hull.length < 3) return []
|
|
738
|
+
|
|
739
|
+
const edges: [number[], number[]][] = []
|
|
530
740
|
for (let idx = 0; idx < selected_hull.length; idx++) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
741
|
+
const point_a = selected_hull[idx]
|
|
742
|
+
const point_b = selected_hull[(idx + 1) % selected_hull.length]
|
|
743
|
+
const point_a_idx = selected_coord_to_idx.get(
|
|
744
|
+
`${point_a[0].toFixed(6)},${point_a[1].toFixed(6)}`,
|
|
745
|
+
)
|
|
746
|
+
const point_b_idx = selected_coord_to_idx.get(
|
|
747
|
+
`${point_b[0].toFixed(6)},${point_b[1].toFixed(6)}`,
|
|
748
|
+
)
|
|
749
|
+
if (
|
|
750
|
+
point_a_idx == null || point_b_idx == null || point_a_idx >= pts.length ||
|
|
751
|
+
point_b_idx >= pts.length
|
|
752
|
+
) {
|
|
753
|
+
console.warn(`get_2d_hull_edges: invalid edge`, {
|
|
754
|
+
point_a,
|
|
755
|
+
point_b,
|
|
756
|
+
point_a_idx,
|
|
757
|
+
point_b_idx,
|
|
758
|
+
})
|
|
759
|
+
continue
|
|
760
|
+
}
|
|
761
|
+
edges.push([pts[point_a_idx], pts[point_b_idx]])
|
|
546
762
|
}
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
763
|
+
|
|
764
|
+
return edges
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Build globally deduplicated edge geometry for domain boundaries using
|
|
768
|
+
// 3D convex hull crease edges (not 2D projected hull).
|
|
769
|
+
const edge_geometry = $derived.by(() => {
|
|
552
770
|
if (is_projection_mode) {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
// Fall back to per-domain edges below.
|
|
567
|
-
}
|
|
771
|
+
const all_points = render_domains
|
|
772
|
+
.filter((domain) => !domain.is_draw_formula)
|
|
773
|
+
.flatMap((domain) => domain.points_3d)
|
|
774
|
+
const unique_points = dedup_3d(all_points)
|
|
775
|
+
if (unique_points.length >= 4) {
|
|
776
|
+
try {
|
|
777
|
+
const hull_vectors = unique_points.map((point) => to_vec3(point))
|
|
778
|
+
const hull_geometry = new ConvexGeometry(hull_vectors)
|
|
779
|
+
const hull_edges = new THREE.EdgesGeometry(hull_geometry)
|
|
780
|
+
hull_geometry.dispose()
|
|
781
|
+
return hull_edges
|
|
782
|
+
} catch {
|
|
783
|
+
// Fall back to per-domain edges below.
|
|
568
784
|
}
|
|
785
|
+
}
|
|
569
786
|
}
|
|
570
|
-
|
|
571
|
-
const
|
|
787
|
+
|
|
788
|
+
const seen = new SvelteSet<string>()
|
|
789
|
+
const positions: number[] = []
|
|
572
790
|
for (const domain of render_domains) {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
|
|
594
|
-
const occlusion_hull_geometry = $derived.by(() => {
|
|
791
|
+
if (domain.is_draw_formula) continue
|
|
792
|
+
// Compute edges in swizzled (Three.js) coords since ConvexGeometry works there
|
|
793
|
+
const swizzled = domain.points_3d.map((point) => to_render_xyz(point))
|
|
794
|
+
for (const [pa, pb] of get_domain_edges(swizzled)) {
|
|
795
|
+
const ka = pa.map((v) => v.toFixed(4)).join(`,`)
|
|
796
|
+
const kb = pb.map((v) => v.toFixed(4)).join(`,`)
|
|
797
|
+
const key = ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`
|
|
798
|
+
if (seen.has(key)) continue
|
|
799
|
+
seen.add(key)
|
|
800
|
+
positions.push(pa[0], pa[1], pa[2], pb[0], pb[1], pb[2])
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
const geom = new THREE.BufferGeometry()
|
|
804
|
+
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(positions, 3))
|
|
805
|
+
return geom
|
|
806
|
+
})
|
|
807
|
+
|
|
808
|
+
// Build a single opaque convex hull mesh from ALL domain vertices for depth
|
|
809
|
+
// occlusion. This seamless surface writes to the depth buffer, hiding wireframe
|
|
810
|
+
// edges on the back side. Using all vertices together avoids gaps between domains.
|
|
811
|
+
const occlusion_hull_geometry = $derived.by((): THREE.BufferGeometry | null => {
|
|
595
812
|
try {
|
|
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
|
-
const hull_base_geometry = $derived.by(() => {
|
|
621
|
-
if (!occlusion_hull_geometry)
|
|
622
|
-
return null;
|
|
813
|
+
const all_points: number[][] = []
|
|
814
|
+
for (const domain of render_domains) {
|
|
815
|
+
if (domain.is_draw_formula) continue
|
|
816
|
+
all_points.push(...domain.points_3d)
|
|
817
|
+
}
|
|
818
|
+
const unique_points = dedup_3d(all_points)
|
|
819
|
+
if (unique_points.length < 4) return null
|
|
820
|
+
const vectors = unique_points.map((point) => to_vec3(point))
|
|
821
|
+
return merge_coplanar_geometry(new ConvexGeometry(vectors))
|
|
822
|
+
} catch {
|
|
823
|
+
return null
|
|
824
|
+
}
|
|
825
|
+
})
|
|
826
|
+
|
|
827
|
+
// Non-indexed hull geometry with artificial closing faces removed.
|
|
828
|
+
// The convex hull includes faces that close the diagram at the lower axis
|
|
829
|
+
// limits — flat walls and diagonal closing triangles. These are artificial
|
|
830
|
+
// (they depend on how far we extend the axes) and clutter the view.
|
|
831
|
+
// We detect them via their outward-pointing face normal: closing faces have
|
|
832
|
+
// normals pointing entirely toward the negative octant (all components ≤ 0),
|
|
833
|
+
// while meaningful domain boundaries always have at least one positive
|
|
834
|
+
// normal component (pointing toward 0 eV / the elemental reference).
|
|
835
|
+
const hull_base_geometry = $derived.by((): THREE.BufferGeometry | null => {
|
|
836
|
+
if (!occlusion_hull_geometry) return null
|
|
623
837
|
const src = occlusion_hull_geometry.index
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const pos = src.getAttribute(`position`)
|
|
627
|
-
const n_verts = pos.count
|
|
628
|
-
const n_faces = n_verts / 3
|
|
838
|
+
? occlusion_hull_geometry.toNonIndexed()
|
|
839
|
+
: occlusion_hull_geometry.clone()
|
|
840
|
+
const pos = src.getAttribute(`position`)
|
|
841
|
+
const n_verts = pos.count
|
|
842
|
+
const n_faces = n_verts / 3
|
|
629
843
|
// Hull centroid for orienting face normals outward
|
|
630
|
-
let hx = 0, hy = 0, hz = 0
|
|
844
|
+
let hx = 0, hy = 0, hz = 0
|
|
631
845
|
for (let vert_idx = 0; vert_idx < n_verts; vert_idx++) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
hx /= n_verts
|
|
637
|
-
hy /= n_verts
|
|
638
|
-
hz /= n_verts
|
|
639
|
-
const kept = []
|
|
846
|
+
hx += pos.getX(vert_idx)
|
|
847
|
+
hy += pos.getY(vert_idx)
|
|
848
|
+
hz += pos.getZ(vert_idx)
|
|
849
|
+
}
|
|
850
|
+
hx /= n_verts
|
|
851
|
+
hy /= n_verts
|
|
852
|
+
hz /= n_verts
|
|
853
|
+
const kept: number[] = []
|
|
640
854
|
for (let face_idx = 0; face_idx < n_faces; face_idx++) {
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
855
|
+
const base = face_idx * 3
|
|
856
|
+
const va: Vec3 = [pos.getX(base), pos.getY(base), pos.getZ(base)]
|
|
857
|
+
const vb: Vec3 = [pos.getX(base + 1), pos.getY(base + 1), pos.getZ(base + 1)]
|
|
858
|
+
const vc: Vec3 = [pos.getX(base + 2), pos.getY(base + 2), pos.getZ(base + 2)]
|
|
859
|
+
// Face normal via cross product of two edges
|
|
860
|
+
let normal = cross_3d(
|
|
861
|
+
[vb[0] - va[0], vb[1] - va[1], vb[2] - va[2]],
|
|
862
|
+
[vc[0] - va[0], vc[1] - va[1], vc[2] - va[2]],
|
|
863
|
+
)
|
|
864
|
+
// Orient outward (away from hull centroid)
|
|
865
|
+
const dx = (va[0] + vb[0] + vc[0]) / 3 - hx
|
|
866
|
+
const dy = (va[1] + vb[1] + vc[1]) / 3 - hy
|
|
867
|
+
const dz = (va[2] + vb[2] + vc[2]) / 3 - hz
|
|
868
|
+
if (normal[0] * dx + normal[1] * dy + normal[2] * dz < 0) {
|
|
869
|
+
normal = [-normal[0], -normal[1], -normal[2]]
|
|
870
|
+
}
|
|
871
|
+
// Closing faces point entirely toward negative octant (all ≤ 0).
|
|
872
|
+
// Meaningful domain faces always have at least one positive component.
|
|
873
|
+
if (normal[0] <= 0 && normal[1] <= 0 && normal[2] <= 0) continue
|
|
874
|
+
kept.push(...va, ...vb, ...vc)
|
|
659
875
|
}
|
|
660
876
|
// Re-merge coplanar faces after the filter — the closing-face removal
|
|
661
877
|
// can expose new coplanar adjacencies or leave fragments that should be
|
|
662
878
|
// merged into cleaner fan triangulations.
|
|
663
|
-
const merged = merge_coplanar_triangles(new Float32Array(kept))
|
|
664
|
-
const geom = new THREE.BufferGeometry()
|
|
665
|
-
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(merged, 3))
|
|
666
|
-
const colors = new Float32Array(merged.length).fill(0.965)
|
|
667
|
-
geom.setAttribute(`color`, new THREE.Float32BufferAttribute(colors, 3))
|
|
668
|
-
return geom
|
|
669
|
-
})
|
|
670
|
-
|
|
671
|
-
//
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const pos = hull_base_geometry.getAttribute(`position`)
|
|
676
|
-
const n_faces = pos.count / 3
|
|
879
|
+
const merged = merge_coplanar_triangles(new Float32Array(kept))
|
|
880
|
+
const geom = new THREE.BufferGeometry()
|
|
881
|
+
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(merged, 3))
|
|
882
|
+
const colors = new Float32Array(merged.length).fill(0.965)
|
|
883
|
+
geom.setAttribute(`color`, new THREE.Float32BufferAttribute(colors, 3))
|
|
884
|
+
return geom
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
// Per-face domain assignment (stable — only changes when geometry or domains change).
|
|
888
|
+
// Uses actual vertex centroid (mean of points_3d) for robust nearest-face matching.
|
|
889
|
+
const face_domain_map = $derived.by((): string[] => {
|
|
890
|
+
if (!hull_base_geometry) return []
|
|
891
|
+
const pos = hull_base_geometry.getAttribute(`position`)
|
|
892
|
+
const n_faces = pos.count / 3
|
|
893
|
+
|
|
677
894
|
// Domain vertex centroids in render coords (swizzled + axis stretch), matching hull_base_geometry.
|
|
678
895
|
const centroids = render_domains
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
let sx = 0, sy = 0, sz = 0
|
|
896
|
+
.filter((d) => !d.is_draw_formula && d.points_3d.length > 0)
|
|
897
|
+
.map((d) => {
|
|
898
|
+
let sx = 0, sy = 0, sz = 0
|
|
682
899
|
for (const pt of d.points_3d) {
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
900
|
+
const [x_val, y_val, z_val] = to_render_xyz(pt)
|
|
901
|
+
sx += x_val
|
|
902
|
+
sy += y_val
|
|
903
|
+
sz += z_val
|
|
687
904
|
}
|
|
688
|
-
const n = d.points_3d.length
|
|
689
|
-
return { formula: d.formula, cx: sx / n, cy: sy / n, cz: sz / n }
|
|
690
|
-
|
|
905
|
+
const n = d.points_3d.length
|
|
906
|
+
return { formula: d.formula, cx: sx / n, cy: sy / n, cz: sz / n }
|
|
907
|
+
})
|
|
908
|
+
|
|
691
909
|
// Assign each face to the nearest domain centroid
|
|
692
|
-
const result = []
|
|
910
|
+
const result: string[] = []
|
|
693
911
|
for (let face_idx = 0; face_idx < n_faces; face_idx++) {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
912
|
+
const base = face_idx * 3
|
|
913
|
+
const fcx = (pos.getX(base) + pos.getX(base + 1) + pos.getX(base + 2)) / 3
|
|
914
|
+
const fcy = (pos.getY(base) + pos.getY(base + 1) + pos.getY(base + 2)) / 3
|
|
915
|
+
const fcz = (pos.getZ(base) + pos.getZ(base + 1) + pos.getZ(base + 2)) / 3
|
|
916
|
+
let best_formula = ``
|
|
917
|
+
let best_dist = Infinity
|
|
918
|
+
for (const dc of centroids) {
|
|
919
|
+
const dist = (fcx - dc.cx) ** 2 + (fcy - dc.cy) ** 2 + (fcz - dc.cz) ** 2
|
|
920
|
+
if (dist < best_dist) {
|
|
921
|
+
best_dist = dist
|
|
922
|
+
best_formula = dc.formula
|
|
706
923
|
}
|
|
707
|
-
|
|
924
|
+
}
|
|
925
|
+
result.push(best_formula)
|
|
708
926
|
}
|
|
927
|
+
|
|
709
928
|
// Unify coplanar adjacent faces to the majority domain so that fan
|
|
710
929
|
// triangulation edges within a single hull face don't create visible
|
|
711
930
|
// color boundaries. Build adjacency via shared edge keys, group
|
|
712
931
|
// coplanar neighbors, then assign each group to its most-common domain.
|
|
713
932
|
if (n_faces > 1) {
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
933
|
+
const tol = 1e-3
|
|
934
|
+
const round = (v: number): number => Math.round(v / tol)
|
|
935
|
+
const vkey = (vert_idx: number): string =>
|
|
936
|
+
`${round(pos.getX(vert_idx))},${round(pos.getY(vert_idx))},${
|
|
937
|
+
round(pos.getZ(vert_idx))
|
|
938
|
+
}`
|
|
939
|
+
const ekey = (ka: string, kb: string): string =>
|
|
940
|
+
ka < kb ? `${ka}|${kb}` : `${kb}|${ka}`
|
|
941
|
+
// Compute face normals
|
|
942
|
+
const normals: Vec3[] = []
|
|
943
|
+
for (let face_idx = 0; face_idx < n_faces; face_idx++) {
|
|
944
|
+
const base = face_idx * 3
|
|
945
|
+
const e1: Vec3 = [
|
|
946
|
+
pos.getX(base + 1) - pos.getX(base),
|
|
947
|
+
pos.getY(base + 1) - pos.getY(base),
|
|
948
|
+
pos.getZ(base + 1) - pos.getZ(base),
|
|
949
|
+
]
|
|
950
|
+
const e2: Vec3 = [
|
|
951
|
+
pos.getX(base + 2) - pos.getX(base),
|
|
952
|
+
pos.getY(base + 2) - pos.getY(base),
|
|
953
|
+
pos.getZ(base + 2) - pos.getZ(base),
|
|
954
|
+
]
|
|
955
|
+
normals.push(normalize_vec3(cross_3d(e1, e2)))
|
|
956
|
+
}
|
|
957
|
+
// Build edge → face adjacency
|
|
958
|
+
const edge_faces = new SvelteMap<string, number[]>()
|
|
959
|
+
for (let face_idx = 0; face_idx < n_faces; face_idx++) {
|
|
960
|
+
const base = face_idx * 3
|
|
961
|
+
const keys = [vkey(base), vkey(base + 1), vkey(base + 2)]
|
|
962
|
+
for (
|
|
963
|
+
const ek of [
|
|
964
|
+
ekey(keys[0], keys[1]),
|
|
965
|
+
ekey(keys[1], keys[2]),
|
|
966
|
+
ekey(keys[0], keys[2]),
|
|
967
|
+
]
|
|
968
|
+
) {
|
|
969
|
+
const list = edge_faces.get(ek)
|
|
970
|
+
if (list) list.push(face_idx)
|
|
971
|
+
else edge_faces.set(ek, [face_idx])
|
|
733
972
|
}
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
ekey(keys[1], keys[2]),
|
|
742
|
-
ekey(keys[0], keys[2]),
|
|
743
|
-
]) {
|
|
744
|
-
const list = edge_faces.get(ek);
|
|
745
|
-
if (list)
|
|
746
|
-
list.push(face_idx);
|
|
747
|
-
else
|
|
748
|
-
edge_faces.set(ek, [face_idx]);
|
|
749
|
-
}
|
|
973
|
+
}
|
|
974
|
+
// Union-find for coplanar adjacent faces
|
|
975
|
+
const parent = Array.from({ length: n_faces }, (_, idx) => idx)
|
|
976
|
+
const find = (x: number): number => {
|
|
977
|
+
while (parent[x] !== x) {
|
|
978
|
+
parent[x] = parent[parent[x]]
|
|
979
|
+
x = parent[x]
|
|
750
980
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
const
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
parent[ra] = rb;
|
|
764
|
-
};
|
|
765
|
-
for (const pair of edge_faces.values()) {
|
|
766
|
-
if (pair.length !== 2)
|
|
767
|
-
continue;
|
|
768
|
-
const [fa, fb] = pair;
|
|
769
|
-
const na = normals[fa], nb = normals[fb];
|
|
770
|
-
if (Math.abs(na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2]) > 1 - tol) {
|
|
771
|
-
union(fa, fb);
|
|
772
|
-
}
|
|
981
|
+
return x
|
|
982
|
+
}
|
|
983
|
+
const union = (a_idx: number, b_idx: number): void => {
|
|
984
|
+
const ra = find(a_idx), rb = find(b_idx)
|
|
985
|
+
if (ra !== rb) parent[ra] = rb
|
|
986
|
+
}
|
|
987
|
+
for (const pair of edge_faces.values()) {
|
|
988
|
+
if (pair.length !== 2) continue
|
|
989
|
+
const [fa, fb] = pair
|
|
990
|
+
const na = normals[fa], nb = normals[fb]
|
|
991
|
+
if (Math.abs(na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2]) > 1 - tol) {
|
|
992
|
+
union(fa, fb)
|
|
773
993
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
994
|
+
}
|
|
995
|
+
// Assign majority domain to each coplanar group
|
|
996
|
+
const groups = new SvelteMap<number, number[]>()
|
|
997
|
+
for (let face_idx = 0; face_idx < n_faces; face_idx++) {
|
|
998
|
+
const root = find(face_idx)
|
|
999
|
+
const grp = groups.get(root)
|
|
1000
|
+
if (grp) grp.push(face_idx)
|
|
1001
|
+
else groups.set(root, [face_idx])
|
|
1002
|
+
}
|
|
1003
|
+
for (const members of groups.values()) {
|
|
1004
|
+
if (members.length < 2) continue
|
|
1005
|
+
// Find most common domain in this group
|
|
1006
|
+
const counts = new SvelteMap<string, number>()
|
|
1007
|
+
for (const member_idx of members) {
|
|
1008
|
+
counts.set(result[member_idx], (counts.get(result[member_idx]) ?? 0) + 1)
|
|
783
1009
|
}
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
}
|
|
792
|
-
let majority = result[members[0]];
|
|
793
|
-
let max_count = 0;
|
|
794
|
-
for (const [formula, count] of counts) {
|
|
795
|
-
if (count > max_count) {
|
|
796
|
-
max_count = count;
|
|
797
|
-
majority = formula;
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
for (const member_idx of members)
|
|
801
|
-
result[member_idx] = majority;
|
|
1010
|
+
let majority = result[members[0]]
|
|
1011
|
+
let max_count = 0
|
|
1012
|
+
for (const [formula, count] of counts) {
|
|
1013
|
+
if (count > max_count) {
|
|
1014
|
+
max_count = count
|
|
1015
|
+
majority = formula
|
|
1016
|
+
}
|
|
802
1017
|
}
|
|
1018
|
+
for (const member_idx of members) result[member_idx] = majority
|
|
1019
|
+
}
|
|
803
1020
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
const
|
|
1021
|
+
|
|
1022
|
+
return result
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
// Reactive color fill: creates a cloned geometry with vertex colors applied.
|
|
1026
|
+
// Only runs when color_mode or domain_colors change — no mutation of hull_base_geometry.
|
|
1027
|
+
const colored_hull_geometry = $derived.by((): THREE.BufferGeometry | null => {
|
|
1028
|
+
const mapping = face_domain_map
|
|
1029
|
+
if (!hull_base_geometry || mapping.length === 0) return hull_base_geometry
|
|
1030
|
+
|
|
1031
|
+
const geom = hull_base_geometry.clone()
|
|
1032
|
+
const color_attr = geom.getAttribute(`color`) as THREE.BufferAttribute
|
|
1033
|
+
const use_colors = color_mode !== `none` && domain_colors.size > 0
|
|
815
1034
|
const fb = use_colors
|
|
816
|
-
|
|
817
|
-
|
|
1035
|
+
? [0.91, 0.91, 0.91] // #e8e8e8
|
|
1036
|
+
: [0.965, 0.965, 0.965] // #f6f6f6
|
|
1037
|
+
|
|
818
1038
|
// Cache parsed RGB per formula to avoid redundant THREE.Color allocations
|
|
819
|
-
const rgb_cache = new SvelteMap()
|
|
1039
|
+
const rgb_cache = new SvelteMap<string, Vec3>()
|
|
820
1040
|
for (const [formula, hex] of domain_colors) {
|
|
821
|
-
|
|
822
|
-
|
|
1041
|
+
const clr = new THREE.Color(hex)
|
|
1042
|
+
rgb_cache.set(formula, [clr.r, clr.g, clr.b])
|
|
823
1043
|
}
|
|
1044
|
+
|
|
824
1045
|
for (let face_idx = 0; face_idx < mapping.length; face_idx++) {
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
1046
|
+
const rgb = use_colors ? rgb_cache.get(mapping[face_idx]) : null
|
|
1047
|
+
const [red, green, blue] = rgb ?? fb
|
|
1048
|
+
const base = face_idx * 3
|
|
1049
|
+
for (let vert_idx = 0; vert_idx < 3; vert_idx++) {
|
|
1050
|
+
color_attr.setXYZ(base + vert_idx, red, green, blue)
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
color_attr.needsUpdate = true
|
|
1054
|
+
return geom
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
const visible_domain_labels = $derived.by(() => {
|
|
1058
|
+
if (!hull_base_geometry || face_domain_map.length === 0) {
|
|
1059
|
+
return render_domains.map((domain) => ({
|
|
1060
|
+
formula: domain.formula,
|
|
1061
|
+
position: swiz(domain.ann_loc[0], domain.ann_loc[1], domain.ann_loc[2]),
|
|
1062
|
+
label_font_size: domain.label_font_size,
|
|
1063
|
+
}))
|
|
831
1064
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1065
|
+
|
|
1066
|
+
const pos = hull_base_geometry.getAttribute(`position`)
|
|
1067
|
+
const pinned_labels = render_domains
|
|
1068
|
+
.filter((domain) => domain.is_draw_formula)
|
|
1069
|
+
.map((domain) => ({
|
|
1070
|
+
formula: domain.formula,
|
|
1071
|
+
position: swiz(domain.ann_loc[0], domain.ann_loc[1], domain.ann_loc[2]),
|
|
1072
|
+
label_font_size: domain.label_font_size,
|
|
1073
|
+
}))
|
|
1074
|
+
const font_size_by_formula = new SvelteMap(
|
|
1075
|
+
render_domains.map((domain) => [domain.formula, domain.label_font_size]),
|
|
1076
|
+
)
|
|
1077
|
+
return get_visible_domain_labels(
|
|
1078
|
+
pos.array,
|
|
1079
|
+
face_domain_map,
|
|
1080
|
+
font_size_by_formula,
|
|
1081
|
+
pinned_labels,
|
|
1082
|
+
)
|
|
1083
|
+
})
|
|
1084
|
+
|
|
1085
|
+
$effect(() => {
|
|
1086
|
+
const geom = hull_base_geometry
|
|
1087
|
+
return () => dispose_geometry(geom)
|
|
1088
|
+
})
|
|
1089
|
+
|
|
1090
|
+
$effect(() => {
|
|
1091
|
+
const geom = colored_hull_geometry
|
|
841
1092
|
// Don't dispose if it's the same object as hull_base_geometry (no clone was made)
|
|
842
|
-
if (geom && geom !== hull_base_geometry)
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
// Domains on the outer surface
|
|
846
|
-
|
|
847
|
-
const
|
|
848
|
-
const on_surface = new SvelteSet();
|
|
1093
|
+
if (geom && geom !== hull_base_geometry) return () => dispose_geometry(geom)
|
|
1094
|
+
})
|
|
1095
|
+
|
|
1096
|
+
// Domains on the outer surface (used by the "Surface" formula overlay quick-select).
|
|
1097
|
+
const surface_formulas = $derived.by((): SvelteSet<string> => {
|
|
1098
|
+
const on_surface = new SvelteSet<string>()
|
|
849
1099
|
if (!occlusion_hull_geometry) {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
return on_surface;
|
|
1100
|
+
for (const domain of render_domains) on_surface.add(domain.formula)
|
|
1101
|
+
return on_surface
|
|
853
1102
|
}
|
|
854
1103
|
// Raycast from each domain's centroid outward -- if it hits the hull,
|
|
855
1104
|
// the centroid is inside (interior domain). Use multiple ray directions
|
|
856
1105
|
// and count: if most hit, the point is interior.
|
|
857
|
-
const raycaster = new THREE.Raycaster()
|
|
858
|
-
const hull_mesh = new THREE.Mesh(occlusion_hull_geometry)
|
|
1106
|
+
const raycaster = new THREE.Raycaster()
|
|
1107
|
+
const hull_mesh = new THREE.Mesh(occlusion_hull_geometry)
|
|
859
1108
|
const directions = [
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
]
|
|
1109
|
+
new THREE.Vector3(1, 0, 0),
|
|
1110
|
+
new THREE.Vector3(0, 1, 0),
|
|
1111
|
+
new THREE.Vector3(0, 0, 1),
|
|
1112
|
+
new THREE.Vector3(-1, 0, 0),
|
|
1113
|
+
new THREE.Vector3(0, -1, 0),
|
|
1114
|
+
new THREE.Vector3(0, 0, -1),
|
|
1115
|
+
]
|
|
867
1116
|
for (const domain of render_domains) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const controls_series = $derived([
|
|
1117
|
+
if (domain.is_draw_formula) {
|
|
1118
|
+
on_surface.add(domain.formula)
|
|
1119
|
+
continue
|
|
1120
|
+
}
|
|
1121
|
+
const origin = to_vec3(domain.ann_loc)
|
|
1122
|
+
// Count how many rays hit the hull from the centroid
|
|
1123
|
+
let hits = 0
|
|
1124
|
+
for (const dir of directions) {
|
|
1125
|
+
raycaster.set(origin, dir)
|
|
1126
|
+
if (raycaster.intersectObject(hull_mesh).length > 0) hits++
|
|
1127
|
+
}
|
|
1128
|
+
// If fewer than 4 of 6 rays hit, centroid is on or near the surface
|
|
1129
|
+
if (hits < 4) on_surface.add(domain.formula)
|
|
1130
|
+
}
|
|
1131
|
+
return on_surface
|
|
1132
|
+
})
|
|
1133
|
+
|
|
1134
|
+
// Deduplicate 3D points within tolerance (reuses compute.ts dedup_points)
|
|
1135
|
+
function dedup_3d(pts: number[][], tol: number = 1e-4): number[][] {
|
|
1136
|
+
return dedup_points(pts, tol).unique
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
const controls_series = $derived<DataSeries3D[]>([
|
|
891
1140
|
{
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1141
|
+
x: render_domains.flatMap((domain) =>
|
|
1142
|
+
domain.points_3d.map((point) => point[1])
|
|
1143
|
+
),
|
|
1144
|
+
y: render_domains.flatMap((domain) =>
|
|
1145
|
+
domain.points_3d.map((point) => point[2])
|
|
1146
|
+
),
|
|
1147
|
+
z: render_domains.flatMap((domain) =>
|
|
1148
|
+
domain.points_3d.map((point) => point[0])
|
|
1149
|
+
),
|
|
1150
|
+
label: `domains`,
|
|
896
1151
|
},
|
|
897
|
-
])
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
const result = []
|
|
1152
|
+
])
|
|
1153
|
+
|
|
1154
|
+
// Build formula overlay edge geometries (per formula, colored) using crease edges
|
|
1155
|
+
const formula_edge_data = $derived.by(() => {
|
|
1156
|
+
if (!draw_formula_lines || formulas_to_draw.length === 0) return []
|
|
1157
|
+
const result: { geometry: THREE.BufferGeometry; color: string }[] = []
|
|
903
1158
|
for (const domain of render_domains) {
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
// Build formula overlay mesh geometries (convex hull surface)
|
|
920
|
-
const formula_mesh_data = $derived.by(() => {
|
|
921
|
-
const result = []
|
|
922
|
-
if (!draw_formula_meshes)
|
|
923
|
-
return result;
|
|
1159
|
+
if (!domain.is_draw_formula) continue
|
|
1160
|
+
const color_idx = formulas_to_draw.indexOf(domain.formula) %
|
|
1161
|
+
formula_colors.length
|
|
1162
|
+
const swizzled = domain.points_3d.map((point) => to_render_xyz(point))
|
|
1163
|
+
const positions: number[] = []
|
|
1164
|
+
for (const [pa, pb] of get_domain_edges(swizzled)) {
|
|
1165
|
+
positions.push(pa[0], pa[1], pa[2], pb[0], pb[1], pb[2])
|
|
1166
|
+
}
|
|
1167
|
+
const geom = new THREE.BufferGeometry()
|
|
1168
|
+
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(positions, 3))
|
|
1169
|
+
result.push({ geometry: geom, color: formula_colors[color_idx] })
|
|
1170
|
+
}
|
|
1171
|
+
return result
|
|
1172
|
+
})
|
|
1173
|
+
|
|
1174
|
+
// Build formula overlay mesh geometries (convex hull surface)
|
|
1175
|
+
const formula_mesh_data = $derived.by(() => {
|
|
1176
|
+
const result: { geometry: THREE.BufferGeometry; color: string }[] = []
|
|
1177
|
+
if (!draw_formula_meshes) return result
|
|
924
1178
|
for (const domain of render_domains) {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1179
|
+
if (!domain.is_draw_formula || domain.points_3d.length < 4) continue
|
|
1180
|
+
const color_idx = formulas_to_draw.indexOf(domain.formula) %
|
|
1181
|
+
formula_colors.length
|
|
1182
|
+
const unique = dedup_3d(domain.points_3d)
|
|
1183
|
+
if (unique.length < 4) continue
|
|
1184
|
+
const vectors = unique.map((pt) => to_vec3(pt))
|
|
1185
|
+
try {
|
|
1186
|
+
const geom = merge_coplanar_geometry(new ConvexGeometry(vectors))
|
|
1187
|
+
result.push({ geometry: geom, color: formula_colors[color_idx] })
|
|
1188
|
+
} catch {
|
|
1189
|
+
// Degenerate hull, skip
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
return result
|
|
1193
|
+
})
|
|
1194
|
+
|
|
1195
|
+
function get_touches_limits(
|
|
1196
|
+
points_3d: number[][],
|
|
1197
|
+
lims: [number, number][],
|
|
1198
|
+
): string[] {
|
|
1199
|
+
const limit_tol = 1e-3
|
|
1200
|
+
const touches_limits: string[] = []
|
|
1201
|
+
for (
|
|
1202
|
+
let axis_idx = 0;
|
|
1203
|
+
axis_idx < Math.min(plot_elements.length, lims.length);
|
|
1204
|
+
axis_idx++
|
|
1205
|
+
) {
|
|
1206
|
+
const [axis_min, axis_max] = lims[axis_idx]
|
|
1207
|
+
const axis_name = plot_elements[axis_idx] ?? `axis_${axis_idx}`
|
|
1208
|
+
const touches_min = points_3d.some((point) =>
|
|
1209
|
+
Math.abs(point[axis_idx] - axis_min) < limit_tol
|
|
1210
|
+
)
|
|
1211
|
+
const touches_max = points_3d.some((point) =>
|
|
1212
|
+
Math.abs(point[axis_idx] - axis_max) < limit_tol
|
|
1213
|
+
)
|
|
1214
|
+
if (touches_min) touches_limits.push(`${axis_name} lower bound`)
|
|
1215
|
+
if (touches_max) touches_limits.push(`${axis_name} upper bound`)
|
|
1216
|
+
}
|
|
1217
|
+
return touches_limits
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Post-process ConvexGeometry to merge coplanar triangles, eliminating
|
|
1221
|
+
// internal diagonal edges across flat faces of the convex hull.
|
|
1222
|
+
function merge_coplanar_geometry(geom: THREE.BufferGeometry): THREE.BufferGeometry {
|
|
1223
|
+
const non_indexed = geom.index ? geom.toNonIndexed() : geom
|
|
1224
|
+
const pos = non_indexed.getAttribute(`position`)
|
|
1225
|
+
const merged = merge_coplanar_triangles(pos.array as Float32Array)
|
|
1226
|
+
const result = new THREE.BufferGeometry()
|
|
1227
|
+
result.setAttribute(`position`, new THREE.Float32BufferAttribute(merged, 3))
|
|
1228
|
+
result.computeVertexNormals()
|
|
967
1229
|
// Dispose intermediate geometry from toNonIndexed() (avoid double-dispose if same object)
|
|
968
|
-
if (non_indexed !== geom)
|
|
969
|
-
non_indexed.dispose();
|
|
1230
|
+
if (non_indexed !== geom) non_indexed.dispose()
|
|
970
1231
|
// Callers always pass a freshly created ConvexGeometry, so we own it
|
|
971
|
-
geom.dispose()
|
|
972
|
-
return result
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1232
|
+
geom.dispose()
|
|
1233
|
+
return result
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function create_hover_geometry(
|
|
1237
|
+
points_3d: number[][],
|
|
1238
|
+
): { geometry: THREE.BufferGeometry; n_vertices: number } | null {
|
|
1239
|
+
const unique_points = dedup_3d(points_3d)
|
|
1240
|
+
if (unique_points.length < 3) return null
|
|
978
1241
|
// For exactly 3 unique points (planar/degenerate domain), create a triangle
|
|
979
1242
|
// geometry directly since ConvexGeometry requires 4+ points for a 3D hull
|
|
980
1243
|
if (unique_points.length === 3) {
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1244
|
+
const geom = new THREE.BufferGeometry()
|
|
1245
|
+
const vectors = unique_points.map((pt) => to_vec3(pt))
|
|
1246
|
+
const verts = new Float32Array(vectors.flatMap((v) => [v.x, v.y, v.z]))
|
|
1247
|
+
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(verts, 3))
|
|
1248
|
+
geom.setIndex([0, 1, 2, 2, 1, 0]) // both winding orders for double-sided pick
|
|
1249
|
+
geom.computeVertexNormals()
|
|
1250
|
+
return { geometry: geom, n_vertices: 3 }
|
|
988
1251
|
}
|
|
989
1252
|
try {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
}
|
|
999
|
-
// Domain adjacency: two domains are neighbors if they share any vertex (within tolerance)
|
|
1000
|
-
const domain_neighbors = $derived.by(() => {
|
|
1001
|
-
const tol = 1e-4;
|
|
1002
|
-
const vertex_owners = new SvelteMap();
|
|
1003
|
-
for (const domain of render_domains) {
|
|
1004
|
-
for (const pt of domain.points_3d) {
|
|
1005
|
-
const key = pt.map((val) => (Math.round(val / tol) * tol).toFixed(4)).join(`,`);
|
|
1006
|
-
const owners = vertex_owners.get(key);
|
|
1007
|
-
if (owners) {
|
|
1008
|
-
if (!owners.includes(domain.formula))
|
|
1009
|
-
owners.push(domain.formula);
|
|
1010
|
-
}
|
|
1011
|
-
else
|
|
1012
|
-
vertex_owners.set(key, [domain.formula]);
|
|
1013
|
-
}
|
|
1253
|
+
return {
|
|
1254
|
+
geometry: merge_coplanar_geometry(
|
|
1255
|
+
new ConvexGeometry(unique_points.map((point) => to_vec3(point))),
|
|
1256
|
+
),
|
|
1257
|
+
n_vertices: unique_points.length,
|
|
1258
|
+
}
|
|
1259
|
+
} catch {
|
|
1260
|
+
return null
|
|
1014
1261
|
}
|
|
1015
|
-
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Domain adjacency: two domains are neighbors if they share any vertex (within tolerance)
|
|
1265
|
+
const domain_neighbors = $derived.by((): SvelteMap<string, string[]> => {
|
|
1266
|
+
const tol = 1e-4
|
|
1267
|
+
const vertex_owners = new SvelteMap<string, string[]>()
|
|
1268
|
+
for (const domain of render_domains) {
|
|
1269
|
+
for (const pt of domain.points_3d) {
|
|
1270
|
+
const key = pt.map((val) => (Math.round(val / tol) * tol).toFixed(4)).join(
|
|
1271
|
+
`,`,
|
|
1272
|
+
)
|
|
1273
|
+
const owners = vertex_owners.get(key)
|
|
1274
|
+
if (owners) {
|
|
1275
|
+
if (!owners.includes(domain.formula)) owners.push(domain.formula)
|
|
1276
|
+
} else vertex_owners.set(key, [domain.formula])
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
const neighbors = new SvelteMap<string, SvelteSet<string>>()
|
|
1016
1280
|
for (const domain of render_domains) {
|
|
1017
|
-
|
|
1281
|
+
neighbors.set(domain.formula, new SvelteSet())
|
|
1018
1282
|
}
|
|
1019
1283
|
for (const owners of vertex_owners.values()) {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
for (let
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
neighbors.get(owners[jdx])?.add(owners[idx]);
|
|
1026
|
-
}
|
|
1284
|
+
if (owners.length < 2) continue
|
|
1285
|
+
for (let idx = 0; idx < owners.length; idx++) {
|
|
1286
|
+
for (let jdx = idx + 1; jdx < owners.length; jdx++) {
|
|
1287
|
+
neighbors.get(owners[idx])?.add(owners[jdx])
|
|
1288
|
+
neighbors.get(owners[jdx])?.add(owners[idx])
|
|
1027
1289
|
}
|
|
1290
|
+
}
|
|
1028
1291
|
}
|
|
1029
|
-
const result = new SvelteMap()
|
|
1030
|
-
for (const [formula, set] of neighbors)
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const hover_mesh_data = $derived.by(() => {
|
|
1035
|
-
if (!diagram_data)
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1292
|
+
const result = new SvelteMap<string, string[]>()
|
|
1293
|
+
for (const [formula, set] of neighbors) result.set(formula, [...set].sort())
|
|
1294
|
+
return result
|
|
1295
|
+
})
|
|
1296
|
+
|
|
1297
|
+
const hover_mesh_data = $derived.by((): HoverMeshData[] => {
|
|
1298
|
+
if (!diagram_data) return []
|
|
1299
|
+
const result: HoverMeshData[] = []
|
|
1300
|
+
const lims = diagram_data.lims
|
|
1301
|
+
const energy_stats_by_formula = entry_energy_stats_by_formula
|
|
1302
|
+
|
|
1040
1303
|
for (const domain of render_domains) {
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
geometry
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
const geometry
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
$effect(() => {
|
|
1094
|
-
const geometry =
|
|
1095
|
-
return () => dispose_geometry(geometry)
|
|
1096
|
-
})
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
const
|
|
1120
|
-
|
|
1304
|
+
if (domain.points_3d.length < 3) continue
|
|
1305
|
+
const hover_geometry = create_hover_geometry(domain.points_3d)
|
|
1306
|
+
if (!hover_geometry) continue
|
|
1307
|
+
const { geometry, n_vertices } = hover_geometry
|
|
1308
|
+
|
|
1309
|
+
const swizzled_points = domain.points_3d.map((point) => to_render_xyz(point))
|
|
1310
|
+
const edge_count = get_domain_edges(swizzled_points).length
|
|
1311
|
+
const axis_ranges = build_axis_ranges(domain.points_3d, plot_elements)
|
|
1312
|
+
const touches_limits = get_touches_limits(domain.points_3d, lims)
|
|
1313
|
+
const energy_stats = energy_stats_by_formula.get(domain.formula) ?? {
|
|
1314
|
+
matching_entry_count: 0,
|
|
1315
|
+
min_energy_per_atom: null,
|
|
1316
|
+
max_energy_per_atom: null,
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const info: ChemPotHoverInfo3D = {
|
|
1320
|
+
formula: domain.formula,
|
|
1321
|
+
view: `3d`,
|
|
1322
|
+
n_vertices,
|
|
1323
|
+
n_edges: edge_count,
|
|
1324
|
+
n_points: domain.points_3d.length,
|
|
1325
|
+
ann_loc: domain.ann_loc,
|
|
1326
|
+
axis_ranges,
|
|
1327
|
+
touches_limits,
|
|
1328
|
+
is_elemental: all_entry_elements.includes(domain.formula),
|
|
1329
|
+
is_draw_formula: domain.is_draw_formula,
|
|
1330
|
+
matching_entry_count: energy_stats.matching_entry_count,
|
|
1331
|
+
min_energy_per_atom: energy_stats.min_energy_per_atom,
|
|
1332
|
+
max_energy_per_atom: energy_stats.max_energy_per_atom,
|
|
1333
|
+
neighbors: domain_neighbors.get(domain.formula) ?? [],
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
result.push({
|
|
1337
|
+
formula: domain.formula,
|
|
1338
|
+
geometry,
|
|
1339
|
+
info,
|
|
1340
|
+
})
|
|
1341
|
+
}
|
|
1342
|
+
return result
|
|
1343
|
+
})
|
|
1344
|
+
|
|
1345
|
+
function dispose_geometry(geometry: THREE.BufferGeometry | null | undefined): void {
|
|
1346
|
+
if (!geometry) return
|
|
1347
|
+
geometry.dispose()
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
function dispose_geometries(
|
|
1351
|
+
geometries: (THREE.BufferGeometry | null | undefined)[],
|
|
1352
|
+
): void {
|
|
1353
|
+
for (const geometry of geometries) dispose_geometry(geometry)
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
$effect(() => {
|
|
1357
|
+
const geometry = edge_geometry
|
|
1358
|
+
return () => dispose_geometry(geometry)
|
|
1359
|
+
})
|
|
1360
|
+
|
|
1361
|
+
$effect(() => {
|
|
1362
|
+
const geometry = occlusion_hull_geometry
|
|
1363
|
+
return () => dispose_geometry(geometry)
|
|
1364
|
+
})
|
|
1365
|
+
|
|
1366
|
+
$effect(() => {
|
|
1367
|
+
const geometry = bounding_box_geometry
|
|
1368
|
+
return () => dispose_geometry(geometry)
|
|
1369
|
+
})
|
|
1370
|
+
|
|
1371
|
+
$effect(() => {
|
|
1372
|
+
const geometries = formula_edge_data.map((data) => data.geometry)
|
|
1373
|
+
return () => dispose_geometries(geometries)
|
|
1374
|
+
})
|
|
1375
|
+
|
|
1376
|
+
$effect(() => {
|
|
1377
|
+
const geometries = formula_mesh_data.map((data) => data.geometry)
|
|
1378
|
+
return () => dispose_geometries(geometries)
|
|
1379
|
+
})
|
|
1380
|
+
|
|
1381
|
+
$effect(() => {
|
|
1382
|
+
const geometries = hover_mesh_data.map((data) => data.geometry)
|
|
1383
|
+
return () => dispose_geometries(geometries)
|
|
1384
|
+
})
|
|
1385
|
+
|
|
1386
|
+
// === Grid, axes, ticks (matching ScatterPlot3D style) ===
|
|
1387
|
+
|
|
1388
|
+
// Bounding box of all data points in DATA coordinates (before swizzle)
|
|
1389
|
+
const raw_data_bbox = $derived.by(() => {
|
|
1390
|
+
const pts = render_domains.flatMap((d) => d.points_3d)
|
|
1391
|
+
if (pts.length === 0) return { mins: [0, 0, 0], maxs: [1, 1, 1] }
|
|
1392
|
+
const mins = [Infinity, Infinity, Infinity]
|
|
1393
|
+
const maxs = [-Infinity, -Infinity, -Infinity]
|
|
1121
1394
|
for (const pt of pts) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
maxs[dim] = pt[dim];
|
|
1127
|
-
}
|
|
1395
|
+
for (let dim = 0; dim < 3; dim++) {
|
|
1396
|
+
if (pt[dim] < mins[dim]) mins[dim] = pt[dim]
|
|
1397
|
+
if (pt[dim] > maxs[dim]) maxs[dim] = pt[dim]
|
|
1398
|
+
}
|
|
1128
1399
|
}
|
|
1129
|
-
return { mins, maxs }
|
|
1130
|
-
})
|
|
1131
|
-
|
|
1132
|
-
//
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
const
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1400
|
+
return { mins, maxs }
|
|
1401
|
+
})
|
|
1402
|
+
|
|
1403
|
+
// Axis range controls are in swizzled axis order:
|
|
1404
|
+
// x-axis control -> data axis 1, y-axis control -> data axis 2, z-axis control -> data axis 0
|
|
1405
|
+
const data_bbox = $derived.by(() => {
|
|
1406
|
+
const mins = [...raw_data_bbox.mins]
|
|
1407
|
+
const maxs = [...raw_data_bbox.maxs]
|
|
1408
|
+
const range_by_data_axis: ([number | null, number | null] | undefined)[] = [
|
|
1409
|
+
z_axis.range,
|
|
1410
|
+
x_axis.range,
|
|
1411
|
+
y_axis.range,
|
|
1412
|
+
]
|
|
1141
1413
|
for (let axis_idx = 0; axis_idx < 3; axis_idx++) {
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
// Generate nice tick values for each data axis using D3
|
|
1154
|
-
function gen_ticks(min_val, max_val, count = 5) {
|
|
1414
|
+
const range = range_by_data_axis[axis_idx]
|
|
1415
|
+
if (!range) continue
|
|
1416
|
+
const [range_min, range_max] = range
|
|
1417
|
+
if (range_min !== null) mins[axis_idx] = range_min
|
|
1418
|
+
if (range_max !== null) maxs[axis_idx] = range_max
|
|
1419
|
+
}
|
|
1420
|
+
return { mins, maxs }
|
|
1421
|
+
})
|
|
1422
|
+
|
|
1423
|
+
// Generate nice tick values for each data axis using D3
|
|
1424
|
+
function gen_ticks(min_val: number, max_val: number, count: number = 5): number[] {
|
|
1155
1425
|
if (!isFinite(min_val) || !isFinite(max_val) || min_val === max_val) {
|
|
1156
|
-
|
|
1426
|
+
return [min_val]
|
|
1157
1427
|
}
|
|
1158
|
-
return scaleLinear().domain([min_val, max_val]).nice().ticks(count)
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1428
|
+
return scaleLinear().domain([min_val, max_val]).nice().ticks(count)
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
// Ticks in DATA coordinates for each of the 3 data axes
|
|
1432
|
+
const data_ticks = $derived([
|
|
1162
1433
|
gen_ticks(data_bbox.mins[0], data_bbox.maxs[0]),
|
|
1163
1434
|
gen_ticks(data_bbox.mins[1], data_bbox.maxs[1]),
|
|
1164
1435
|
gen_ticks(data_bbox.mins[2], data_bbox.maxs[2]),
|
|
1165
|
-
])
|
|
1166
|
-
|
|
1167
|
-
//
|
|
1168
|
-
// For
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
})
|
|
1182
|
-
})
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
const
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
//
|
|
1217
|
-
//
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1436
|
+
])
|
|
1437
|
+
|
|
1438
|
+
// Niced ranges (from ticks) padded so the grid extends beyond the diagram.
|
|
1439
|
+
// For horizontal axes (0,1): pad both sides.
|
|
1440
|
+
// For vertical axis (2): use actual data range and round min down to an integer.
|
|
1441
|
+
const niced_range = $derived.by(() => {
|
|
1442
|
+
return [0, 1, 2].map((axis): Vec2 => {
|
|
1443
|
+
const ticks = data_ticks[axis]
|
|
1444
|
+
const lo = ticks[0]
|
|
1445
|
+
const hi = ticks.at(-1) ?? lo
|
|
1446
|
+
const step = ticks.length > 1 ? ticks[1] - ticks[0] : 1
|
|
1447
|
+
if (axis === 2) {
|
|
1448
|
+
const min_data = data_bbox.mins[2]
|
|
1449
|
+
return [Math.floor(min_data), hi]
|
|
1450
|
+
}
|
|
1451
|
+
return [lo - step, hi + step]
|
|
1452
|
+
})
|
|
1453
|
+
})
|
|
1454
|
+
|
|
1455
|
+
// Helper to create a line geometry from two Vec3 arrays
|
|
1456
|
+
function make_line_geom(
|
|
1457
|
+
start: Vec3,
|
|
1458
|
+
end: Vec3,
|
|
1459
|
+
): THREE.BufferGeometry {
|
|
1460
|
+
const geom = new THREE.BufferGeometry()
|
|
1461
|
+
geom.setAttribute(
|
|
1462
|
+
`position`,
|
|
1463
|
+
new THREE.BufferAttribute(new Float32Array([...start, ...end]), 3),
|
|
1464
|
+
)
|
|
1465
|
+
return geom
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Swizzle a data-coord triple to Three.js coords
|
|
1469
|
+
function swiz(d0: number, d1: number, d2: number): Vec3 {
|
|
1470
|
+
const [scale_x, scale_y, scale_z] = render_axis_scale
|
|
1471
|
+
return [d1 * scale_x, d2 * scale_y, d0 * scale_z] // data[0]→Z, data[1]→X, data[2]→Y
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const axis_colors = [`#e74c3c`, `#2ecc71`, `#3498db`] as const
|
|
1475
|
+
function chem_axis_label(data_axis: number): string {
|
|
1476
|
+
const el = plot_elements[data_axis]
|
|
1477
|
+
const prefix = formal_chempots ? `\u0394` : ``
|
|
1478
|
+
return `${prefix}\u03BC<sub>${el}</sub> <span class="axis-unit">(eV)</span>`
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// Proportional offsets for tick marks and labels, scaled to data extent
|
|
1482
|
+
const tick_size = $derived(data_extent * 0.015)
|
|
1483
|
+
const tick_label_dist = $derived(data_extent * 0.04)
|
|
1484
|
+
const axis_label_dist = $derived(data_extent * 0.02)
|
|
1485
|
+
|
|
1486
|
+
// Place axis label just past the outer end of the axis (the end closer to 0).
|
|
1487
|
+
// In isometric 3D, the end near 0 projects outward at the front edge of the
|
|
1488
|
+
// bounding box, while the negative end projects inward toward the center.
|
|
1489
|
+
function outer_end(range: [number, number]): number {
|
|
1490
|
+
return Math.abs(range[0]) <= Math.abs(range[1]) ? range[0] : range[1]
|
|
1491
|
+
}
|
|
1492
|
+
// Direction from range center toward outer end (to extend the label beyond the grid)
|
|
1493
|
+
function outer_dir(range: [number, number]): number {
|
|
1494
|
+
const end = outer_end(range)
|
|
1495
|
+
const mid = (range[0] + range[1]) / 2
|
|
1496
|
+
return end >= mid ? 1 : -1
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Grid/axis configuration for each data axis.
|
|
1500
|
+
// Axes, ticks, and labels are placed on the backside (far from camera)
|
|
1501
|
+
// matching ScatterPlot3DScene's dynamic backside tracking pattern.
|
|
1502
|
+
const grid_config = $derived.by(() => {
|
|
1503
|
+
const [r0, r1, r2] = niced_range
|
|
1504
|
+
|
|
1221
1505
|
return [0, 1, 2].map((axis) => {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1506
|
+
const ticks = data_ticks[axis]
|
|
1507
|
+
const color = axis_colors[axis]
|
|
1508
|
+
const label = axis === 0
|
|
1509
|
+
? (z_axis.label || chem_axis_label(0))
|
|
1510
|
+
: axis === 1
|
|
1511
|
+
? (x_axis.label || chem_axis_label(1))
|
|
1512
|
+
: (y_axis.label || chem_axis_label(2))
|
|
1513
|
+
|
|
1514
|
+
const tick_geoms: THREE.BufferGeometry[] = []
|
|
1515
|
+
const grid_geoms: THREE.BufferGeometry[] = []
|
|
1516
|
+
const tick_labels: { pos: Vec3; text: string }[] = []
|
|
1517
|
+
let line_geom: THREE.BufferGeometry
|
|
1518
|
+
let label_pos: Vec3
|
|
1519
|
+
|
|
1520
|
+
if (axis === 0) {
|
|
1521
|
+
// Data axis 0 (Three.js Z, depth): axis at backside d1 and d2
|
|
1522
|
+
const ls = swiz(r0[0], back[1], back[2])
|
|
1523
|
+
const le = swiz(r0[1], back[1], back[2])
|
|
1524
|
+
line_geom = make_line_geom(ls, le)
|
|
1525
|
+
// Axis label past the outer end of the axis (near 0, projects outward)
|
|
1526
|
+
label_pos = swiz(
|
|
1527
|
+
outer_end(r0) + outer_dir(r0) * axis_label_dist,
|
|
1528
|
+
back[1] + out_x * tick_label_dist * 0.5,
|
|
1529
|
+
back[2] + out_y * tick_label_dist,
|
|
1530
|
+
)
|
|
1531
|
+
for (const val of ticks) {
|
|
1532
|
+
tick_geoms.push(make_line_geom(
|
|
1533
|
+
swiz(val, back[1], back[2]),
|
|
1534
|
+
swiz(val, back[1], back[2] + out_y * tick_size),
|
|
1535
|
+
))
|
|
1536
|
+
grid_geoms.push(
|
|
1537
|
+
make_line_geom(swiz(val, r1[0], back[2]), swiz(val, r1[1], back[2])),
|
|
1538
|
+
)
|
|
1539
|
+
grid_geoms.push(
|
|
1540
|
+
make_line_geom(swiz(val, back[1], r2[0]), swiz(val, back[1], r2[1])),
|
|
1541
|
+
)
|
|
1542
|
+
tick_labels.push({
|
|
1543
|
+
pos: swiz(
|
|
1544
|
+
val,
|
|
1545
|
+
back[1] + out_x * tick_label_dist * 0.5,
|
|
1546
|
+
back[2] + out_y * tick_label_dist,
|
|
1547
|
+
),
|
|
1548
|
+
text: format_num(val, `.3~g`),
|
|
1549
|
+
})
|
|
1250
1550
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1551
|
+
} else if (axis === 1) {
|
|
1552
|
+
// Data axis 1 (Three.js X, horizontal): axis at backside d0 and d2
|
|
1553
|
+
const ls = swiz(back[0], r1[0], back[2])
|
|
1554
|
+
const le = swiz(back[0], r1[1], back[2])
|
|
1555
|
+
line_geom = make_line_geom(ls, le)
|
|
1556
|
+
label_pos = swiz(
|
|
1557
|
+
back[0],
|
|
1558
|
+
outer_end(r1) + outer_dir(r1) * axis_label_dist,
|
|
1559
|
+
back[2] + out_y * tick_label_dist,
|
|
1560
|
+
)
|
|
1561
|
+
for (const val of ticks) {
|
|
1562
|
+
tick_geoms.push(make_line_geom(
|
|
1563
|
+
swiz(back[0], val, back[2]),
|
|
1564
|
+
swiz(back[0], val, back[2] + out_y * tick_size),
|
|
1565
|
+
))
|
|
1566
|
+
grid_geoms.push(
|
|
1567
|
+
make_line_geom(swiz(r0[0], val, back[2]), swiz(r0[1], val, back[2])),
|
|
1568
|
+
)
|
|
1569
|
+
grid_geoms.push(
|
|
1570
|
+
make_line_geom(swiz(back[0], val, r2[0]), swiz(back[0], val, r2[1])),
|
|
1571
|
+
)
|
|
1572
|
+
tick_labels.push({
|
|
1573
|
+
pos: swiz(back[0], val, back[2] + out_y * tick_label_dist),
|
|
1574
|
+
text: format_num(val, `.3~g`),
|
|
1575
|
+
})
|
|
1266
1576
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1577
|
+
} else {
|
|
1578
|
+
// Data axis 2 (Three.js Y, vertical): axis at backside d0 and d1
|
|
1579
|
+
const ls = swiz(back[0], back[1], r2[0])
|
|
1580
|
+
const le = swiz(back[0], back[1], r2[1])
|
|
1581
|
+
line_geom = make_line_geom(ls, le)
|
|
1582
|
+
label_pos = swiz(
|
|
1583
|
+
back[0],
|
|
1584
|
+
back[1] + out_x * tick_label_dist,
|
|
1585
|
+
outer_end(r2) + outer_dir(r2) * axis_label_dist,
|
|
1586
|
+
)
|
|
1587
|
+
for (const val of ticks) {
|
|
1588
|
+
tick_geoms.push(make_line_geom(
|
|
1589
|
+
swiz(back[0], back[1], val),
|
|
1590
|
+
swiz(back[0], back[1] + out_x * tick_size, val),
|
|
1591
|
+
))
|
|
1592
|
+
grid_geoms.push(
|
|
1593
|
+
make_line_geom(swiz(r0[0], back[1], val), swiz(r0[1], back[1], val)),
|
|
1594
|
+
)
|
|
1595
|
+
grid_geoms.push(
|
|
1596
|
+
make_line_geom(swiz(back[0], r1[0], val), swiz(back[0], r1[1], val)),
|
|
1597
|
+
)
|
|
1598
|
+
tick_labels.push({
|
|
1599
|
+
pos: swiz(back[0], back[1] + out_x * tick_label_dist, val),
|
|
1600
|
+
text: format_num(val, `.3~g`),
|
|
1601
|
+
})
|
|
1282
1602
|
}
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
return { axis, color, label, line_geom, tick_geoms, grid_geoms, tick_labels, label_pos }
|
|
1606
|
+
})
|
|
1607
|
+
})
|
|
1608
|
+
|
|
1609
|
+
let label_occlusion_frame: number | null = null
|
|
1610
|
+
let tick_labels_occluded = false
|
|
1611
|
+
const has_occluding_domain_labels = $derived(
|
|
1612
|
+
label_stable && visible_domain_labels.length > 0,
|
|
1613
|
+
)
|
|
1614
|
+
const can_update_label_occlusion = $derived(
|
|
1615
|
+
mounted &&
|
|
1616
|
+
display.show_axis_labels &&
|
|
1617
|
+
grid_config.length > 0 &&
|
|
1618
|
+
Number.isFinite(zoom_scale) &&
|
|
1619
|
+
container_width > 0 &&
|
|
1620
|
+
container_height > 0,
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
function update_label_occlusion(): void {
|
|
1624
|
+
if (!wrapper) return
|
|
1625
|
+
const tick_labels = Array.from(
|
|
1626
|
+
wrapper.querySelectorAll<HTMLElement>(`.axis-tick-label`),
|
|
1627
|
+
)
|
|
1628
|
+
tick_labels_occluded = false
|
|
1629
|
+
for (const tick_label of tick_labels) {
|
|
1630
|
+
tick_label.style.visibility = ``
|
|
1631
|
+
}
|
|
1632
|
+
const domain_rects = Array.from(
|
|
1633
|
+
wrapper.querySelectorAll<HTMLElement>(`.domain-label`),
|
|
1634
|
+
)
|
|
1635
|
+
.filter((domain_label) => {
|
|
1636
|
+
const style = getComputedStyle(domain_label)
|
|
1637
|
+
return style.display !== `none` && style.visibility !== `hidden`
|
|
1638
|
+
})
|
|
1639
|
+
.map((domain_label) => pad_rect(domain_label.getBoundingClientRect(), 1))
|
|
1640
|
+
if (domain_rects.length === 0) return
|
|
1641
|
+
|
|
1642
|
+
for (const tick_label of tick_labels) {
|
|
1643
|
+
const style = getComputedStyle(tick_label)
|
|
1644
|
+
if (style.display === `none` || style.visibility === `hidden`) continue
|
|
1645
|
+
const tick_rect = tick_label.getBoundingClientRect()
|
|
1646
|
+
if (domain_rects.some((domain_rect) => rects_overlap(tick_rect, domain_rect))) {
|
|
1647
|
+
tick_label.style.visibility = `hidden`
|
|
1648
|
+
tick_labels_occluded = true
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
function schedule_label_occlusion_update(): void {
|
|
1654
|
+
if (typeof requestAnimationFrame === `undefined`) return
|
|
1655
|
+
if (label_occlusion_frame !== null) cancelAnimationFrame(label_occlusion_frame)
|
|
1656
|
+
label_occlusion_frame = requestAnimationFrame(() => {
|
|
1657
|
+
label_occlusion_frame = null
|
|
1658
|
+
update_label_occlusion()
|
|
1659
|
+
})
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
// Update backside positions when camera crosses axis planes.
|
|
1663
|
+
// Only updates when sign changes to avoid triggering geometry recreation every frame.
|
|
1664
|
+
function update_backside(): void {
|
|
1665
|
+
const cam = orbit_controls_ref?.object?.position
|
|
1666
|
+
if (!cam) return
|
|
1667
|
+
const [r0, r1, r2] = niced_range
|
|
1302
1668
|
// swiz: data[0]→Z, data[1]→X, data[2]→Y
|
|
1303
|
-
const new_back_0 = cam.z > data_center.z ? r0[0] : r0[1]
|
|
1304
|
-
const new_back_1 = cam.x > data_center.x ? r1[0] : r1[1]
|
|
1305
|
-
const new_back_2 = cam.y > data_center.y ? r2[0] : r2[1]
|
|
1669
|
+
const new_back_0 = cam.z > data_center.z ? r0[0] : r0[1]
|
|
1670
|
+
const new_back_1 = cam.x > data_center.x ? r1[0] : r1[1]
|
|
1671
|
+
const new_back_2 = cam.y > data_center.y ? r2[0] : r2[1]
|
|
1306
1672
|
if (back[0] !== new_back_0 || back[1] !== new_back_1 || back[2] !== new_back_2) {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1673
|
+
back = [new_back_0, new_back_1, new_back_2]
|
|
1674
|
+
out_x = cam.x > data_center.x ? -1 : 1
|
|
1675
|
+
out_y = cam.y > data_center.y ? -1 : 1
|
|
1310
1676
|
}
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
function store_camera_view_state(): void {
|
|
1313
1680
|
// Prime framing baseline on first user interaction so the next geometry
|
|
1314
1681
|
// change can preserve zoom/center immediately (not only from second change).
|
|
1315
1682
|
if (last_data_center === null) {
|
|
1316
|
-
|
|
1683
|
+
last_data_center = [data_center.x, data_center.y, data_center.z]
|
|
1317
1684
|
}
|
|
1318
1685
|
if (last_data_extent === null) {
|
|
1319
|
-
|
|
1686
|
+
last_data_extent = data_extent
|
|
1320
1687
|
}
|
|
1321
|
-
const controls = orbit_controls_ref
|
|
1322
|
-
const controls_camera = controls?.object
|
|
1688
|
+
const controls = orbit_controls_ref
|
|
1689
|
+
const controls_camera = controls?.object
|
|
1323
1690
|
if (controls_camera) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
}
|
|
1333
|
-
const controls_target = controls?.target
|
|
1691
|
+
camera_position_override = [
|
|
1692
|
+
controls_camera.position.x,
|
|
1693
|
+
controls_camera.position.y,
|
|
1694
|
+
controls_camera.position.z,
|
|
1695
|
+
]
|
|
1696
|
+
if (controls_camera instanceof THREE.OrthographicCamera) {
|
|
1697
|
+
orthographic_zoom_override = controls_camera.zoom
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
const controls_target = controls?.target
|
|
1334
1701
|
if (controls_target) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
}
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
//
|
|
1344
|
-
|
|
1702
|
+
camera_target_override = [
|
|
1703
|
+
controls_target.x,
|
|
1704
|
+
controls_target.y,
|
|
1705
|
+
controls_target.z,
|
|
1706
|
+
]
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Preserve user framing across temperature-driven geometry changes:
|
|
1711
|
+
// shift camera/target with domain center and keep orthographic zoom relative to extent.
|
|
1712
|
+
$effect(() => {
|
|
1345
1713
|
if (camera_position_override && camera_target_override && last_data_center) {
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1714
|
+
const [last_x, last_y, last_z] = last_data_center
|
|
1715
|
+
const delta_x = data_center.x - last_x
|
|
1716
|
+
const delta_y = data_center.y - last_y
|
|
1717
|
+
const delta_z = data_center.z - last_z
|
|
1718
|
+
if (delta_x !== 0 || delta_y !== 0 || delta_z !== 0) {
|
|
1719
|
+
camera_position_override = [
|
|
1720
|
+
camera_position_override[0] + delta_x,
|
|
1721
|
+
camera_position_override[1] + delta_y,
|
|
1722
|
+
camera_position_override[2] + delta_z,
|
|
1723
|
+
]
|
|
1724
|
+
camera_target_override = [
|
|
1725
|
+
camera_target_override[0] + delta_x,
|
|
1726
|
+
camera_target_override[1] + delta_y,
|
|
1727
|
+
camera_target_override[2] + delta_z,
|
|
1728
|
+
]
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
if (
|
|
1732
|
+
orthographic_zoom_override !== null &&
|
|
1733
|
+
last_data_extent !== null &&
|
|
1734
|
+
last_data_extent > 0 &&
|
|
1735
|
+
data_extent > 0
|
|
1736
|
+
) {
|
|
1737
|
+
orthographic_zoom_override *= last_data_extent / data_extent
|
|
1362
1738
|
}
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1739
|
+
last_data_center = [data_center.x, data_center.y, data_center.z]
|
|
1740
|
+
last_data_extent = data_extent
|
|
1741
|
+
})
|
|
1742
|
+
|
|
1743
|
+
$effect(() => {
|
|
1744
|
+
const controls = orbit_controls_ref
|
|
1745
|
+
if (!controls) return
|
|
1746
|
+
const on_controls_change = (): void => {
|
|
1747
|
+
update_backside()
|
|
1748
|
+
store_camera_view_state()
|
|
1749
|
+
if (has_occluding_domain_labels) schedule_label_occlusion_update()
|
|
1750
|
+
}
|
|
1751
|
+
controls.addEventListener(`change`, on_controls_change)
|
|
1752
|
+
untrack(() => update_backside())
|
|
1753
|
+
controls.update()
|
|
1754
|
+
return () => controls.removeEventListener(`change`, on_controls_change)
|
|
1755
|
+
})
|
|
1756
|
+
|
|
1757
|
+
$effect(() => {
|
|
1758
|
+
if (!can_update_label_occlusion) return
|
|
1759
|
+
if (!has_occluding_domain_labels && !tick_labels_occluded) return
|
|
1760
|
+
schedule_label_occlusion_update()
|
|
1761
|
+
})
|
|
1762
|
+
|
|
1763
|
+
$effect(() => {
|
|
1764
|
+
set_fullscreen_bg(wrapper, fullscreen, `--chempot-3d-bg-fullscreen`)
|
|
1765
|
+
})
|
|
1766
|
+
|
|
1767
|
+
$effect(() => {
|
|
1768
|
+
const grid_geometries = grid_config
|
|
1390
1769
|
return () => {
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1770
|
+
for (const grid_item of grid_geometries) {
|
|
1771
|
+
dispose_geometry(grid_item.line_geom)
|
|
1772
|
+
for (const tick_geometry of grid_item.tick_geoms) {
|
|
1773
|
+
dispose_geometry(tick_geometry)
|
|
1774
|
+
}
|
|
1775
|
+
for (const line_geometry of grid_item.grid_geoms) {
|
|
1776
|
+
dispose_geometry(line_geometry)
|
|
1399
1777
|
}
|
|
1400
|
-
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
const
|
|
1408
|
-
const
|
|
1409
|
-
const
|
|
1410
|
-
const
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
})
|
|
1781
|
+
|
|
1782
|
+
const projection_planes = $derived.by(() => {
|
|
1783
|
+
const projections = display.projections
|
|
1784
|
+
if (!projections) return []
|
|
1785
|
+
const [r0, r1, r2] = niced_range
|
|
1786
|
+
const s0 = (r0[1] - r0[0]) * (display.projection_scale ?? 0.5)
|
|
1787
|
+
const s1 = (r1[1] - r1[0]) * (display.projection_scale ?? 0.5)
|
|
1788
|
+
const s2 = (r2[1] - r2[0]) * (display.projection_scale ?? 0.5)
|
|
1789
|
+
const planes: {
|
|
1790
|
+
key: string
|
|
1791
|
+
pos: Vec3
|
|
1792
|
+
rot: Vec3
|
|
1793
|
+
size: [number, number]
|
|
1794
|
+
color: string
|
|
1795
|
+
}[] = []
|
|
1411
1796
|
if (projections.xy) {
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1797
|
+
planes.push({
|
|
1798
|
+
key: `xy`,
|
|
1799
|
+
pos: swiz((r0[0] + r0[1]) / 2, (r1[0] + r1[1]) / 2, back[2]),
|
|
1800
|
+
rot: [-Math.PI / 2, 0, 0],
|
|
1801
|
+
size: [s1, s0],
|
|
1802
|
+
color: `#5dade2`,
|
|
1803
|
+
})
|
|
1419
1804
|
}
|
|
1420
1805
|
if (projections.xz) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1806
|
+
planes.push({
|
|
1807
|
+
key: `xz`,
|
|
1808
|
+
pos: swiz((r0[0] + r0[1]) / 2, back[1], (r2[0] + r2[1]) / 2),
|
|
1809
|
+
rot: [0, Math.PI / 2, 0],
|
|
1810
|
+
size: [s0, s2],
|
|
1811
|
+
color: `#58d68d`,
|
|
1812
|
+
})
|
|
1428
1813
|
}
|
|
1429
1814
|
if (projections.yz) {
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
}
|
|
1438
|
-
return planes
|
|
1439
|
-
})
|
|
1440
|
-
|
|
1441
|
-
|
|
1815
|
+
planes.push({
|
|
1816
|
+
key: `yz`,
|
|
1817
|
+
pos: swiz(back[0], (r1[0] + r1[1]) / 2, (r2[0] + r2[1]) / 2),
|
|
1818
|
+
rot: [0, 0, 0],
|
|
1819
|
+
size: [s1, s2],
|
|
1820
|
+
color: `#f5b041`,
|
|
1821
|
+
})
|
|
1822
|
+
}
|
|
1823
|
+
return planes
|
|
1824
|
+
})
|
|
1825
|
+
|
|
1826
|
+
const bounding_box_geometry = $derived.by(() => {
|
|
1827
|
+
const [r0, r1, r2] = niced_range
|
|
1442
1828
|
const vertices = [
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
]
|
|
1829
|
+
swiz(r0[0], r1[0], r2[0]),
|
|
1830
|
+
swiz(r0[1], r1[0], r2[0]),
|
|
1831
|
+
swiz(r0[1], r1[1], r2[0]),
|
|
1832
|
+
swiz(r0[0], r1[1], r2[0]),
|
|
1833
|
+
swiz(r0[0], r1[0], r2[1]),
|
|
1834
|
+
swiz(r0[1], r1[0], r2[1]),
|
|
1835
|
+
swiz(r0[1], r1[1], r2[1]),
|
|
1836
|
+
swiz(r0[0], r1[1], r2[1]),
|
|
1837
|
+
]
|
|
1452
1838
|
const edges = [
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
]
|
|
1466
|
-
const positions = []
|
|
1839
|
+
[0, 1],
|
|
1840
|
+
[1, 2],
|
|
1841
|
+
[2, 3],
|
|
1842
|
+
[3, 0],
|
|
1843
|
+
[4, 5],
|
|
1844
|
+
[5, 6],
|
|
1845
|
+
[6, 7],
|
|
1846
|
+
[7, 4],
|
|
1847
|
+
[0, 4],
|
|
1848
|
+
[1, 5],
|
|
1849
|
+
[2, 6],
|
|
1850
|
+
[3, 7],
|
|
1851
|
+
]
|
|
1852
|
+
const positions: number[] = []
|
|
1467
1853
|
for (const [start_idx, end_idx] of edges) {
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
}
|
|
1472
|
-
const geom = new THREE.BufferGeometry()
|
|
1473
|
-
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(positions, 3))
|
|
1474
|
-
return geom
|
|
1475
|
-
})
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const current_owner_idx = next_projection.indexOf(element)
|
|
1854
|
+
const start = vertices[start_idx]
|
|
1855
|
+
const end = vertices[end_idx]
|
|
1856
|
+
positions.push(start[0], start[1], start[2], end[0], end[1], end[2])
|
|
1857
|
+
}
|
|
1858
|
+
const geom = new THREE.BufferGeometry()
|
|
1859
|
+
geom.setAttribute(`position`, new THREE.Float32BufferAttribute(positions, 3))
|
|
1860
|
+
return geom
|
|
1861
|
+
})
|
|
1862
|
+
|
|
1863
|
+
function reset_controls(): void {
|
|
1864
|
+
formal_chempots_override = null
|
|
1865
|
+
label_stable_override = null
|
|
1866
|
+
element_padding_override = null
|
|
1867
|
+
default_min_limit_override = null
|
|
1868
|
+
draw_formula_meshes_override = null
|
|
1869
|
+
draw_formula_lines_override = null
|
|
1870
|
+
color_mode_override = null
|
|
1871
|
+
color_scale_override = null
|
|
1872
|
+
reverse_color_scale_override = null
|
|
1873
|
+
projection_elements_override = null
|
|
1874
|
+
formulas_to_draw_override = null
|
|
1875
|
+
formula_filter_query = ``
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
function set_projection_axis(axis_idx: number, element: string): void {
|
|
1879
|
+
if (!all_entry_elements.includes(element)) return
|
|
1880
|
+
const next_projection = [...plot_elements]
|
|
1881
|
+
if (next_projection.length !== 3) return
|
|
1882
|
+
const current_owner_idx = next_projection.indexOf(element)
|
|
1497
1883
|
if (current_owner_idx !== -1 && current_owner_idx !== axis_idx) {
|
|
1498
|
-
|
|
1499
|
-
}
|
|
1500
|
-
next_projection[axis_idx] = element
|
|
1501
|
-
const normalized = normalize_projection_triplet(
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1884
|
+
next_projection[current_owner_idx] = next_projection[axis_idx]
|
|
1885
|
+
}
|
|
1886
|
+
next_projection[axis_idx] = element
|
|
1887
|
+
const normalized = normalize_projection_triplet(
|
|
1888
|
+
next_projection,
|
|
1889
|
+
all_entry_elements,
|
|
1890
|
+
)
|
|
1891
|
+
if (normalized) projection_elements_override = normalized
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function apply_projection_preset(preset_elements: string[]): void {
|
|
1895
|
+
const normalized = normalize_projection_triplet(
|
|
1896
|
+
preset_elements,
|
|
1897
|
+
all_entry_elements,
|
|
1898
|
+
)
|
|
1899
|
+
if (normalized) projection_elements_override = normalized
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function toggle_formula_selection(formula: string): void {
|
|
1903
|
+
const selected_formulas = new SvelteSet(formulas_to_draw)
|
|
1904
|
+
if (selected_formulas.has(formula)) selected_formulas.delete(formula)
|
|
1905
|
+
else selected_formulas.add(formula)
|
|
1906
|
+
formulas_to_draw_override = [...selected_formulas]
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function select_surface_formulas(): void {
|
|
1519
1910
|
formulas_to_draw_override = render_domains
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
const neighbors = domain_neighbors.get(hover_info.formula) ?? []
|
|
1527
|
-
formulas_to_draw_override = [hover_info.formula, ...neighbors]
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
const
|
|
1532
|
-
link
|
|
1533
|
-
link.
|
|
1534
|
-
link.
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1911
|
+
.filter((domain) => surface_formulas.has(domain.formula))
|
|
1912
|
+
.map((domain) => domain.formula)
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
function select_neighbor_formulas(): void {
|
|
1916
|
+
if (hover_info?.view !== `3d`) return
|
|
1917
|
+
const neighbors = domain_neighbors.get(hover_info.formula) ?? []
|
|
1918
|
+
formulas_to_draw_override = [hover_info.formula, ...neighbors]
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
function download_blob(blob: Blob, filename: string): void {
|
|
1922
|
+
const url = URL.createObjectURL(blob)
|
|
1923
|
+
const link = document.createElement(`a`)
|
|
1924
|
+
link.href = url
|
|
1925
|
+
link.download = filename
|
|
1926
|
+
link.click()
|
|
1927
|
+
URL.revokeObjectURL(url)
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
let png_dpi = $state(150)
|
|
1931
|
+
const export_basename = $derived(`chempot-${plot_elements.join(`-`)}`)
|
|
1932
|
+
|
|
1933
|
+
function get_view_settings(): Record<string, unknown> {
|
|
1934
|
+
const camera_position = orbit_controls_ref?.object?.position
|
|
1935
|
+
const camera_target = orbit_controls_ref?.target
|
|
1542
1936
|
return {
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1937
|
+
elements: plot_elements,
|
|
1938
|
+
camera_projection,
|
|
1939
|
+
auto_rotate,
|
|
1940
|
+
color_mode,
|
|
1941
|
+
color_scale,
|
|
1942
|
+
reverse_color_scale,
|
|
1943
|
+
camera_position: camera_position
|
|
1944
|
+
? [camera_position.x, camera_position.y, camera_position.z]
|
|
1945
|
+
: null,
|
|
1946
|
+
camera_target: camera_target
|
|
1947
|
+
? [camera_target.x, camera_target.y, camera_target.z]
|
|
1948
|
+
: null,
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
interface OverlayTextItem {
|
|
1953
|
+
x: number
|
|
1954
|
+
y: number
|
|
1955
|
+
text: string
|
|
1956
|
+
font: string
|
|
1957
|
+
font_size: string
|
|
1958
|
+
font_family: string
|
|
1959
|
+
font_weight: string
|
|
1960
|
+
color: string
|
|
1961
|
+
}
|
|
1962
|
+
function get_overlay_text_items(canvas_rect: DOMRect): OverlayTextItem[] {
|
|
1963
|
+
if (!wrapper) return []
|
|
1964
|
+
const text_items: OverlayTextItem[] = []
|
|
1965
|
+
for (
|
|
1966
|
+
const element of wrapper.querySelectorAll(
|
|
1967
|
+
`.tick-label, .axis-label, .domain-label`,
|
|
1968
|
+
)
|
|
1969
|
+
) {
|
|
1970
|
+
const html_element = element as HTMLElement
|
|
1971
|
+
const style = getComputedStyle(html_element)
|
|
1972
|
+
if (style.display === `none` || style.visibility === `hidden`) continue
|
|
1973
|
+
const element_rect = html_element.getBoundingClientRect()
|
|
1974
|
+
text_items.push({
|
|
1975
|
+
x: element_rect.left + element_rect.width / 2 - canvas_rect.left,
|
|
1976
|
+
y: element_rect.top + element_rect.height / 2 - canvas_rect.top,
|
|
1977
|
+
text: html_element.textContent ?? ``,
|
|
1978
|
+
font: style.font || `${style.fontSize} ${style.fontFamily}`,
|
|
1979
|
+
font_size: style.fontSize || `11px`,
|
|
1980
|
+
font_family: style.fontFamily || `sans-serif`,
|
|
1981
|
+
font_weight: style.fontWeight || `400`,
|
|
1982
|
+
color: style.color || `#333`,
|
|
1983
|
+
})
|
|
1984
|
+
}
|
|
1985
|
+
return text_items
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
function export_png_file(): void {
|
|
1989
|
+
if (!wrapper) return
|
|
1990
|
+
const gl_canvas = wrapper.querySelector(`canvas`)
|
|
1991
|
+
if (!(gl_canvas instanceof HTMLCanvasElement)) return
|
|
1992
|
+
|
|
1586
1993
|
// Composite WebGL canvas + HTML overlay labels into a single image
|
|
1587
|
-
const rect = gl_canvas.getBoundingClientRect()
|
|
1588
|
-
const scale = Math.min(png_dpi / 72, 10)
|
|
1589
|
-
const out = document.createElement(`canvas`)
|
|
1590
|
-
out.width = Math.round(rect.width * scale)
|
|
1591
|
-
out.height = Math.round(rect.height * scale)
|
|
1592
|
-
const ctx = out.getContext(`2d`)
|
|
1593
|
-
if (!ctx)
|
|
1594
|
-
|
|
1595
|
-
|
|
1994
|
+
const rect = gl_canvas.getBoundingClientRect()
|
|
1995
|
+
const scale = Math.min(png_dpi / 72, 10)
|
|
1996
|
+
const out = document.createElement(`canvas`)
|
|
1997
|
+
out.width = Math.round(rect.width * scale)
|
|
1998
|
+
out.height = Math.round(rect.height * scale)
|
|
1999
|
+
const ctx = out.getContext(`2d`)
|
|
2000
|
+
if (!ctx) return
|
|
2001
|
+
ctx.scale(scale, scale)
|
|
2002
|
+
|
|
1596
2003
|
// Draw the WebGL canvas as background
|
|
1597
|
-
ctx.drawImage(gl_canvas, 0, 0, rect.width, rect.height)
|
|
2004
|
+
ctx.drawImage(gl_canvas, 0, 0, rect.width, rect.height)
|
|
2005
|
+
|
|
1598
2006
|
// Draw all HTML overlay text (tick labels, axis labels, domain labels)
|
|
1599
2007
|
for (const text_item of get_overlay_text_items(rect)) {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
2008
|
+
ctx.font = text_item.font
|
|
2009
|
+
ctx.fillStyle = text_item.color
|
|
2010
|
+
ctx.textAlign = `center`
|
|
2011
|
+
ctx.textBaseline = `middle`
|
|
2012
|
+
ctx.fillText(text_item.text, text_item.x, text_item.y)
|
|
1605
2013
|
}
|
|
2014
|
+
|
|
1606
2015
|
out.toBlob((blob) => {
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
function xml_escape(text) {
|
|
2016
|
+
if (!blob) return
|
|
2017
|
+
download_blob(blob, `${export_basename}.png`)
|
|
2018
|
+
}, `image/png`)
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function xml_escape(text: string): string {
|
|
1613
2022
|
return text
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
const gl_canvas = wrapper.querySelector(`canvas`)
|
|
1624
|
-
if (!(gl_canvas instanceof HTMLCanvasElement))
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
2023
|
+
.replaceAll(`&`, `&`)
|
|
2024
|
+
.replaceAll(`<`, `<`)
|
|
2025
|
+
.replaceAll(`>`, `>`)
|
|
2026
|
+
.replaceAll(`"`, `"`)
|
|
2027
|
+
.replaceAll(`'`, `'`)
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
function export_svg_file(): void {
|
|
2031
|
+
if (!wrapper) return
|
|
2032
|
+
const gl_canvas = wrapper.querySelector(`canvas`)
|
|
2033
|
+
if (!(gl_canvas instanceof HTMLCanvasElement)) return
|
|
2034
|
+
const canvas_rect = gl_canvas.getBoundingClientRect()
|
|
2035
|
+
if (canvas_rect.width === 0 || canvas_rect.height === 0) return
|
|
2036
|
+
const png_data_url = gl_canvas.toDataURL(`image/png`)
|
|
2037
|
+
const text_nodes = get_overlay_text_items(canvas_rect).map((text_item) =>
|
|
2038
|
+
`<text x="${text_item.x.toFixed(2)}" y="${
|
|
2039
|
+
text_item.y.toFixed(2)
|
|
2040
|
+
}" text-anchor="middle" dominant-baseline="central" fill="${
|
|
2041
|
+
xml_escape(text_item.color)
|
|
2042
|
+
}" font-size="${xml_escape(text_item.font_size)}" font-family="${
|
|
2043
|
+
xml_escape(text_item.font_family)
|
|
2044
|
+
}" font-weight="${xml_escape(text_item.font_weight)}">${
|
|
2045
|
+
xml_escape(text_item.text)
|
|
2046
|
+
}</text>`
|
|
2047
|
+
)
|
|
2048
|
+
const metadata = xml_escape(JSON.stringify(get_view_settings()))
|
|
1632
2049
|
const svg = [
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
].join(``)
|
|
1640
|
-
download_blob(
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
function
|
|
1647
|
-
const
|
|
1648
|
-
|
|
2050
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
2051
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${canvas_rect.width}" height="${canvas_rect.height}" viewBox="0 0 ${canvas_rect.width} ${canvas_rect.height}">`,
|
|
2052
|
+
`<metadata>${metadata}</metadata>`,
|
|
2053
|
+
`<image href="${png_data_url}" x="0" y="0" width="${canvas_rect.width}" height="${canvas_rect.height}" />`,
|
|
2054
|
+
...text_nodes,
|
|
2055
|
+
`</svg>`,
|
|
2056
|
+
].join(``)
|
|
2057
|
+
download_blob(
|
|
2058
|
+
new Blob([svg], { type: `image/svg+xml` }),
|
|
2059
|
+
`${export_basename}.svg`,
|
|
2060
|
+
)
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
function export_view_json_file(): void {
|
|
2064
|
+
const json_text = JSON.stringify(get_view_settings(), null, 2)
|
|
2065
|
+
download_blob(
|
|
2066
|
+
new Blob([json_text], { type: `application/json` }),
|
|
2067
|
+
`${export_basename}-view.json`,
|
|
2068
|
+
)
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2071
|
+
function export_glb_file(): void {
|
|
2072
|
+
const gltf_exporter = new GLTFExporter()
|
|
2073
|
+
const export_root = new THREE.Group()
|
|
1649
2074
|
if (colored_hull_geometry) {
|
|
1650
|
-
|
|
2075
|
+
export_root.add(
|
|
2076
|
+
new THREE.Mesh(
|
|
2077
|
+
colored_hull_geometry.clone(),
|
|
2078
|
+
new THREE.MeshBasicMaterial({
|
|
1651
2079
|
vertexColors: true,
|
|
1652
2080
|
transparent: true,
|
|
1653
2081
|
opacity: color_mode === `none` ? 0.25 : 0.4,
|
|
1654
2082
|
side: THREE.DoubleSide,
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2083
|
+
}),
|
|
2084
|
+
),
|
|
2085
|
+
)
|
|
2086
|
+
}
|
|
2087
|
+
export_root.add(
|
|
2088
|
+
new THREE.LineSegments(
|
|
2089
|
+
edge_geometry.clone(),
|
|
2090
|
+
new THREE.LineBasicMaterial({ color: 0x333333 }),
|
|
2091
|
+
),
|
|
2092
|
+
)
|
|
1658
2093
|
for (const { geometry, color } of formula_mesh_data) {
|
|
1659
|
-
|
|
2094
|
+
export_root.add(
|
|
2095
|
+
new THREE.Mesh(
|
|
2096
|
+
geometry.clone(),
|
|
2097
|
+
new THREE.MeshBasicMaterial({
|
|
1660
2098
|
color: new THREE.Color(color),
|
|
1661
2099
|
transparent: true,
|
|
1662
2100
|
opacity: 0.13,
|
|
1663
2101
|
side: THREE.DoubleSide,
|
|
1664
|
-
|
|
2102
|
+
}),
|
|
2103
|
+
),
|
|
2104
|
+
)
|
|
1665
2105
|
}
|
|
1666
2106
|
if (draw_formula_lines) {
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
2107
|
+
for (const { geometry, color } of formula_edge_data) {
|
|
2108
|
+
export_root.add(
|
|
2109
|
+
new THREE.LineSegments(
|
|
2110
|
+
geometry.clone(),
|
|
2111
|
+
new THREE.LineBasicMaterial({ color: new THREE.Color(color) }),
|
|
2112
|
+
),
|
|
2113
|
+
)
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
gltf_exporter.parse(
|
|
2117
|
+
export_root,
|
|
2118
|
+
(result) => {
|
|
2119
|
+
if (!(result instanceof ArrayBuffer)) return
|
|
2120
|
+
download_blob(
|
|
2121
|
+
new Blob([result], { type: `model/gltf-binary` }),
|
|
2122
|
+
`${export_basename}.glb`,
|
|
2123
|
+
)
|
|
2124
|
+
},
|
|
2125
|
+
(err) => {
|
|
2126
|
+
console.error(`Failed to export GLB:`, err)
|
|
2127
|
+
},
|
|
2128
|
+
{ binary: true, onlyVisible: false },
|
|
2129
|
+
)
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
function get_json_string(): string {
|
|
2133
|
+
return JSON.stringify(
|
|
2134
|
+
{
|
|
1681
2135
|
elements: diagram_data?.elements ?? [],
|
|
1682
2136
|
domains: render_domains.map((domain) => ({
|
|
1683
|
-
|
|
1684
|
-
|
|
2137
|
+
formula: domain.formula,
|
|
2138
|
+
points_3d: domain.points_3d,
|
|
1685
2139
|
})),
|
|
1686
2140
|
lims: diagram_data?.lims ?? [],
|
|
1687
2141
|
view: get_view_settings(),
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
1693
|
-
|
|
2142
|
+
},
|
|
2143
|
+
null,
|
|
2144
|
+
2,
|
|
2145
|
+
)
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
function export_json_file(): void {
|
|
2149
|
+
download_blob(
|
|
2150
|
+
new Blob([get_json_string()], { type: `application/json` }),
|
|
2151
|
+
`${export_basename}.json`,
|
|
2152
|
+
)
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
async function copy_json(): Promise<void> {
|
|
1694
2156
|
try {
|
|
1695
|
-
|
|
1696
|
-
|
|
2157
|
+
await navigator.clipboard.writeText(get_json_string())
|
|
2158
|
+
copy_status = true
|
|
2159
|
+
} catch (err) {
|
|
2160
|
+
copy_status = false
|
|
2161
|
+
console.error(`Failed to copy JSON to clipboard:`, err)
|
|
1697
2162
|
}
|
|
1698
|
-
|
|
1699
|
-
copy_status = false;
|
|
1700
|
-
console.error(`Failed to copy JSON to clipboard:`, err);
|
|
1701
|
-
}
|
|
1702
|
-
if (copy_timeout_id !== null)
|
|
1703
|
-
clearTimeout(copy_timeout_id);
|
|
2163
|
+
if (copy_timeout_id !== null) clearTimeout(copy_timeout_id)
|
|
1704
2164
|
copy_timeout_id = setTimeout(() => {
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
}, 1000)
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
if (
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
refresh_fixed_container_rect();
|
|
1750
|
-
});
|
|
1751
|
-
}
|
|
1752
|
-
$effect(() => {
|
|
1753
|
-
const next_fixed_container_element = find_fixed_container_element();
|
|
1754
|
-
fixed_container_element = next_fixed_container_element;
|
|
1755
|
-
refresh_fixed_container_rect(next_fixed_container_element);
|
|
1756
|
-
});
|
|
1757
|
-
onMount(() => {
|
|
1758
|
-
const handle_layout_change = () => queue_fixed_container_rect_refresh();
|
|
1759
|
-
window.addEventListener(`resize`, handle_layout_change);
|
|
1760
|
-
window.addEventListener(`scroll`, handle_layout_change, true);
|
|
1761
|
-
return () => {
|
|
1762
|
-
window.removeEventListener(`resize`, handle_layout_change);
|
|
1763
|
-
window.removeEventListener(`scroll`, handle_layout_change, true);
|
|
1764
|
-
};
|
|
1765
|
-
});
|
|
1766
|
-
let locked_hover_formula = $state(null);
|
|
1767
|
-
function set_hover_info(domain_data, raw_event) {
|
|
1768
|
-
hover_info = with_hover_pointer(domain_data.info, raw_event, fixed_container_rect);
|
|
1769
|
-
}
|
|
1770
|
-
function clear_hover_lock() {
|
|
1771
|
-
locked_hover_formula = null;
|
|
1772
|
-
hover_info = null;
|
|
1773
|
-
}
|
|
1774
|
-
function handle_phase_hover(domain_data, raw_event) {
|
|
1775
|
-
if (locked_hover_formula && locked_hover_formula !== domain_data.formula)
|
|
1776
|
-
return;
|
|
1777
|
-
set_hover_info(domain_data, raw_event);
|
|
1778
|
-
}
|
|
1779
|
-
function toggle_phase_lock(domain_data, raw_event) {
|
|
2165
|
+
copy_status = false
|
|
2166
|
+
copy_timeout_id = null
|
|
2167
|
+
}, 1000)
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
onDestroy(() => {
|
|
2171
|
+
if (copy_timeout_id !== null) clearTimeout(copy_timeout_id)
|
|
2172
|
+
if (label_occlusion_frame !== null) cancelAnimationFrame(label_occlusion_frame)
|
|
2173
|
+
})
|
|
2174
|
+
|
|
2175
|
+
let locked_hover_formula = $state<string | null>(null)
|
|
2176
|
+
let tooltip_el = $state<HTMLElement>()
|
|
2177
|
+
|
|
2178
|
+
const tooltip_pos = $derived.by(() => {
|
|
2179
|
+
const pointer = hover_info?.pointer
|
|
2180
|
+
if (!pointer) return { x: 4, y: 4 }
|
|
2181
|
+
return constrain_tooltip_position(
|
|
2182
|
+
pointer.x, pointer.y,
|
|
2183
|
+
tooltip_el?.offsetWidth ?? 200,
|
|
2184
|
+
tooltip_el?.offsetHeight ?? 100,
|
|
2185
|
+
container_width, container_height,
|
|
2186
|
+
{ offset: 0 },
|
|
2187
|
+
)
|
|
2188
|
+
})
|
|
2189
|
+
|
|
2190
|
+
function set_hover_info(domain_data: HoverMeshData, raw_event: unknown): void {
|
|
2191
|
+
hover_info = with_hover_pointer<ChemPotHoverInfo>(
|
|
2192
|
+
domain_data.info,
|
|
2193
|
+
raw_event,
|
|
2194
|
+
wrapper?.getBoundingClientRect() ?? null,
|
|
2195
|
+
)
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
function clear_hover_lock(): void {
|
|
2199
|
+
locked_hover_formula = null
|
|
2200
|
+
hover_info = null
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
function handle_phase_hover(domain_data: HoverMeshData, raw_event: unknown): void {
|
|
2204
|
+
if (locked_hover_formula && locked_hover_formula !== domain_data.formula) return
|
|
2205
|
+
set_hover_info(domain_data, raw_event)
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
function toggle_phase_lock(domain_data: HoverMeshData, raw_event: unknown): void {
|
|
1780
2209
|
if (locked_hover_formula === domain_data.formula) {
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
locked_hover_formula = domain_data.formula
|
|
1785
|
-
set_hover_info(domain_data, raw_event)
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
|
|
2210
|
+
clear_hover_lock()
|
|
2211
|
+
return
|
|
2212
|
+
}
|
|
2213
|
+
locked_hover_formula = domain_data.formula
|
|
2214
|
+
set_hover_info(domain_data, raw_event)
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// Color mode cycling (keyboard shortcut 'c')
|
|
2218
|
+
const color_modes: ChemPotColorMode[] = [
|
|
1789
2219
|
`none`,
|
|
1790
2220
|
`energy`,
|
|
1791
2221
|
`formation_energy`,
|
|
1792
2222
|
`arity`,
|
|
1793
2223
|
`entries`,
|
|
1794
|
-
]
|
|
1795
|
-
function cycle_color_mode() {
|
|
1796
|
-
const idx = color_modes.indexOf(color_mode)
|
|
1797
|
-
color_mode_override = color_modes[(idx + 1) % color_modes.length]
|
|
1798
|
-
}
|
|
2224
|
+
]
|
|
2225
|
+
function cycle_color_mode(): void {
|
|
2226
|
+
const idx = color_modes.indexOf(color_mode)
|
|
2227
|
+
color_mode_override = color_modes[(idx + 1) % color_modes.length]
|
|
2228
|
+
}
|
|
1799
2229
|
</script>
|
|
1800
2230
|
|
|
1801
2231
|
<svelte:document
|
|
@@ -1947,7 +2377,7 @@ function cycle_color_mode() {
|
|
|
1947
2377
|
formula_colors.length
|
|
1948
2378
|
]}
|
|
1949
2379
|
></span>
|
|
1950
|
-
{
|
|
2380
|
+
{get_electro_neg_formula(formula, true, ``, `.3~s`)}
|
|
1951
2381
|
</label>
|
|
1952
2382
|
{/each}
|
|
1953
2383
|
{/if}
|
|
@@ -1955,6 +2385,7 @@ function cycle_color_mode() {
|
|
|
1955
2385
|
</DraggablePane>
|
|
1956
2386
|
|
|
1957
2387
|
<ScatterPlot3DControls
|
|
2388
|
+
bind:show={controls_open}
|
|
1958
2389
|
bind:x_axis
|
|
1959
2390
|
bind:y_axis
|
|
1960
2391
|
bind:z_axis
|
|
@@ -2163,7 +2594,12 @@ function cycle_color_mode() {
|
|
|
2163
2594
|
bind:temperature
|
|
2164
2595
|
/>
|
|
2165
2596
|
{/if}
|
|
2166
|
-
|
|
2597
|
+
<div class="canvas-clip">
|
|
2598
|
+
{#if diagram_computing}
|
|
2599
|
+
<div class="computing-state">
|
|
2600
|
+
<Spinner text="Computing chemical potential domains..." style="--spinner-size: 1.2em" />
|
|
2601
|
+
</div>
|
|
2602
|
+
{:else if !diagram_data}
|
|
2167
2603
|
<div class="error-state" role="alert" aria-live="polite">
|
|
2168
2604
|
<p>Cannot compute chemical potential diagram.</p>
|
|
2169
2605
|
<p>Need at least 2 elements with elemental reference entries.</p>
|
|
@@ -2344,7 +2780,7 @@ function cycle_color_mode() {
|
|
|
2344
2780
|
portal={wrapper}
|
|
2345
2781
|
zIndexRange={[1, 0]}
|
|
2346
2782
|
>
|
|
2347
|
-
<span class="tick-label">{tick.text}</span>
|
|
2783
|
+
<span class="tick-label axis-tick-label">{tick.text}</span>
|
|
2348
2784
|
</extras.HTML>
|
|
2349
2785
|
{/each}
|
|
2350
2786
|
<!-- Axis label -->
|
|
@@ -2354,26 +2790,28 @@ function cycle_color_mode() {
|
|
|
2354
2790
|
portal={wrapper}
|
|
2355
2791
|
zIndexRange={[1, 0]}
|
|
2356
2792
|
>
|
|
2357
|
-
<span class="axis-label" style:color={gc.color}>{gc.label}</span>
|
|
2793
|
+
<span class="axis-label" style:color={gc.color}>{@html gc.label}</span>
|
|
2358
2794
|
</extras.HTML>
|
|
2359
2795
|
{/if}
|
|
2360
2796
|
{/each}
|
|
2361
2797
|
|
|
2362
|
-
<!-- Domain labels
|
|
2798
|
+
<!-- Domain labels -->
|
|
2363
2799
|
{#if label_stable}
|
|
2364
|
-
{#each
|
|
2365
|
-
domain
|
|
2366
|
-
(domain.formula)
|
|
2367
|
-
}
|
|
2800
|
+
{#each visible_domain_labels as domain (domain.formula)}
|
|
2368
2801
|
<extras.HTML
|
|
2369
|
-
position={
|
|
2802
|
+
position={domain.position}
|
|
2370
2803
|
center
|
|
2371
2804
|
portal={wrapper}
|
|
2372
|
-
zIndexRange={[
|
|
2805
|
+
zIndexRange={[5, 5]}
|
|
2373
2806
|
>
|
|
2374
2807
|
<span
|
|
2375
2808
|
class="domain-label"
|
|
2376
|
-
|
|
2809
|
+
style:font-size="{(domain.label_font_size * zoom_scale).toFixed(1)}px"
|
|
2810
|
+
>
|
|
2811
|
+
{#each formula_label_segments(domain.formula) as segment}
|
|
2812
|
+
<span class:formula-subscript={segment.subscript}>{segment.text}</span>
|
|
2813
|
+
{/each}
|
|
2814
|
+
</span>
|
|
2377
2815
|
</extras.HTML>
|
|
2378
2816
|
{/each}
|
|
2379
2817
|
{/if}
|
|
@@ -2409,11 +2847,16 @@ function cycle_color_mode() {
|
|
|
2409
2847
|
{/if}
|
|
2410
2848
|
{#if render_local_tooltip && show_tooltip && hover_info?.view === `3d`}
|
|
2411
2849
|
<aside
|
|
2850
|
+
bind:this={tooltip_el}
|
|
2412
2851
|
class="phase-tooltip"
|
|
2413
|
-
style:left="{
|
|
2414
|
-
style:top="{
|
|
2852
|
+
style:left="{tooltip_pos.x}px"
|
|
2853
|
+
style:top="{tooltip_pos.y}px"
|
|
2415
2854
|
>
|
|
2416
|
-
<h4>
|
|
2855
|
+
<h4>
|
|
2856
|
+
{#each formula_label_segments(hover_info.formula) as segment}
|
|
2857
|
+
<span class:formula-subscript={segment.subscript}>{segment.text}</span>
|
|
2858
|
+
{/each}
|
|
2859
|
+
</h4>
|
|
2417
2860
|
{#if locked_hover_formula === hover_info.formula}
|
|
2418
2861
|
<p>Pinned · Press Esc to unlock</p>
|
|
2419
2862
|
{/if}
|
|
@@ -2451,6 +2894,7 @@ function cycle_color_mode() {
|
|
|
2451
2894
|
{/if}
|
|
2452
2895
|
</aside>
|
|
2453
2896
|
{/if}
|
|
2897
|
+
</div>
|
|
2454
2898
|
</div>
|
|
2455
2899
|
|
|
2456
2900
|
<style>
|
|
@@ -2458,12 +2902,17 @@ function cycle_color_mode() {
|
|
|
2458
2902
|
position: relative;
|
|
2459
2903
|
overflow: clip;
|
|
2460
2904
|
}
|
|
2905
|
+
.canvas-clip {
|
|
2906
|
+
position: relative;
|
|
2907
|
+
overflow: clip;
|
|
2908
|
+
width: 100%;
|
|
2909
|
+
height: 100%;
|
|
2910
|
+
}
|
|
2461
2911
|
.chempot-diagram-3d:fullscreen {
|
|
2462
2912
|
background: var(--chempot-3d-bg-fullscreen, var(--bg-color, #fff));
|
|
2463
2913
|
}
|
|
2464
|
-
/* Threlte <extras.HTML portal={wrapper}> appends absolutely-positioned divs
|
|
2465
|
-
|
|
2466
|
-
events and prevent the Three.js raycaster from detecting hover meshes. */
|
|
2914
|
+
/* Threlte <extras.HTML portal={wrapper}> appends absolutely-positioned overlay divs
|
|
2915
|
+
for 3D labels. pointer-events: none prevents them from blocking raycasting. */
|
|
2467
2916
|
.chempot-diagram-3d > :global(div[style*='position: absolute'][style*='top: 0']) {
|
|
2468
2917
|
pointer-events: none !important;
|
|
2469
2918
|
}
|
|
@@ -2474,6 +2923,21 @@ function cycle_color_mode() {
|
|
|
2474
2923
|
display: flex;
|
|
2475
2924
|
gap: 8px;
|
|
2476
2925
|
z-index: 20;
|
|
2926
|
+
opacity: 0;
|
|
2927
|
+
transition: opacity 0.25s ease;
|
|
2928
|
+
pointer-events: none;
|
|
2929
|
+
}
|
|
2930
|
+
.chempot-diagram-3d:hover > section,
|
|
2931
|
+
.chempot-diagram-3d:focus-within > section,
|
|
2932
|
+
.chempot-diagram-3d > section:has(:global(.pane-open)) {
|
|
2933
|
+
opacity: 1;
|
|
2934
|
+
pointer-events: auto;
|
|
2935
|
+
}
|
|
2936
|
+
@media (hover: none) {
|
|
2937
|
+
.chempot-diagram-3d > section {
|
|
2938
|
+
opacity: 1;
|
|
2939
|
+
pointer-events: auto;
|
|
2940
|
+
}
|
|
2477
2941
|
}
|
|
2478
2942
|
.chempot-diagram-3d > section > :global(button),
|
|
2479
2943
|
.chempot-diagram-3d > section > :global(.pane-toggle) {
|
|
@@ -2511,6 +2975,14 @@ function cycle_color_mode() {
|
|
|
2511
2975
|
.chempot-diagram-3d :global(.export-row > label) {
|
|
2512
2976
|
margin: 0;
|
|
2513
2977
|
}
|
|
2978
|
+
.chempot-diagram-3d :global(.export-row button) {
|
|
2979
|
+
width: 1.4em;
|
|
2980
|
+
height: 1.4em;
|
|
2981
|
+
padding: 0;
|
|
2982
|
+
display: inline-flex;
|
|
2983
|
+
align-items: center;
|
|
2984
|
+
justify-content: center;
|
|
2985
|
+
}
|
|
2514
2986
|
.chempot-diagram-3d :global(.chempot-checks) {
|
|
2515
2987
|
display: flex;
|
|
2516
2988
|
flex-wrap: wrap;
|
|
@@ -2552,16 +3024,17 @@ function cycle_color_mode() {
|
|
|
2552
3024
|
}
|
|
2553
3025
|
.chempot-diagram-3d :global(.overlay-actions) {
|
|
2554
3026
|
display: flex;
|
|
2555
|
-
gap:
|
|
3027
|
+
gap: 3pt;
|
|
2556
3028
|
margin: 0 0 4pt;
|
|
2557
3029
|
}
|
|
2558
3030
|
.chempot-diagram-3d :global(.overlay-actions button) {
|
|
2559
|
-
border:
|
|
3031
|
+
border: none;
|
|
2560
3032
|
border-radius: 3px;
|
|
2561
3033
|
padding: 2px 6px;
|
|
2562
|
-
background: transparent;
|
|
3034
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
2563
3035
|
cursor: pointer;
|
|
2564
3036
|
color: var(--text-color, currentColor);
|
|
3037
|
+
font-size: 0.85em;
|
|
2565
3038
|
}
|
|
2566
3039
|
.chempot-diagram-3d :global(.overlay-search) {
|
|
2567
3040
|
display: flex;
|
|
@@ -2574,21 +3047,40 @@ function cycle_color_mode() {
|
|
|
2574
3047
|
min-width: 10em;
|
|
2575
3048
|
}
|
|
2576
3049
|
.chempot-diagram-3d :global(.formula-list) {
|
|
3050
|
+
display: flex;
|
|
3051
|
+
flex-wrap: wrap;
|
|
3052
|
+
gap: 3pt;
|
|
2577
3053
|
max-height: min(42vh, 18rem);
|
|
2578
3054
|
overflow: auto;
|
|
2579
|
-
|
|
2580
|
-
border-radius: 4px;
|
|
2581
|
-
padding: 4pt;
|
|
3055
|
+
padding: 2pt 0;
|
|
2582
3056
|
}
|
|
2583
3057
|
.chempot-diagram-3d :global(.formula-list label) {
|
|
2584
|
-
display: flex;
|
|
3058
|
+
display: inline-flex;
|
|
2585
3059
|
align-items: center;
|
|
2586
|
-
gap:
|
|
2587
|
-
|
|
3060
|
+
gap: 3pt;
|
|
3061
|
+
padding: 1px 5px;
|
|
3062
|
+
border-radius: 3px;
|
|
3063
|
+
font-size: 0.88em;
|
|
3064
|
+
cursor: pointer;
|
|
3065
|
+
background: color-mix(in srgb, currentColor 6%, transparent);
|
|
3066
|
+
}
|
|
3067
|
+
.chempot-diagram-3d :global(.formula-list label:has(input:checked)) {
|
|
3068
|
+
background: color-mix(in srgb, currentColor 16%, transparent);
|
|
3069
|
+
}
|
|
3070
|
+
.chempot-diagram-3d :global(.formula-list input[type='checkbox']) {
|
|
3071
|
+
position: absolute;
|
|
3072
|
+
width: 1px;
|
|
3073
|
+
height: 1px;
|
|
3074
|
+
overflow: hidden;
|
|
3075
|
+
clip: rect(0 0 0 0);
|
|
3076
|
+
}
|
|
3077
|
+
.chempot-diagram-3d :global(.formula-list label:has(input:focus-visible)) {
|
|
3078
|
+
outline: 2px solid Highlight;
|
|
3079
|
+
outline-offset: 1px;
|
|
2588
3080
|
}
|
|
2589
3081
|
.chempot-diagram-3d :global(.formula-color-dot) {
|
|
2590
|
-
width: 0.
|
|
2591
|
-
height: 0.
|
|
3082
|
+
width: 0.55em;
|
|
3083
|
+
height: 0.55em;
|
|
2592
3084
|
border-radius: 50%;
|
|
2593
3085
|
flex-shrink: 0;
|
|
2594
3086
|
}
|
|
@@ -2604,6 +3096,12 @@ function cycle_color_mode() {
|
|
|
2604
3096
|
min-width: 0;
|
|
2605
3097
|
padding: 2px 4px;
|
|
2606
3098
|
}
|
|
3099
|
+
.computing-state {
|
|
3100
|
+
display: flex;
|
|
3101
|
+
align-items: center;
|
|
3102
|
+
justify-content: center;
|
|
3103
|
+
min-height: 200px;
|
|
3104
|
+
}
|
|
2607
3105
|
.error-state {
|
|
2608
3106
|
display: flex;
|
|
2609
3107
|
flex-direction: column;
|
|
@@ -2620,19 +3118,27 @@ function cycle_color_mode() {
|
|
|
2620
3118
|
.axis-label {
|
|
2621
3119
|
font: bold 13px sans-serif;
|
|
2622
3120
|
}
|
|
3121
|
+
.axis-label :global(.axis-unit) {
|
|
3122
|
+
font-weight: 300;
|
|
3123
|
+
opacity: 0.7;
|
|
3124
|
+
}
|
|
2623
3125
|
.tick-label {
|
|
2624
3126
|
font-size: 10px;
|
|
2625
3127
|
color: var(--text-color, #333);
|
|
2626
3128
|
}
|
|
2627
3129
|
.domain-label {
|
|
2628
|
-
font:
|
|
3130
|
+
font-family: sans-serif;
|
|
2629
3131
|
color: var(--text-color, #333);
|
|
2630
3132
|
opacity: 0.7;
|
|
2631
3133
|
white-space: nowrap;
|
|
2632
3134
|
pointer-events: none;
|
|
2633
3135
|
}
|
|
3136
|
+
.formula-subscript {
|
|
3137
|
+
font-size: calc(11em / 12);
|
|
3138
|
+
vertical-align: -0.28em;
|
|
3139
|
+
}
|
|
2634
3140
|
.phase-tooltip {
|
|
2635
|
-
position:
|
|
3141
|
+
position: absolute;
|
|
2636
3142
|
max-width: min(32rem, 92vw);
|
|
2637
3143
|
background: var(
|
|
2638
3144
|
--tooltip-bg,
|
|
@@ -2642,14 +3148,14 @@ function cycle_color_mode() {
|
|
|
2642
3148
|
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
|
2643
3149
|
border-radius: 6px;
|
|
2644
3150
|
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.18);
|
|
2645
|
-
padding:
|
|
3151
|
+
padding: 4px 6px;
|
|
2646
3152
|
font-size: 12px;
|
|
2647
|
-
line-height: 1.
|
|
3153
|
+
line-height: 1.25;
|
|
2648
3154
|
pointer-events: none;
|
|
2649
3155
|
z-index: 100;
|
|
2650
3156
|
}
|
|
2651
3157
|
.phase-tooltip h4 {
|
|
2652
|
-
margin: 0 0
|
|
3158
|
+
margin: 0 0 2px;
|
|
2653
3159
|
font-size: 13px;
|
|
2654
3160
|
}
|
|
2655
3161
|
.phase-tooltip p {
|
|
@@ -2659,7 +3165,7 @@ function cycle_color_mode() {
|
|
|
2659
3165
|
text-overflow: ellipsis;
|
|
2660
3166
|
}
|
|
2661
3167
|
.phase-tooltip h5 {
|
|
2662
|
-
margin-top:
|
|
3168
|
+
margin-top: 4px;
|
|
2663
3169
|
margin-bottom: 0;
|
|
2664
3170
|
font-size: 12px;
|
|
2665
3171
|
font-weight: 600;
|