matterviz 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/FilePicker.svelte +40 -50
- package/dist/api/optimade.js +10 -7
- package/dist/app.css +15 -0
- package/dist/brillouin/BrillouinZone.svelte +18 -9
- package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
- package/dist/brillouin/compute.js +10 -16
- package/dist/brillouin/geometry.js +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
- package/dist/chempot-diagram/async-compute.svelte.js +12 -13
- package/dist/chempot-diagram/color.d.ts +21 -1
- package/dist/chempot-diagram/color.js +70 -1
- package/dist/chempot-diagram/compute.d.ts +7 -0
- package/dist/chempot-diagram/compute.js +85 -26
- package/dist/chempot-diagram/export.js +1 -6
- package/dist/chempot-diagram/temperature.js +9 -10
- package/dist/composition/BarChart.svelte +7 -9
- package/dist/composition/Composition.svelte +3 -8
- package/dist/composition/Formula.svelte +2 -2
- package/dist/composition/FormulaFilter.svelte +27 -93
- package/dist/composition/format.js +6 -8
- package/dist/composition/parse.js +50 -105
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +6 -17
- package/dist/convex-hull/ConvexHull2D.svelte +0 -2
- package/dist/convex-hull/ConvexHull3D.svelte +19 -47
- package/dist/convex-hull/ConvexHull4D.svelte +65 -95
- package/dist/convex-hull/StructurePopup.svelte +1 -1
- package/dist/convex-hull/gas-thermodynamics.js +21 -53
- package/dist/convex-hull/helpers.js +1 -7
- package/dist/convex-hull/thermodynamics.js +19 -27
- package/dist/coordination/CoordinationBarPlot.svelte +12 -21
- package/dist/element/BohrAtom.svelte +0 -1
- package/dist/element/ElementTile.svelte +26 -44
- package/dist/element/data.d.ts +2 -0
- package/dist/element/data.js +5 -1
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +1 -1
- package/dist/fermi-surface/FermiSurface.svelte +22 -23
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
- package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
- package/dist/fermi-surface/compute.js +22 -55
- package/dist/fermi-surface/parse.js +38 -70
- package/dist/file-viewer/JsonBrowser.svelte +1043 -0
- package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
- package/dist/file-viewer/PlotPanel.svelte +350 -0
- package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
- package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
- package/dist/file-viewer/detect.d.ts +11 -0
- package/dist/file-viewer/detect.js +381 -0
- package/dist/file-viewer/eligibility.d.ts +4 -0
- package/dist/file-viewer/eligibility.js +53 -0
- package/dist/file-viewer/host-protocol.d.ts +50 -0
- package/dist/file-viewer/host-transfer.d.ts +25 -0
- package/dist/file-viewer/host-transfer.js +42 -0
- package/dist/file-viewer/index.d.ts +5 -0
- package/dist/file-viewer/index.js +5 -0
- package/dist/file-viewer/main.d.ts +44 -0
- package/dist/file-viewer/main.js +535 -0
- package/dist/file-viewer/parse.d.ts +9 -0
- package/dist/file-viewer/parse.js +170 -0
- package/dist/file-viewer/plot-utils.d.ts +24 -0
- package/dist/file-viewer/plot-utils.js +191 -0
- package/dist/file-viewer/types.d.ts +4 -0
- package/dist/file-viewer/types.js +5 -0
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/io/ExportPane.svelte +1 -1
- package/dist/io/decompress.d.ts +2 -0
- package/dist/io/decompress.js +3 -2
- package/dist/io/export.d.ts +7 -4
- package/dist/io/export.js +70 -18
- package/dist/io/file-drop.js +60 -35
- package/dist/io/url-drop.d.ts +2 -0
- package/dist/io/url-drop.js +28 -16
- package/dist/isosurface/Isosurface.svelte +687 -196
- package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
- package/dist/isosurface/IsosurfaceControls.svelte +564 -85
- package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
- package/dist/isosurface/VolumeSlice.svelte +224 -0
- package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
- package/dist/isosurface/coloring.d.ts +20 -0
- package/dist/isosurface/coloring.js +118 -0
- package/dist/isosurface/geometry-worker-types.d.ts +38 -0
- package/dist/isosurface/geometry.worker.d.ts +1 -0
- package/dist/isosurface/geometry.worker.js +59 -0
- package/dist/isosurface/grid.d.ts +10 -0
- package/dist/isosurface/grid.js +31 -0
- package/dist/isosurface/index.d.ts +4 -0
- package/dist/isosurface/index.js +4 -0
- package/dist/isosurface/profile.d.ts +12 -0
- package/dist/isosurface/profile.js +19 -0
- package/dist/isosurface/sampling.d.ts +34 -0
- package/dist/isosurface/sampling.js +408 -0
- package/dist/isosurface/slice-rendering.d.ts +13 -0
- package/dist/isosurface/slice-rendering.js +79 -0
- package/dist/isosurface/slice.d.ts +22 -2
- package/dist/isosurface/slice.js +200 -116
- package/dist/isosurface/types.d.ts +24 -1
- package/dist/isosurface/types.js +120 -8
- package/dist/json-path.d.ts +4 -0
- package/dist/json-path.js +89 -0
- package/dist/labels.js +1 -1
- package/dist/layout/InfoCard.svelte +1 -3
- package/dist/layout/SettingsSection.svelte +52 -61
- package/dist/layout/SettingsSection.svelte.d.ts +6 -7
- package/dist/layout/ViewerChrome.svelte +7 -1
- package/dist/layout/json-tree/JsonNode.svelte +14 -48
- package/dist/layout/json-tree/JsonTree.svelte +48 -95
- package/dist/layout/json-tree/JsonValue.svelte +3 -18
- package/dist/layout/json-tree/utils.d.ts +0 -3
- package/dist/layout/json-tree/utils.js +50 -179
- package/dist/marching-cubes.d.ts +8 -1
- package/dist/marching-cubes.js +115 -48
- package/dist/math.d.ts +3 -2
- package/dist/math.js +29 -38
- package/dist/overlays/ContextMenu.svelte +15 -32
- package/dist/overlays/DraggablePane.svelte +39 -58
- package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
- package/dist/overlays/GlassChip.svelte +3 -1
- package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
- package/dist/phase-diagram/build-diagram.js +1 -6
- package/dist/phase-diagram/svg-to-diagram.js +9 -14
- package/dist/plot/bar/BarPlot.svelte +38 -42
- package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
- package/dist/plot/box/BoxPlot.svelte +27 -23
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +1 -1
- package/dist/plot/core/data-transform.d.ts +1 -2
- package/dist/plot/core/data-transform.js +0 -10
- package/dist/plot/core/layout.d.ts +12 -5
- package/dist/plot/core/layout.js +37 -27
- package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
- package/dist/plot/core/pan-zoom.svelte.js +34 -17
- package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -21
- package/dist/plot/core/svg.js +0 -1
- package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
- package/dist/plot/core/utils/hierarchy-chart.js +12 -0
- package/dist/plot/core/utils/hierarchy-labels.js +3 -2
- package/dist/plot/histogram/Histogram.svelte +21 -26
- package/dist/plot/sankey/sankey.js +13 -9
- package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
- package/dist/plot/scatter/ScatterPlot.svelte +67 -52
- package/dist/plot/scatter/adaptive-density.d.ts +1 -1
- package/dist/plot/scatter/adaptive-density.js +36 -20
- package/dist/plot/sunburst/Sunburst.svelte +20 -20
- package/dist/plot/treemap/Treemap.svelte +175 -60
- package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
- package/dist/plot/treemap/index.d.ts +1 -0
- package/dist/plot/treemap/labels.d.ts +38 -0
- package/dist/plot/treemap/labels.js +80 -0
- package/dist/plot/treemap/treemap.d.ts +4 -2
- package/dist/plot/treemap/treemap.js +5 -2
- package/dist/rdf/RdfPlot.svelte +17 -35
- package/dist/rdf/calc-rdf.js +59 -44
- package/dist/rdf/index.d.ts +0 -1
- package/dist/sanitize.js +84 -26
- package/dist/scene/SceneCamera.svelte +1 -1
- package/dist/scene/SceneCamera.svelte.d.ts +1 -1
- package/dist/scene/index.d.ts +1 -2
- package/dist/scene/index.js +1 -1
- package/dist/scene/{props.js → props.svelte.js} +15 -1
- package/dist/settings.js +159 -482
- package/dist/spectral/Bands.svelte +151 -158
- package/dist/spectral/BrillouinBandsDos.svelte +4 -19
- package/dist/spectral/Dos.svelte +26 -24
- package/dist/spectral/helpers.js +40 -105
- package/dist/structure/AtomLegend.svelte +8 -13
- package/dist/structure/AtomLegend.svelte.d.ts +2 -3
- package/dist/structure/CellSelect.svelte +35 -10
- package/dist/structure/Structure.svelte +177 -99
- package/dist/structure/Structure.svelte.d.ts +1 -1
- package/dist/structure/StructureCarousel.svelte +10 -3
- package/dist/structure/StructureControls.svelte +8 -26
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +65 -76
- package/dist/structure/StructureScene.svelte +75 -109
- package/dist/structure/StructureScene.svelte.d.ts +3 -1
- package/dist/structure/StructureViewport.svelte +120 -55
- package/dist/structure/StructureViewport.svelte.d.ts +3 -1
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +4 -3
- package/dist/structure/bond-order-perception.js +3 -8
- package/dist/structure/bonding.d.ts +0 -1
- package/dist/structure/bonding.js +16 -23
- package/dist/structure/export.js +50 -74
- package/dist/structure/format-detect.js +5 -7
- package/dist/structure/index.js +6 -12
- package/dist/structure/parse.js +38 -100
- package/dist/structure/partial-occupancy.js +7 -4
- package/dist/structure/pbc.js +8 -10
- package/dist/structure/polyhedra.js +5 -4
- package/dist/structure/prop-groups.d.ts +85 -0
- package/dist/structure/prop-groups.js +18 -0
- package/dist/symmetry/SymmetryStats.svelte +0 -4
- package/dist/symmetry/WyckoffTable.svelte +5 -4
- package/dist/symmetry/cell-transform.js +5 -14
- package/dist/symmetry/index.js +6 -9
- package/dist/table/HeatmapTable.svelte +61 -103
- package/dist/table/ToggleMenu.svelte +166 -44
- package/dist/table/ToggleMenu.svelte.d.ts +1 -1
- package/dist/theme/embedded.d.ts +18 -0
- package/dist/theme/embedded.js +206 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +1 -2
- package/dist/theme/themes.mjs +31 -60
- package/dist/tooltip/TooltipContent.svelte +1 -1
- package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
- package/dist/tooltip/index.d.ts +8 -1
- package/dist/trajectory/Trajectory.svelte +140 -120
- package/dist/trajectory/TrajectoryError.svelte +1 -66
- package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
- package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
- package/dist/trajectory/extract.js +19 -29
- package/dist/trajectory/format-detect.d.ts +2 -0
- package/dist/trajectory/format-detect.js +7 -5
- package/dist/trajectory/frame-reader.js +3 -4
- package/dist/trajectory/helpers.js +16 -49
- package/dist/trajectory/index.js +2 -8
- package/dist/trajectory/parse/ase.js +1 -1
- package/dist/trajectory/parse/index.d.ts +4 -3
- package/dist/trajectory/parse/index.js +12 -17
- package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
- package/dist/trajectory/parse/vaspout-h5.js +1 -4
- package/dist/trajectory/parse/xyz.js +48 -2
- package/dist/trajectory/plotting.js +14 -37
- package/dist/xrd/XrdPlot.svelte +43 -68
- package/dist/xrd/calc-xrd.js +22 -58
- package/dist/xrd/parse.js +13 -22
- package/package.json +40 -8
- package/readme.md +6 -5
- package/dist/scene/types.d.ts +0 -26
- package/dist/tooltip/types.d.ts +0 -8
- package/dist/trajectory/constants.d.ts +0 -6
- package/dist/trajectory/constants.js +0 -11
- /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
- /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
|
@@ -50,9 +50,7 @@ export const atomic_num_to_symbols = (atomic_composition) => {
|
|
|
50
50
|
export const atomic_symbol_to_num = (symbol_composition) => {
|
|
51
51
|
const atomic_composition = {};
|
|
52
52
|
for (const [symbol, amount] of Object.entries(symbol_composition)) {
|
|
53
|
-
|
|
54
|
-
throw new Error(`Invalid element symbol: ${symbol}`);
|
|
55
|
-
const atomic_num = SYMBOL_TO_ATOMIC_NUMBER[symbol];
|
|
53
|
+
const atomic_num = is_elem_symbol(symbol) ? SYMBOL_TO_ATOMIC_NUMBER[symbol] : undefined;
|
|
56
54
|
if (!atomic_num)
|
|
57
55
|
throw new Error(`Invalid element symbol: ${symbol}`);
|
|
58
56
|
if (amount > 0) {
|
|
@@ -138,9 +136,7 @@ export const fractional_composition = (composition, by_weight = false) => {
|
|
|
138
136
|
const filtered = Object.fromEntries(Object.entries(composition).filter(([, amount]) => amount > 0));
|
|
139
137
|
if (by_weight) {
|
|
140
138
|
const element_weights = Object.fromEntries(Object.entries(filtered).map(([element, amount]) => {
|
|
141
|
-
|
|
142
|
-
throw new Error(`Unknown element: ${element}`);
|
|
143
|
-
const atomic_mass = ATOMIC_WEIGHTS.get(element);
|
|
139
|
+
const atomic_mass = is_elem_symbol(element) ? ATOMIC_WEIGHTS.get(element) : undefined;
|
|
144
140
|
if (!atomic_mass)
|
|
145
141
|
throw new Error(`Unknown element: ${element}`);
|
|
146
142
|
return [element, amount * atomic_mass];
|
|
@@ -222,46 +218,30 @@ const parse_oxidation_state = (oxidation_str) => {
|
|
|
222
218
|
export const parse_formula_with_oxidation = (formula, strict = false) => {
|
|
223
219
|
const elements = [];
|
|
224
220
|
const cleaned_formula = expand_parentheses(formula.replaceAll(/\s/g, ``));
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
// Followed by one of:
|
|
228
|
-
// - oxidation then optional count: (?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])(count?)
|
|
229
|
-
// - count then optional oxidation: count(?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])?
|
|
230
|
-
// - just oxidation: (?:\^([+-]?\d+[+-]?|[+-])|\[([+-]?\d+[+-]?|[+-])\])
|
|
231
|
-
// - just count: count
|
|
232
|
-
// - neither
|
|
221
|
+
// Element symbol followed by optional oxidation state (^2+ or [2+] syntax) and
|
|
222
|
+
// optional count, in either order: Fe^2+O3, Fe[2+]O3, Fe3^2+, Fe2
|
|
233
223
|
const regex = /(?<element>[A-Z][a-z]?)(?:(?:\^(?<oxi_caret>[+-]?\d+[+-]?|[+-])|\[(?<oxi_bracket>[+-]?\d+[+-]?|[+-])\])(?<count_after>(?:\d+(?:\.\d+)?|\.\d+)?)|(?<count_before>(?:\d+(?:\.\d+)?|\.\d+))(?:\^(?<oxi_caret_2>[+-]?\d+[+-]?|[+-])|\[(?<oxi_bracket_2>[+-]?\d+[+-]?|[+-])\])?)?/g;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
// Count can be in group 4 (after oxidation) or 5 (before oxidation)
|
|
240
|
-
const oxidation_str = match[2] || match[3] || match[6] || match[7];
|
|
241
|
-
const count_str = match[4] || match[5];
|
|
242
|
-
const count = parse_count(count_str);
|
|
224
|
+
for (const match of cleaned_formula.matchAll(regex)) {
|
|
225
|
+
const groups = match.groups ?? {};
|
|
226
|
+
const element = groups.element;
|
|
227
|
+
const oxidation_str = groups.oxi_caret || groups.oxi_bracket || groups.oxi_caret_2 || groups.oxi_bracket_2;
|
|
228
|
+
const count = parse_count(groups.count_after || groups.count_before);
|
|
243
229
|
if (!is_elem_symbol(element))
|
|
244
230
|
throw new Error(`Invalid element symbol: ${element}`);
|
|
245
231
|
const oxidation_state = oxidation_str ? parse_oxidation_state(oxidation_str) : undefined;
|
|
246
|
-
// Find or add element entry
|
|
247
232
|
const existing = elements.find((el) => el.element === element);
|
|
248
|
-
if (existing) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
if (oxidation_state === undefined) {
|
|
252
|
-
// No oxidation state in current match, nothing to do
|
|
253
|
-
}
|
|
254
|
-
else if (existing.oxidation_state === undefined) {
|
|
255
|
-
// Set oxidation state on first occurrence
|
|
256
|
-
existing.oxidation_state = oxidation_state;
|
|
257
|
-
}
|
|
258
|
-
else if (strict && existing.oxidation_state !== oxidation_state) {
|
|
259
|
-
// In strict mode, throw on conflicting oxidation states
|
|
260
|
-
throw new Error(`Conflicting oxidation states for ${element}: ${format_state(existing.oxidation_state)} and ${format_state(oxidation_state)}`);
|
|
261
|
-
}
|
|
233
|
+
if (!existing) {
|
|
234
|
+
elements.push({ element, amount: count, oxidation_state, orig_idx: elements.length });
|
|
235
|
+
continue;
|
|
262
236
|
}
|
|
263
|
-
|
|
264
|
-
|
|
237
|
+
existing.amount += count;
|
|
238
|
+
if (oxidation_state === undefined)
|
|
239
|
+
continue;
|
|
240
|
+
if (existing.oxidation_state === undefined) {
|
|
241
|
+
existing.oxidation_state = oxidation_state; // Set on first occurrence
|
|
242
|
+
}
|
|
243
|
+
else if (strict && existing.oxidation_state !== oxidation_state) {
|
|
244
|
+
throw new Error(`Conflicting oxidation states for ${element}: ${format_state(existing.oxidation_state)} and ${format_state(oxidation_state)}`);
|
|
265
245
|
}
|
|
266
246
|
}
|
|
267
247
|
return elements;
|
|
@@ -298,20 +278,12 @@ export function generate_chem_sys_subspaces(input) {
|
|
|
298
278
|
let elements;
|
|
299
279
|
if (typeof input === `string`)
|
|
300
280
|
elements = extract_formula_elements(input);
|
|
301
|
-
else if (Array.isArray(input)) {
|
|
302
|
-
const uniq = [...new Set(input)];
|
|
303
|
-
for (const elem of uniq) {
|
|
304
|
-
if (!is_elem_symbol(elem))
|
|
305
|
-
throw new Error(`Invalid element symbol: ${elem}`);
|
|
306
|
-
}
|
|
307
|
-
elements = uniq;
|
|
308
|
-
}
|
|
309
281
|
else {
|
|
310
|
-
|
|
311
|
-
|
|
282
|
+
const symbols = Array.isArray(input) ? input : Object.keys(input);
|
|
283
|
+
elements = [...new Set(symbols)];
|
|
284
|
+
for (const elem of elements) {
|
|
312
285
|
if (!is_elem_symbol(elem))
|
|
313
286
|
throw new Error(`Invalid element symbol: ${elem}`);
|
|
314
|
-
elements.push(elem);
|
|
315
287
|
}
|
|
316
288
|
}
|
|
317
289
|
const sorted = [...elements].sort();
|
|
@@ -343,7 +315,7 @@ export const has_wildcards = (input) => input.includes(`*`);
|
|
|
343
315
|
// Throws if any non-wildcard token is not a valid element symbol.
|
|
344
316
|
export function parse_chemsys_with_wildcards(input) {
|
|
345
317
|
const tokens = input
|
|
346
|
-
.replaceAll(
|
|
318
|
+
.replaceAll(`-`, `,`)
|
|
347
319
|
.split(`,`)
|
|
348
320
|
.map((tok) => tok.trim())
|
|
349
321
|
.filter(Boolean);
|
|
@@ -378,32 +350,21 @@ export const ELEM_WILDCARD = {
|
|
|
378
350
|
// "**O4" -> *(1), *(1), O(4) - matches ternary oxides with 1:1:4 ratio
|
|
379
351
|
export function parse_formula_with_wildcards(formula) {
|
|
380
352
|
const tokens = [];
|
|
381
|
-
//
|
|
353
|
+
// Swap * for a placeholder so parentheses expansion treats it as a pseudo-element
|
|
382
354
|
let cleaned = formula.replaceAll(/\s/g, ``);
|
|
383
|
-
// Protect wildcards from parentheses expansion by replacing * with placeholder
|
|
384
355
|
cleaned = cleaned.replace(ELEM_WILDCARD.to_placeholder, ELEM_WILDCARD.placeholder);
|
|
385
356
|
cleaned = expand_parentheses(cleaned);
|
|
386
|
-
// Restore wildcards
|
|
387
357
|
cleaned = cleaned.replace(ELEM_WILDCARD.from_placeholder, `*`);
|
|
388
|
-
//
|
|
389
|
-
// 1. Standard element symbol with optional decimal count
|
|
390
|
-
// 2. Wildcard with optional decimal count
|
|
358
|
+
// Match element symbol or wildcard, each with optional decimal count
|
|
391
359
|
const regex = /(?<element>[A-Z][a-z]?)(?<count>(?:\d+(?:\.\d+)?|\.\d+)?)|(?<wildcard>\*)(?<wildcard_count>(?:\d+(?:\.\d+)?|\.\d+)?)/g;
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
if (
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
}
|
|
399
|
-
else if (match[1]) {
|
|
400
|
-
// Element symbol match
|
|
401
|
-
const element = match[1];
|
|
402
|
-
const count = parse_count(match[2]);
|
|
403
|
-
if (!is_elem_symbol(element)) {
|
|
360
|
+
for (const match of cleaned.matchAll(regex)) {
|
|
361
|
+
const { element, count, wildcard, wildcard_count } = match.groups ?? {};
|
|
362
|
+
if (wildcard)
|
|
363
|
+
tokens.push({ element: null, count: parse_count(wildcard_count) });
|
|
364
|
+
else if (element) {
|
|
365
|
+
if (!is_elem_symbol(element))
|
|
404
366
|
throw new Error(`Invalid element symbol: ${element}`);
|
|
405
|
-
}
|
|
406
|
-
tokens.push({ element, count });
|
|
367
|
+
tokens.push({ element, count: parse_count(count) });
|
|
407
368
|
}
|
|
408
369
|
}
|
|
409
370
|
return tokens;
|
|
@@ -436,48 +397,32 @@ export function matches_chemsys_wildcard(formula, explicit_elements, wildcard_co
|
|
|
436
397
|
export function matches_formula_wildcard(formula, pattern) {
|
|
437
398
|
try {
|
|
438
399
|
const composition = parse_formula(formula);
|
|
439
|
-
//
|
|
440
|
-
|
|
400
|
+
// Split pattern into merged explicit element counts (e.g. "LiLi" -> Li: 2)
|
|
401
|
+
// and wildcard counts
|
|
402
|
+
const explicit_counts = new Map();
|
|
441
403
|
const wildcard_counts = [];
|
|
442
404
|
for (const token of pattern) {
|
|
443
|
-
if (token.element === null)
|
|
405
|
+
if (token.element === null)
|
|
444
406
|
wildcard_counts.push(token.count);
|
|
445
|
-
}
|
|
446
407
|
else {
|
|
447
|
-
|
|
448
|
-
const existing = explicit_requirements.find((req) => req.element === token.element);
|
|
449
|
-
if (existing) {
|
|
450
|
-
existing.count += token.count;
|
|
451
|
-
}
|
|
452
|
-
else {
|
|
453
|
-
explicit_requirements.push({ element: token.element, count: token.count });
|
|
454
|
-
}
|
|
408
|
+
explicit_counts.set(token.element, (explicit_counts.get(token.element) ?? 0) + token.count);
|
|
455
409
|
}
|
|
456
410
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
for (const req of explicit_requirements) {
|
|
460
|
-
if (composition[req.element] !== req.count)
|
|
411
|
+
for (const [element, count] of explicit_counts) {
|
|
412
|
+
if (composition[element] !== count)
|
|
461
413
|
return false;
|
|
462
|
-
used_elements.add(req.element);
|
|
463
414
|
}
|
|
464
|
-
//
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
415
|
+
// Remaining elements are wildcard candidates. Each wildcard needs a distinct
|
|
416
|
+
// element with exactly its count, so sorting both count lists reduces the
|
|
417
|
+
// matching to a positional comparison
|
|
418
|
+
const remaining_counts = Object.entries(composition)
|
|
419
|
+
.filter(([elem]) => !explicit_counts.has(elem))
|
|
420
|
+
.map(([, count]) => count)
|
|
421
|
+
.sort((cnt_a, cnt_b) => cnt_a - cnt_b);
|
|
422
|
+
if (remaining_counts.length !== wildcard_counts.length)
|
|
470
423
|
return false;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
const sorted_remaining = [...remaining_elements].sort((a, b) => a.count - b.count);
|
|
474
|
-
const sorted_wildcards = [...wildcard_counts].sort((a, b) => a - b);
|
|
475
|
-
// Check if counts match (simple comparison works because we need exact matches)
|
|
476
|
-
for (let idx = 0; idx < sorted_wildcards.length; idx++) {
|
|
477
|
-
if (sorted_remaining[idx].count !== sorted_wildcards[idx])
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
return true;
|
|
424
|
+
wildcard_counts.sort((cnt_a, cnt_b) => cnt_a - cnt_b);
|
|
425
|
+
return wildcard_counts.every((count, idx) => remaining_counts[idx] === count);
|
|
481
426
|
}
|
|
482
427
|
catch {
|
|
483
428
|
return false;
|
package/dist/constants.d.ts
CHANGED
|
@@ -13,7 +13,6 @@ export declare const STRUCT_KEYWORDS_STRICT: readonly string[];
|
|
|
13
13
|
export declare const TRAJ_KEYWORDS_REGEX: RegExp;
|
|
14
14
|
export declare const STRUCT_KEYWORDS_REGEX: RegExp;
|
|
15
15
|
export declare const STRUCT_KEYWORDS_STRICT_REGEX: RegExp;
|
|
16
|
-
export declare const TRAJ_KEYWORDS_SIMPLE_REGEX: RegExp;
|
|
17
16
|
export declare const TRAJ_EXTENSIONS: readonly string[];
|
|
18
17
|
export declare const TRAJ_EXTENSIONS_REGEX: RegExp;
|
|
19
18
|
export declare const STRUCTURE_EXTENSIONS: readonly string[];
|
package/dist/constants.js
CHANGED
|
@@ -14,11 +14,13 @@ export const COMPRESSION_EXTENSIONS = Object.freeze(Object.values(COMPRESSION_FO
|
|
|
14
14
|
export const TRAJ_KEYWORDS = Object.freeze([
|
|
15
15
|
`trajectory`,
|
|
16
16
|
`traj`,
|
|
17
|
+
`relaxation`,
|
|
17
18
|
`relax`,
|
|
18
19
|
`npt`,
|
|
19
20
|
`nvt`,
|
|
20
21
|
`nve`,
|
|
21
22
|
`qha`,
|
|
23
|
+
`dpmd`, // DeePMD trajectory outputs (before `md` so the longer token wins)
|
|
22
24
|
`md`,
|
|
23
25
|
`dynamics`,
|
|
24
26
|
`simulation`,
|
|
@@ -41,26 +43,13 @@ export const STRUCT_KEYWORDS = Object.freeze([
|
|
|
41
43
|
`phonopy`,
|
|
42
44
|
]);
|
|
43
45
|
// More restrictive keywords for JSON/YAML files (excludes generic terms like "data")
|
|
44
|
-
export const STRUCT_KEYWORDS_STRICT = Object.freeze(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
`crystal`,
|
|
49
|
-
`material`,
|
|
50
|
-
`lattice`,
|
|
51
|
-
`geometry`,
|
|
52
|
-
`unit_cell`,
|
|
53
|
-
`unitcell`,
|
|
54
|
-
`atoms`,
|
|
55
|
-
`sites`,
|
|
56
|
-
`phono3py`,
|
|
57
|
-
`phonopy`,
|
|
58
|
-
]);
|
|
59
|
-
// Regex patterns for keyword matching
|
|
46
|
+
export const STRUCT_KEYWORDS_STRICT = Object.freeze(STRUCT_KEYWORDS.filter((keyword) => keyword !== `data`));
|
|
47
|
+
// Regex patterns for keyword matching. Keywords must be delimited on both sides
|
|
48
|
+
// (`md_300K`, `si_md.log`) so bare prefixes like `md/notes.log` or `mdp_run` do not
|
|
49
|
+
// match. `relaxation` is listed explicitly (not only via the `relax` prefix).
|
|
60
50
|
export const TRAJ_KEYWORDS_REGEX = new RegExp(`(^|[-_.])(${TRAJ_KEYWORDS.join(`|`)})([-_.]|$)`, `i`);
|
|
61
51
|
export const STRUCT_KEYWORDS_REGEX = new RegExp(`(${STRUCT_KEYWORDS.join(`|`)})`, `i`);
|
|
62
52
|
export const STRUCT_KEYWORDS_STRICT_REGEX = new RegExp(`(${STRUCT_KEYWORDS_STRICT.join(`|`)})`, `i`);
|
|
63
|
-
export const TRAJ_KEYWORDS_SIMPLE_REGEX = new RegExp(`(${TRAJ_KEYWORDS.join(`|`)})`, `i`);
|
|
64
53
|
// Build a case-insensitive `\.(ext1|ext2|...)$` regex from extensions (leading dots stripped)
|
|
65
54
|
const ext_regex = (exts) => new RegExp(`\\.(${exts.map((ext) => ext.slice(1)).join(`|`)})$`, `i`);
|
|
66
55
|
// File extensions for different file types
|
|
@@ -95,10 +95,8 @@
|
|
|
95
95
|
const controls_config = $derived(normalize_show_controls(show_controls))
|
|
96
96
|
const merged_config = $derived({
|
|
97
97
|
...default_hull_config,
|
|
98
|
-
point_size: 6, // Binary diagrams use slightly smaller points
|
|
99
98
|
...config,
|
|
100
99
|
colors: { ...default_hull_config.colors, ...config.colors },
|
|
101
|
-
margin: { t: 40, r: 40, b: 60, l: 60, ...config.margin },
|
|
102
100
|
})
|
|
103
101
|
|
|
104
102
|
// Narrow deriveds to primitive fields so heavy downstream deriveds (scatter series,
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import { sanitize_html } from '../sanitize'
|
|
17
17
|
import { Spinner } from '../feedback'
|
|
18
18
|
import { format_num } from '../labels'
|
|
19
|
-
import { to_radians, type
|
|
19
|
+
import { to_radians, type Vec2, type Vec3 } from '../math'
|
|
20
20
|
import { ColorBar } from '../plot'
|
|
21
21
|
import {
|
|
22
22
|
centered_rect,
|
|
@@ -123,7 +123,6 @@
|
|
|
123
123
|
...default_hull_config,
|
|
124
124
|
...config,
|
|
125
125
|
colors: { ...default_hull_config.colors, ...config.colors },
|
|
126
|
-
margin: { t: 40, r: 40, b: 60, l: 60, ...config.margin },
|
|
127
126
|
})
|
|
128
127
|
|
|
129
128
|
// Shared reactive data pipeline (temperature → gas → energy mode → hull data → threshold)
|
|
@@ -187,7 +186,7 @@
|
|
|
187
186
|
})
|
|
188
187
|
|
|
189
188
|
// Cached hull model for e_above_hull queries; recompute only when faces change
|
|
190
|
-
|
|
189
|
+
const hull_model = $derived(thermo.build_lower_hull_model(hull_faces))
|
|
191
190
|
|
|
192
191
|
// Enrich coords with e_above_hull from cached hull model (before filtering)
|
|
193
192
|
const all_enriched_entries = $derived.by(() => {
|
|
@@ -432,7 +431,7 @@
|
|
|
432
431
|
)
|
|
433
432
|
|
|
434
433
|
// Cache energy color scale per frame/setting
|
|
435
|
-
const energy_color_scale = $derived
|
|
434
|
+
const energy_color_scale = $derived(
|
|
436
435
|
helpers.get_energy_color_scale(color_mode, color_scale, plot_entries),
|
|
437
436
|
)
|
|
438
437
|
|
|
@@ -472,65 +471,38 @@
|
|
|
472
471
|
}
|
|
473
472
|
}
|
|
474
473
|
|
|
474
|
+
// Dashed triangle outline: base triangle at E_form = 0, bottom triangle at the most
|
|
475
|
+
// negative formation energy, and vertical edges connecting corresponding corners
|
|
475
476
|
function draw_structure_outline(): void {
|
|
476
477
|
if (!ctx || !canvas) return
|
|
477
478
|
|
|
478
|
-
// Set consistent style for all triangle structure lines
|
|
479
479
|
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
480
480
|
ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
|
|
481
|
-
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
|
|
481
|
+
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
|
|
482
482
|
|
|
483
|
-
|
|
484
|
-
draw_triangle_structure()
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function draw_triangle_structure(): void {
|
|
488
|
-
if (!ctx || !canvas) return
|
|
489
|
-
|
|
490
|
-
// Get formation energy range for vertical edges
|
|
491
|
-
const e_form_min = energy_range.min // Includes 0 for elemental references
|
|
492
|
-
const e_form_max = energy_range.max // Includes 0 for elemental references
|
|
493
|
-
|
|
494
|
-
// Draw base triangle edges (top triangle at formation energy = 0)
|
|
483
|
+
const { min: e_form_min } = energy_range
|
|
495
484
|
const triangle_edges = get_triangle_edges()
|
|
496
485
|
ctx.beginPath()
|
|
497
|
-
for (const [
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
486
|
+
for (const z_plane of [0, e_form_min]) {
|
|
487
|
+
for (const [v1, v2] of triangle_edges) {
|
|
488
|
+
const proj1 = project_3d_point(v1.x, v1.y, z_plane)
|
|
489
|
+
const proj2 = project_3d_point(v2.x, v2.y, z_plane)
|
|
490
|
+
ctx.moveTo(proj1.x, proj1.y)
|
|
491
|
+
ctx.lineTo(proj2.x, proj2.y)
|
|
492
|
+
}
|
|
503
493
|
}
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
// Draw vertical edges from corners (from most negative to 0 formation energy)
|
|
507
|
-
const vertical_edges = get_triangle_vertical_edges(e_form_min, e_form_max)
|
|
508
|
-
ctx.beginPath()
|
|
509
|
-
for (const [v1, v2] of vertical_edges) {
|
|
494
|
+
for (const [v1, v2] of get_triangle_vertical_edges(e_form_min, 0)) {
|
|
510
495
|
const proj1 = project_3d_point(v1.x, v1.y, v1.z)
|
|
511
496
|
const proj2 = project_3d_point(v2.x, v2.y, v2.z)
|
|
512
|
-
|
|
513
|
-
ctx.moveTo(proj1.x, proj1.y)
|
|
514
|
-
ctx.lineTo(proj2.x, proj2.y)
|
|
515
|
-
}
|
|
516
|
-
ctx.stroke()
|
|
517
|
-
|
|
518
|
-
// Draw bottom triangle (connecting the bottom tips of vertical lines)
|
|
519
|
-
const bottom_triangle_edges = get_triangle_edges()
|
|
520
|
-
ctx.beginPath()
|
|
521
|
-
for (const [v1, v2] of bottom_triangle_edges) {
|
|
522
|
-
const proj1 = project_3d_point(v1.x, v1.y, e_form_min) // Bottom triangle at most negative energy
|
|
523
|
-
const proj2 = project_3d_point(v2.x, v2.y, e_form_min)
|
|
524
|
-
|
|
525
497
|
ctx.moveTo(proj1.x, proj1.y)
|
|
526
498
|
ctx.lineTo(proj2.x, proj2.y)
|
|
527
499
|
}
|
|
528
500
|
ctx.stroke()
|
|
529
501
|
|
|
530
|
-
// Reset stroke style
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
ctx.setLineDash([])
|
|
502
|
+
// Reset stroke style and line dash for subsequent drawing operations
|
|
503
|
+
ctx.strokeStyle =
|
|
504
|
+
getComputedStyle(canvas).getPropertyValue(`--hull-edge-color`) || `#212121`
|
|
505
|
+
ctx.setLineDash([])
|
|
534
506
|
}
|
|
535
507
|
|
|
536
508
|
function draw_element_labels(): void {
|
|
@@ -96,13 +96,24 @@
|
|
|
96
96
|
...default_hull_config,
|
|
97
97
|
...config,
|
|
98
98
|
colors: { ...default_hull_config.colors, ...config.colors },
|
|
99
|
-
margin: { t: 60, r: 60, b: 60, l: 60, ...config.margin },
|
|
100
99
|
})
|
|
101
100
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
101
|
+
// Cache resolved canvas colors and refresh them only when the theme changes.
|
|
102
|
+
const initial_text_color = helpers.get_canvas_text_color(is_dark_mode())
|
|
103
|
+
let text_color = $state(initial_text_color)
|
|
104
|
+
let hull_edge_color = $state(initial_text_color)
|
|
105
|
+
function refresh_canvas_colors(dark_mode: boolean): void {
|
|
106
|
+
const next_text_color = helpers.get_canvas_text_color(dark_mode)
|
|
107
|
+
text_color = next_text_color
|
|
108
|
+
const resolved_edge_color = canvas
|
|
109
|
+
? getComputedStyle(canvas).getPropertyValue(`--hull-edge-color`).trim()
|
|
110
|
+
: ``
|
|
111
|
+
hull_edge_color = resolved_edge_color || next_text_color
|
|
112
|
+
}
|
|
113
|
+
$effect(() => {
|
|
114
|
+
refresh_canvas_colors(is_dark_mode())
|
|
115
|
+
return watch_dark_mode(refresh_canvas_colors)
|
|
116
|
+
})
|
|
106
117
|
|
|
107
118
|
// Shared reactive data pipeline (temperature → gas → energy mode → hull data → threshold)
|
|
108
119
|
// Explicit generic breaks the circular type inference through the all_enriched_entries thunk
|
|
@@ -132,31 +143,33 @@
|
|
|
132
143
|
const elements = $derived(hull_data.elements)
|
|
133
144
|
const plot_entries = $derived(hull_data.plot_entries)
|
|
134
145
|
|
|
146
|
+
// Entry → 4D hull point (3 barycentric composition fractions + e_form as w),
|
|
147
|
+
// or null when the entry lacks a finite formation energy or valid composition
|
|
148
|
+
function to_point_4d(entry: ConvexHullEntry): Point4D | null {
|
|
149
|
+
if (
|
|
150
|
+
!Number.isFinite(entry.e_form_per_atom) ||
|
|
151
|
+
![entry.x, entry.y, entry.z].every(Number.isFinite)
|
|
152
|
+
)
|
|
153
|
+
return null
|
|
154
|
+
const amounts = elements.map((el) => entry.composition[el] || 0)
|
|
155
|
+
const total = amounts.reduce((sum, amt) => sum + amt, 0)
|
|
156
|
+
if (!(total > 0)) return null
|
|
157
|
+
const [x, y, z] = amounts.map((amt) => amt / total)
|
|
158
|
+
return [x, y, z].every(Number.isFinite)
|
|
159
|
+
? { x, y, z, w: entry.e_form_per_atom ?? NaN }
|
|
160
|
+
: null
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
// Compute 4D hull for visualization (always compute when we have formation energies)
|
|
136
164
|
const hull_4d = $derived.by(() => {
|
|
137
165
|
if (elements.length !== 4) return []
|
|
138
166
|
|
|
139
167
|
try {
|
|
140
|
-
//
|
|
141
|
-
const
|
|
142
|
-
(ent) => !ent.exclude_from_hull
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// Convert to 4D points for hull computation using barycentric coordinates (composition fractions)
|
|
146
|
-
const points_4d: Point4D[] = coords
|
|
147
|
-
.filter(
|
|
148
|
-
(ent) =>
|
|
149
|
-
Number.isFinite(ent.e_form_per_atom) &&
|
|
150
|
-
[ent.x, ent.y, ent.z].every(Number.isFinite),
|
|
151
|
-
)
|
|
152
|
-
.map((ent) => {
|
|
153
|
-
const amounts = elements.map((el) => ent.composition[el] || 0)
|
|
154
|
-
const total = amounts.reduce((sum, amt) => sum + amt, 0)
|
|
155
|
-
if (!(total > 0)) return { x: NaN, y: NaN, z: NaN, w: NaN }
|
|
156
|
-
const [x, y, z] = amounts.map((amt) => amt / total)
|
|
157
|
-
return { x, y, z, w: ent.e_form_per_atom ?? NaN }
|
|
158
|
-
})
|
|
159
|
-
.filter((point) => [point.x, point.y, point.z, point.w].every(Number.isFinite))
|
|
168
|
+
// Excluded entries don't participate in hull construction
|
|
169
|
+
const points_4d = compute_4d_coords(pd_data.entries, elements)
|
|
170
|
+
.filter((ent) => !ent.exclude_from_hull)
|
|
171
|
+
.map(to_point_4d)
|
|
172
|
+
.filter((point): point is Point4D => point !== null)
|
|
160
173
|
|
|
161
174
|
if (points_4d.length < 5) return [] // Need at least 5 points for 4D hull
|
|
162
175
|
|
|
@@ -177,18 +190,8 @@
|
|
|
177
190
|
|
|
178
191
|
// Build 4D points, tracking original indices for mapping hull distances back
|
|
179
192
|
const valid = coords.flatMap((entry, idx) => {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
![entry.x, entry.y, entry.z].every(Number.isFinite)
|
|
183
|
-
)
|
|
184
|
-
return []
|
|
185
|
-
const amounts = elements.map((el) => entry.composition[el] || 0)
|
|
186
|
-
const total = amounts.reduce((sum, amt) => sum + amt, 0)
|
|
187
|
-
if (!(total > 0)) return []
|
|
188
|
-
const [x, y, z] = amounts.map((amt) => amt / total)
|
|
189
|
-
return [x, y, z].every(Number.isFinite)
|
|
190
|
-
? [{ idx, pt: { x, y, z, w: entry.e_form_per_atom ?? NaN } }]
|
|
191
|
-
: []
|
|
193
|
+
const pt = to_point_4d(entry)
|
|
194
|
+
return pt ? [{ idx, pt }] : []
|
|
192
195
|
})
|
|
193
196
|
const raw_dists = thermo.compute_e_above_hull_4d(
|
|
194
197
|
valid.map((item) => item.pt),
|
|
@@ -206,7 +209,7 @@
|
|
|
206
209
|
}
|
|
207
210
|
})
|
|
208
211
|
|
|
209
|
-
let canvas
|
|
212
|
+
let canvas = $state<HTMLCanvasElement>()
|
|
210
213
|
let ctx: CanvasRenderingContext2D | null = null
|
|
211
214
|
|
|
212
215
|
// Camera state - following Materials Project's 3D camera setup
|
|
@@ -293,13 +296,11 @@
|
|
|
293
296
|
// Re-render when important state changes
|
|
294
297
|
$effect(() => {
|
|
295
298
|
// oxfmt-ignore
|
|
296
|
-
void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_data.visible_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, elements] // track reactively
|
|
299
|
+
void [show_hull_faces, color_mode, color_scale, camera.rotation_x, camera.rotation_y, camera.zoom, camera.center_x, camera.center_y, plot_entries, hull_data.visible_entries, hull_face_color, hull_face_opacity, hull_face_color_mode, element_colors, text_color, hull_edge_color, elements] // track reactively
|
|
297
300
|
|
|
298
301
|
render_once()
|
|
299
302
|
})
|
|
300
303
|
|
|
301
|
-
// Visibility toggles are now bindable props
|
|
302
|
-
|
|
303
304
|
// Smart label defaults: hide labels for large datasets. Applied once per dataset
|
|
304
305
|
// (keyed on the entries prop) so later entry-count changes from temperature/gas
|
|
305
306
|
// filtering don't clobber the user's label toggles.
|
|
@@ -354,7 +355,7 @@
|
|
|
354
355
|
)
|
|
355
356
|
|
|
356
357
|
// Cache energy color scale per frame/setting
|
|
357
|
-
const energy_color_scale = $derived
|
|
358
|
+
const energy_color_scale = $derived(
|
|
358
359
|
helpers.get_energy_color_scale(color_mode, color_scale, plot_entries),
|
|
359
360
|
)
|
|
360
361
|
|
|
@@ -371,16 +372,11 @@
|
|
|
371
372
|
): { x: number; y: number; depth: number } {
|
|
372
373
|
if (!canvas) return { x: 0, y: 0, depth: 0 }
|
|
373
374
|
|
|
374
|
-
// Center coordinates around tetrahedron
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
const centroid_y = (0 + Math.sqrt(3) / 2 + Math.sqrt(3) / 6 + 0) / 4 // = √3/6
|
|
380
|
-
const centroid_z = (0 + 0 + Math.sqrt(6) / 3 + 0) / 4 // = √6/12
|
|
381
|
-
centered_x = x - centroid_x
|
|
382
|
-
centered_y = y - centroid_y
|
|
383
|
-
centered_z = z - centroid_z
|
|
375
|
+
// Center coordinates around the tetrahedron centroid: average of vertices
|
|
376
|
+
// (1,0,0), (0.5,√3/2,0), (0.5,√3/6,√6/3), (0,0,0)
|
|
377
|
+
const centered_x = x - (1 + 0.5 + 0.5 + 0) / 4 // centroid_x = 0.5
|
|
378
|
+
const centered_y = y - (0 + Math.sqrt(3) / 2 + Math.sqrt(3) / 6 + 0) / 4 // = √3/6
|
|
379
|
+
const centered_z = z - (0 + 0 + Math.sqrt(6) / 3 + 0) / 4 // = √6/12
|
|
384
380
|
|
|
385
381
|
// Apply 3D transformations around the centered coordinates
|
|
386
382
|
const cos_x = Math.cos(camera.rotation_x)
|
|
@@ -408,56 +404,33 @@
|
|
|
408
404
|
}
|
|
409
405
|
}
|
|
410
406
|
|
|
407
|
+
// Dashed tetrahedron outline (matching the gray structure lines used in 3D)
|
|
408
|
+
// plus corner element labels
|
|
411
409
|
function draw_structure_outline(): void {
|
|
412
410
|
if (!ctx || !canvas) return
|
|
413
411
|
|
|
414
|
-
const styles = getComputedStyle(canvas)
|
|
415
|
-
// Match gray dashed structure lines used in 3D
|
|
416
412
|
ctx.strokeStyle = CONVEX_HULL_STYLE.structure_line.color
|
|
417
413
|
ctx.lineWidth = CONVEX_HULL_STYLE.structure_line.line_width
|
|
418
414
|
ctx.setLineDash(CONVEX_HULL_STYLE.structure_line.dash)
|
|
419
415
|
|
|
420
|
-
// Draw tetrahedron edges
|
|
421
|
-
draw_tetrahedron()
|
|
422
|
-
|
|
423
|
-
// Reset dash and stroke for subsequent drawings
|
|
424
|
-
ctx.setLineDash([])
|
|
425
|
-
ctx.strokeStyle = styles.getPropertyValue(`--hull-edge-color`) || `#212121`
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
function draw_tetrahedron(): void {
|
|
429
|
-
if (!ctx) return
|
|
430
|
-
|
|
431
|
-
// Convert vertices to Point3D objects
|
|
432
416
|
const vertices = TETRAHEDRON_VERTICES.map(([x, y, z]) => ({ x, y, z }))
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
const edges = [
|
|
436
|
-
[0, 1],
|
|
437
|
-
[0, 2],
|
|
438
|
-
[0, 3], // From vertex 0
|
|
439
|
-
[1, 2],
|
|
440
|
-
[1, 3], // From vertex 1
|
|
441
|
-
[2, 3], // From vertex 2
|
|
442
|
-
]
|
|
443
|
-
|
|
444
|
-
// Draw edges
|
|
417
|
+
// oxfmt-ignore
|
|
418
|
+
const edges = [[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]
|
|
445
419
|
ctx.beginPath()
|
|
446
420
|
for (const [start, end] of edges) {
|
|
447
|
-
const
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
const proj1 = project_3d_point(v1.x, v1.y, v1.z)
|
|
451
|
-
const proj2 = project_3d_point(v2.x, v2.y, v2.z)
|
|
452
|
-
|
|
421
|
+
const proj1 = project_3d_point(vertices[start].x, vertices[start].y, vertices[start].z)
|
|
422
|
+
const proj2 = project_3d_point(vertices[end].x, vertices[end].y, vertices[end].z)
|
|
453
423
|
ctx.moveTo(proj1.x, proj1.y)
|
|
454
424
|
ctx.lineTo(proj2.x, proj2.y)
|
|
455
425
|
}
|
|
456
426
|
ctx.stroke()
|
|
457
427
|
|
|
458
|
-
//
|
|
428
|
+
// Reset dash and stroke for subsequent drawings
|
|
429
|
+
ctx.setLineDash([])
|
|
430
|
+
ctx.strokeStyle = hull_edge_color
|
|
431
|
+
|
|
432
|
+
// Corner element labels: place just outside each vertex, along the centroid→vertex line
|
|
459
433
|
if (elements.length === 4) {
|
|
460
|
-
// Tetrahedron centroid in barycentric space maps to average of vertices
|
|
461
434
|
const centroid = {
|
|
462
435
|
x: (vertices[0].x + vertices[1].x + vertices[2].x + vertices[3].x) / 4,
|
|
463
436
|
y: (vertices[0].y + vertices[1].y + vertices[2].y + vertices[3].y) / 4,
|
|
@@ -472,16 +445,13 @@
|
|
|
472
445
|
const distance = 0.06
|
|
473
446
|
for (let idx = 0; idx < 4; idx++) {
|
|
474
447
|
const vx = vertices[idx]
|
|
475
|
-
|
|
476
|
-
const { x: cx, y: cy, z: cz } = centroid
|
|
477
|
-
const dir = { x: vx.x - cx, y: vx.y - cy, z: vx.z - cz }
|
|
448
|
+
const dir = { x: vx.x - centroid.x, y: vx.y - centroid.y, z: vx.z - centroid.z }
|
|
478
449
|
const len = Math.hypot(dir.x, dir.y, dir.z) || 1
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const proj = project_3d_point(label_pos.x, label_pos.y, label_pos.z)
|
|
450
|
+
const proj = project_3d_point(
|
|
451
|
+
vx.x + (dir.x / len) * distance,
|
|
452
|
+
vx.y + (dir.y / len) * distance,
|
|
453
|
+
vx.z + (dir.z / len) * distance,
|
|
454
|
+
)
|
|
485
455
|
ctx.fillText(elements[idx], proj.x, proj.y)
|
|
486
456
|
}
|
|
487
457
|
}
|