matterviz 0.4.0 → 0.4.1
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/brillouin/BrillouinZone.svelte +68 -145
- package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -14
- package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
- package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
- package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
- package/dist/brillouin/BrillouinZoneScene.svelte +49 -203
- package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
- package/dist/brillouin/ReciprocalVectors.svelte +39 -0
- package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
- package/dist/brillouin/compute.d.ts +2 -0
- package/dist/brillouin/compute.js +80 -77
- package/dist/brillouin/geometry.d.ts +8 -0
- package/dist/brillouin/geometry.js +57 -0
- package/dist/brillouin/index.d.ts +2 -0
- package/dist/brillouin/index.js +2 -0
- package/dist/brillouin/types.d.ts +2 -2
- package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +100 -191
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +176 -464
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
- package/dist/chempot-diagram/color.d.ts +3 -6
- package/dist/chempot-diagram/color.js +5 -5
- package/dist/chempot-diagram/compute.d.ts +3 -3
- package/dist/chempot-diagram/compute.js +3 -1
- package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
- package/dist/chempot-diagram/controls-state.svelte.js +42 -0
- package/dist/chempot-diagram/export.d.ts +47 -0
- package/dist/chempot-diagram/export.js +133 -0
- package/dist/chempot-diagram/index.d.ts +1 -0
- package/dist/chempot-diagram/index.js +1 -0
- package/dist/chempot-diagram/pointer.d.ts +0 -10
- package/dist/chempot-diagram/pointer.js +4 -4
- package/dist/chempot-diagram/types.d.ts +3 -3
- package/dist/colors/index.js +2 -2
- package/dist/composition/FormulaFilter.svelte +6 -5
- package/dist/composition/PieChart.svelte +5 -5
- package/dist/composition/chem-sys.js +3 -2
- package/dist/composition/format.js +3 -2
- package/dist/composition/parse.d.ts +0 -1
- package/dist/composition/parse.js +17 -19
- package/dist/controls.d.ts +1 -0
- package/dist/controls.js +0 -1
- package/dist/convex-hull/ConvexHull.svelte +8 -10
- package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
- package/dist/convex-hull/ConvexHull2D.svelte +94 -175
- package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull3D.svelte +176 -680
- package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHull4D.svelte +180 -680
- package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
- package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
- package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
- package/dist/convex-hull/ConvexHullControls.svelte +88 -7
- package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
- package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
- package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
- package/dist/convex-hull/ConvexHullStats.svelte +29 -168
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
- package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
- package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
- package/dist/convex-hull/barycentric-coords.d.ts +2 -4
- package/dist/convex-hull/barycentric-coords.js +6 -33
- package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
- package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
- package/dist/convex-hull/helpers.d.ts +39 -7
- package/dist/convex-hull/helpers.js +154 -69
- package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
- package/dist/convex-hull/hull-state.svelte.js +124 -0
- package/dist/convex-hull/index.d.ts +9 -7
- package/dist/convex-hull/index.js +7 -2
- package/dist/convex-hull/thermodynamics.js +91 -920
- package/dist/convex-hull/types.d.ts +12 -4
- package/dist/convex-hull/types.js +12 -0
- package/dist/coordination/CoordinationBarPlot.svelte +4 -11
- package/dist/element/BohrAtom.svelte +2 -1
- package/dist/element/ElementTile.svelte.d.ts +1 -1
- package/dist/element/index.d.ts +4 -0
- package/dist/element/index.js +18 -0
- package/dist/feedback/DragOverlay.svelte +3 -1
- package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
- package/dist/feedback/StatusMessage.svelte +13 -3
- package/dist/fermi-surface/FermiSurface.svelte +67 -146
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -224
- package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
- package/dist/fermi-surface/compute.js +11 -10
- package/dist/fermi-surface/export.js +4 -15
- package/dist/fermi-surface/index.d.ts +0 -1
- package/dist/fermi-surface/index.js +0 -1
- package/dist/fermi-surface/parse.d.ts +1 -1
- package/dist/fermi-surface/parse.js +64 -75
- package/dist/fermi-surface/types.d.ts +2 -2
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +55 -40
- package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
- package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
- package/dist/heatmap-matrix/index.d.ts +3 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/io/ExportPane.svelte +166 -0
- package/dist/io/ExportPane.svelte.d.ts +17 -0
- package/dist/io/decompress.js +1 -2
- package/dist/io/export.d.ts +5 -1
- package/dist/io/export.js +32 -28
- package/dist/io/fetch.d.ts +2 -1
- package/dist/io/file-drop.d.ts +7 -0
- package/dist/io/file-drop.js +13 -0
- package/dist/io/index.d.ts +2 -0
- package/dist/io/index.js +10 -0
- package/dist/io/types.d.ts +13 -0
- package/dist/isosurface/parse.js +46 -44
- package/dist/labels.js +1 -1
- package/dist/layout/FullscreenButton.svelte +33 -0
- package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
- package/dist/layout/FullscreenToggle.svelte +8 -14
- package/dist/layout/ViewerChrome.svelte +116 -0
- package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
- package/dist/layout/fullscreen.d.ts +4 -0
- package/dist/layout/fullscreen.svelte.d.ts +8 -0
- package/dist/layout/fullscreen.svelte.js +37 -0
- package/dist/layout/index.d.ts +3 -0
- package/dist/layout/index.js +3 -0
- package/dist/math.d.ts +7 -3
- package/dist/math.js +18 -21
- package/dist/overlays/index.d.ts +4 -0
- package/dist/periodic-table/PeriodicTable.svelte +9 -8
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
- package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
- package/dist/phase-diagram/colors.js +1 -1
- package/dist/phase-diagram/parse.d.ts +2 -1
- package/dist/plot/bar/BarPlot.svelte +79 -316
- package/dist/plot/bar/BarPlot.svelte.d.ts +7 -15
- package/dist/plot/bar/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/bar/SpacegroupBarPlot.svelte +2 -1
- package/dist/plot/box/BoxPlot.svelte +76 -246
- package/dist/plot/box/BoxPlot.svelte.d.ts +4 -3
- package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
- package/dist/plot/box/Violin.svelte.d.ts +1 -1
- package/dist/plot/box/box-plot.d.ts +3 -2
- package/dist/plot/box/box-plot.js +5 -2
- package/dist/plot/box/kde.d.ts +2 -1
- package/dist/plot/box/kde.js +4 -4
- package/dist/plot/core/auto-place.d.ts +1 -1
- package/dist/plot/core/auto-place.js +4 -1
- package/dist/plot/core/components/ColorBar.svelte +5 -5
- package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
- package/dist/plot/core/components/Line.svelte +3 -2
- package/dist/plot/core/components/Line.svelte.d.ts +3 -2
- package/dist/plot/core/components/PlotAxis.svelte +2 -1
- package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
- package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/core/components/ReferenceLine3D.svelte +2 -2
- package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
- package/dist/plot/core/components/ReferencePlane.svelte +2 -2
- package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
- package/dist/plot/core/data-cleaning.js +18 -18
- package/dist/plot/core/fill-utils.d.ts +4 -3
- package/dist/plot/core/fill-utils.js +6 -3
- package/dist/plot/core/interactions.d.ts +5 -1
- package/dist/plot/core/interactions.js +14 -0
- package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
- package/dist/plot/core/pan-zoom.svelte.js +221 -0
- package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
- package/dist/plot/core/placed-tween.svelte.js +68 -0
- package/dist/plot/core/reference-line.d.ts +10 -10
- package/dist/plot/core/reference-line.js +6 -6
- package/dist/plot/core/scales.d.ts +17 -25
- package/dist/plot/core/scales.js +10 -8
- package/dist/plot/core/svg.d.ts +2 -1
- package/dist/plot/core/types.d.ts +18 -7
- package/dist/plot/core/utils/label-placement.d.ts +1 -1
- package/dist/plot/core/utils/label-placement.js +3 -3
- package/dist/plot/core/utils.d.ts +2 -1
- package/dist/plot/histogram/Histogram.svelte +77 -314
- package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/sankey/Sankey.svelte +2 -5
- package/dist/plot/sankey/Sankey.svelte.d.ts +1 -1
- package/dist/plot/sankey/sankey.js +3 -1
- package/dist/plot/scatter/BinnedScatterPlot.svelte +3 -5
- package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +4 -4
- package/dist/plot/scatter/ScatterPlot.svelte +160 -450
- package/dist/plot/scatter/ScatterPlot.svelte.d.ts +7 -15
- package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
- package/dist/plot/scatter/binned-scatter-types.d.ts +4 -11
- package/dist/plot/scatter/index.d.ts +1 -1
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte +15 -26
- package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +9 -10
- package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +122 -121
- package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
- package/dist/plot/scatter-3d/Surface3D.svelte +6 -5
- package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
- package/dist/plot/sunburst/Sunburst.svelte +16 -20
- package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
- package/dist/plot/sunburst/SunburstControls.svelte.d.ts +1 -1
- package/dist/plot/sunburst/sunburst.js +4 -1
- package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
- package/dist/sanitize.js +13 -2
- package/dist/scene/SceneCamera.svelte +62 -0
- package/dist/scene/SceneCamera.svelte.d.ts +19 -0
- package/dist/scene/bind-renderer.svelte.d.ts +2 -0
- package/dist/scene/bind-renderer.svelte.js +14 -0
- package/dist/scene/index.d.ts +4 -0
- package/dist/scene/index.js +5 -0
- package/dist/scene/props.js +52 -0
- package/dist/scene/types.d.ts +26 -0
- package/dist/scene/types.js +1 -0
- package/dist/settings.d.ts +14 -2
- package/dist/settings.js +59 -1
- package/dist/spectral/Bands.svelte +8 -7
- package/dist/spectral/Bands.svelte.d.ts +3 -2
- package/dist/spectral/BandsAndDos.svelte +22 -24
- package/dist/spectral/BrillouinBandsDos.svelte +3 -3
- package/dist/spectral/Dos.svelte +5 -4
- package/dist/spectral/Dos.svelte.d.ts +2 -1
- package/dist/spectral/helpers.d.ts +6 -6
- package/dist/spectral/helpers.js +43 -37
- package/dist/state.svelte.d.ts +0 -7
- package/dist/state.svelte.js +0 -6
- package/dist/structure/Arrow.svelte +2 -4
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/CanvasTooltip.svelte +1 -0
- package/dist/structure/CellSelect.svelte +11 -3
- package/dist/structure/CellSelect.svelte.d.ts +2 -1
- package/dist/structure/Lattice.svelte +2 -2
- package/dist/structure/Structure.svelte +291 -355
- package/dist/structure/Structure.svelte.d.ts +5 -15
- package/dist/structure/StructureControls.svelte +217 -2
- package/dist/structure/StructureControls.svelte.d.ts +5 -3
- package/dist/structure/StructureExportPane.svelte +54 -156
- package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
- package/dist/structure/StructureInfoPane.svelte +5 -3
- package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
- package/dist/structure/StructureScene.svelte +365 -198
- package/dist/structure/StructureScene.svelte.d.ts +22 -20
- package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
- package/dist/structure/{label-placement.js → atom-label-placement.js} +12 -2
- package/dist/structure/atom-properties.d.ts +1 -1
- package/dist/structure/atom-properties.js +11 -16
- package/dist/structure/bond-order-perception.js +2 -4
- package/dist/structure/bonding.d.ts +3 -0
- package/dist/structure/bonding.js +91 -48
- package/dist/structure/export.d.ts +24 -4
- package/dist/structure/export.js +64 -122
- package/dist/structure/index.d.ts +2 -0
- package/dist/structure/index.js +2 -0
- package/dist/structure/parse.d.ts +3 -2
- package/dist/structure/parse.js +333 -370
- package/dist/structure/partial-occupancy.d.ts +0 -1
- package/dist/structure/partial-occupancy.js +1 -1
- package/dist/structure/pbc.d.ts +1 -1
- package/dist/structure/pbc.js +186 -13
- package/dist/structure/polyhedra.d.ts +41 -0
- package/dist/structure/polyhedra.js +602 -0
- package/dist/structure/site.d.ts +4 -0
- package/dist/structure/site.js +1 -0
- package/dist/structure/supercell.js +3 -2
- package/dist/structure/validation.js +5 -6
- package/dist/symmetry/SymmetryElementControls.svelte +69 -0
- package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
- package/dist/symmetry/SymmetryElements.svelte +354 -0
- package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
- package/dist/symmetry/SymmetryStats.svelte +111 -6
- package/dist/symmetry/WyckoffTable.svelte +68 -7
- package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
- package/dist/symmetry/cell-transform.js +7 -14
- package/dist/symmetry/index.d.ts +14 -4
- package/dist/symmetry/index.js +301 -80
- package/dist/symmetry/spacegroups.d.ts +5 -1
- package/dist/symmetry/spacegroups.js +15 -1
- package/dist/symmetry/symmetry-elements.d.ts +33 -0
- package/dist/symmetry/symmetry-elements.js +521 -0
- package/dist/symmetry/wyckoff-db.d.ts +9 -0
- package/dist/symmetry/wyckoff-db.js +87 -0
- package/dist/table/HeatmapTable.svelte +4 -15
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/trajectory/Trajectory.svelte +58 -61
- package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
- package/dist/trajectory/TrajectoryExportPane.svelte +15 -24
- package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
- package/dist/trajectory/TrajectoryInfoPane.svelte +3 -2
- package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
- package/dist/trajectory/constants.js +6 -2
- package/dist/trajectory/extract.js +17 -37
- package/dist/trajectory/format-detect.d.ts +0 -1
- package/dist/trajectory/format-detect.js +3 -9
- package/dist/trajectory/frame-reader.d.ts +0 -1
- package/dist/trajectory/frame-reader.js +62 -128
- package/dist/trajectory/helpers.d.ts +10 -2
- package/dist/trajectory/helpers.js +56 -36
- package/dist/trajectory/parse/ase.d.ts +9 -1
- package/dist/trajectory/parse/ase.js +47 -32
- package/dist/trajectory/parse/diagnostics.d.ts +3 -0
- package/dist/trajectory/parse/diagnostics.js +14 -0
- package/dist/trajectory/parse/index.d.ts +1 -1
- package/dist/trajectory/parse/index.js +54 -102
- package/dist/trajectory/parse/lammps.d.ts +0 -2
- package/dist/trajectory/parse/lammps.js +8 -6
- package/dist/trajectory/parse/pymatgen.d.ts +2 -0
- package/dist/trajectory/parse/pymatgen.js +74 -0
- package/dist/trajectory/parse/vasp.js +4 -3
- package/dist/trajectory/parse/xyz.d.ts +9 -21
- package/dist/trajectory/parse/xyz.js +28 -33
- package/dist/trajectory/plotting.d.ts +0 -1
- package/dist/trajectory/plotting.js +3 -100
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +1 -1
- package/dist/xrd/XrdPlot.svelte +14 -29
- package/dist/xrd/broadening.d.ts +2 -1
- package/dist/xrd/calc-xrd.js +6 -11
- package/dist/xrd/index.d.ts +2 -2
- package/package.json +29 -16
- package/dist/element/data.json +0 -11864
- package/dist/fermi-surface/marching-cubes.d.ts +0 -2
- package/dist/fermi-surface/marching-cubes.js +0 -2
- package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
- package/dist/plot/core/hover-lock.svelte.js +0 -45
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { count_atoms_in_composition, extract_formula_elements, sort_by_electronegativity, } from '../composition';
|
|
2
2
|
import * as math from '../math';
|
|
3
|
-
import {
|
|
3
|
+
import { composition_to_barycentric_nd } from './barycentric-coords';
|
|
4
4
|
import { get_arity, HULL_STABILITY_TOL, is_on_hull, is_unary_entry } from './helpers';
|
|
5
5
|
// Track warned keys to avoid log spam on large datasets with repeated invalid keys
|
|
6
6
|
const warned_keys = new Set();
|
|
7
|
-
const cross_point_2d = (origin, point_a, point_b) => (point_a.x - origin.x) * (point_b.y - origin.y) -
|
|
8
|
-
(point_a.y - origin.y) * (point_b.x - origin.x);
|
|
9
7
|
// Normalize convex hull composition keys by stripping oxidation states (e.g. "V4+" -> "V")
|
|
10
8
|
// and merging amounts for keys that map to the same element. Filters non-positive amounts.
|
|
11
9
|
// Only extracts FIRST valid element from each key (e.g. "Fe2O3" -> "Fe", not both Fe and O).
|
|
@@ -189,115 +187,8 @@ export function calculate_e_above_hull(input, reference_entries) {
|
|
|
189
187
|
results[id] = y_hull === null ? NaN : Math.max(0, e_form - y_hull);
|
|
190
188
|
}
|
|
191
189
|
}
|
|
192
|
-
else if (arity === 3) {
|
|
193
|
-
// Ternary system
|
|
194
|
-
const ref_points = [];
|
|
195
|
-
for (const ref of reference_entries) {
|
|
196
|
-
if (ref.exclude_from_hull)
|
|
197
|
-
continue; // Shown but not used in hull construction
|
|
198
|
-
const e_form = compute_e_form(ref);
|
|
199
|
-
if (typeof e_form !== `number`)
|
|
200
|
-
continue;
|
|
201
|
-
try {
|
|
202
|
-
const bary = composition_to_barycentric_3d(ref.composition, elements);
|
|
203
|
-
const point = barycentric_to_ternary_xyz(bary, e_form);
|
|
204
|
-
ref_points.push(point);
|
|
205
|
-
}
|
|
206
|
-
catch {
|
|
207
|
-
// Ignore invalid compositions
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// Ensure corner points (pure elements default to e_form = 0)
|
|
211
|
-
for (const el of elements) {
|
|
212
|
-
const corner = barycentric_to_ternary_xyz(composition_to_barycentric_3d({ [el]: 1 }, elements), 0);
|
|
213
|
-
const dist = (point) => Math.hypot(point.x - corner.x, point.y - corner.y, point.z - corner.z);
|
|
214
|
-
if (!ref_points.some((point) => dist(point) < 1e-9))
|
|
215
|
-
ref_points.push(corner);
|
|
216
|
-
}
|
|
217
|
-
const hull_triangles = compute_lower_hull_triangles(ref_points);
|
|
218
|
-
const hull_models = build_lower_hull_model(hull_triangles);
|
|
219
|
-
// No facets despite enough refs → all coplanar at e_form = 0: use elemental tie-plane
|
|
220
|
-
const degenerate_hull_3d = hull_triangles.length === 0 && ref_points.length >= arity;
|
|
221
|
-
for (const { entry, e_form } of interest_data) {
|
|
222
|
-
const id = id_of(entry);
|
|
223
|
-
if (typeof e_form !== `number`) {
|
|
224
|
-
results[id] = NaN;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
const bary = composition_to_barycentric_3d(entry.composition, elements);
|
|
229
|
-
if (degenerate_hull_3d) {
|
|
230
|
-
results[id] = Math.max(0, e_form);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const point = barycentric_to_ternary_xyz(bary, e_form);
|
|
234
|
-
const z_hull = e_hull_at_xy(hull_models, point.x, point.y);
|
|
235
|
-
results[id] = z_hull === null ? NaN : Math.max(0, point.z - z_hull);
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
results[id] = NaN;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
else if (arity === 4) {
|
|
243
|
-
// Quaternary system
|
|
244
|
-
const ref_points = [];
|
|
245
|
-
for (const ref of reference_entries) {
|
|
246
|
-
if (ref.exclude_from_hull)
|
|
247
|
-
continue; // Shown but not used in hull construction
|
|
248
|
-
const e_form = compute_e_form(ref);
|
|
249
|
-
if (typeof e_form !== `number`)
|
|
250
|
-
continue;
|
|
251
|
-
try {
|
|
252
|
-
const bary = composition_to_barycentric_4d(ref.composition, elements);
|
|
253
|
-
const tet = barycentric_to_tetrahedral(bary);
|
|
254
|
-
ref_points.push({ ...tet, w: e_form });
|
|
255
|
-
}
|
|
256
|
-
catch {
|
|
257
|
-
// Ignore invalid
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
// Ensure corner points (pure elements default to e_form = 0)
|
|
261
|
-
for (const el of elements) {
|
|
262
|
-
const tet = barycentric_to_tetrahedral(composition_to_barycentric_4d({ [el]: 1 }, elements));
|
|
263
|
-
const corner = { ...tet, w: 0 };
|
|
264
|
-
const dist = (point) => Math.hypot(point.x - corner.x, point.y - corner.y, point.z - corner.z, point.w);
|
|
265
|
-
if (!ref_points.some((point) => dist(point) < 1e-9))
|
|
266
|
-
ref_points.push(corner);
|
|
267
|
-
}
|
|
268
|
-
const hull_tetrahedra = compute_lower_hull_4d(ref_points);
|
|
269
|
-
// No facets despite enough refs → all coplanar at e_form = 0: use elemental tie-plane
|
|
270
|
-
const degenerate_hull_4d = hull_tetrahedra.length === 0 && ref_points.length >= arity;
|
|
271
|
-
const interest_points = [];
|
|
272
|
-
const idx_to_point_idx = new Map(); // entry idx -> point idx
|
|
273
|
-
interest_data.forEach(({ entry, e_form }, idx) => {
|
|
274
|
-
if (typeof e_form !== `number`)
|
|
275
|
-
return;
|
|
276
|
-
try {
|
|
277
|
-
const bary = composition_to_barycentric_4d(entry.composition, elements);
|
|
278
|
-
const tet = barycentric_to_tetrahedral(bary);
|
|
279
|
-
idx_to_point_idx.set(idx, interest_points.length);
|
|
280
|
-
interest_points.push({ ...tet, w: e_form });
|
|
281
|
-
}
|
|
282
|
-
catch {
|
|
283
|
-
// Skip
|
|
284
|
-
}
|
|
285
|
-
});
|
|
286
|
-
const distances = compute_e_above_hull_4d(interest_points, hull_tetrahedra);
|
|
287
|
-
// Map back
|
|
288
|
-
for (let idx = 0; idx < interest_data.length; idx++) {
|
|
289
|
-
const { entry, e_form } = interest_data[idx];
|
|
290
|
-
const id = id_of(entry);
|
|
291
|
-
const point_idx = idx_to_point_idx.get(idx);
|
|
292
|
-
const on_tie_plane = degenerate_hull_4d && typeof e_form === `number`;
|
|
293
|
-
if (point_idx === undefined)
|
|
294
|
-
results[id] = NaN;
|
|
295
|
-
else
|
|
296
|
-
results[id] = Math.max(0, on_tie_plane ? e_form : distances[point_idx]);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
190
|
else {
|
|
300
|
-
// Arity
|
|
191
|
+
// Arity 3+ uses the generalized N-dimensional convex hull in reduced barycentric coords
|
|
301
192
|
// Helper to convert entry to hull point, returns null on expected errors.
|
|
302
193
|
// Barycentric coords sum to 1, so the first is dropped: keeping all N would confine
|
|
303
194
|
// points to an (N-1)-dim affine subspace, leaving the hull permanently degenerate.
|
|
@@ -505,17 +396,11 @@ export function process_hull_for_stats(entries, elements) {
|
|
|
505
396
|
}
|
|
506
397
|
// --- 2D Convex Hull (Binary Phase Diagrams) ---
|
|
507
398
|
export function compute_lower_hull_2d(points) {
|
|
508
|
-
// Andrew's monotone chain
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
while (lower.length >= 2 &&
|
|
514
|
-
cross_point_2d(lower[lower.length - 2], lower[lower.length - 1], point) <= 0)
|
|
515
|
-
lower.pop();
|
|
516
|
-
lower.push(point);
|
|
517
|
-
}
|
|
518
|
-
return lower;
|
|
399
|
+
// Andrew's monotone chain lower hull (Point2D adapter over math.monotone_chain)
|
|
400
|
+
const sorted = points
|
|
401
|
+
.map((pt) => [pt.x, pt.y])
|
|
402
|
+
.toSorted((a, b) => a[0] - b[0] || a[1] - b[1]);
|
|
403
|
+
return math.monotone_chain(sorted).map(([x, y]) => ({ x, y }));
|
|
519
404
|
}
|
|
520
405
|
export function interpolate_hull_2d(hull, x) {
|
|
521
406
|
if (hull.length < 2)
|
|
@@ -529,255 +414,31 @@ export function interpolate_hull_2d(hull, x) {
|
|
|
529
414
|
const p1 = hull[idx];
|
|
530
415
|
const p2 = hull[idx + 1];
|
|
531
416
|
if (x >= p1.x && x <= p2.x) {
|
|
532
|
-
const
|
|
533
|
-
return p1.y * (1 -
|
|
417
|
+
const frac = (x - p1.x) / Math.max(1e-12, p2.x - p1.x);
|
|
418
|
+
return p1.y * (1 - frac) + p2.y * frac;
|
|
534
419
|
}
|
|
535
420
|
}
|
|
536
421
|
return null;
|
|
537
422
|
}
|
|
538
423
|
// --- Convex hull geometry ---
|
|
539
424
|
const EPS = 1e-9;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
425
|
+
// Point conversions between object-shaped public API points and ND number[] points
|
|
426
|
+
const p3_to_nd = (pt) => [pt.x, pt.y, pt.z];
|
|
427
|
+
const p4_to_nd = (pt) => [pt.x, pt.y, pt.z, pt.w];
|
|
428
|
+
// Map an ND facet (vertex indices into `points`) back to the public triangle shape
|
|
429
|
+
const facet_to_triangle = (facet, points) => ({
|
|
430
|
+
vertices: facet.vertex_indices.map((idx) => points[idx]),
|
|
431
|
+
normal: { x: facet.plane.normal[0], y: facet.plane.normal[1], z: facet.plane.normal[2] },
|
|
432
|
+
centroid: { x: facet.centroid[0], y: facet.centroid[1], z: facet.centroid[2] },
|
|
544
433
|
});
|
|
545
|
-
|
|
546
|
-
x: vec1.y * vec2.z - vec1.z * vec2.y,
|
|
547
|
-
y: vec1.z * vec2.x - vec1.x * vec2.z,
|
|
548
|
-
z: vec1.x * vec2.y - vec1.y * vec2.x,
|
|
549
|
-
});
|
|
550
|
-
const norm = (point) => Math.sqrt(point.x * point.x + point.y * point.y + point.z * point.z);
|
|
551
|
-
function normalize(point) {
|
|
552
|
-
const length = norm(point);
|
|
553
|
-
if (length < EPS)
|
|
554
|
-
return { x: 0, y: 0, z: 0 };
|
|
555
|
-
return { x: point.x / length, y: point.y / length, z: point.z / length };
|
|
556
|
-
}
|
|
557
|
-
function compute_plane(p1, p2, p3) {
|
|
558
|
-
const edge_12 = subtract(p2, p1);
|
|
559
|
-
const edge_13 = subtract(p3, p1);
|
|
560
|
-
const normal = normalize(cross(edge_12, edge_13));
|
|
561
|
-
const offset = -(normal.x * p1.x + normal.y * p1.y + normal.z * p1.z);
|
|
562
|
-
return { normal, offset };
|
|
563
|
-
}
|
|
564
|
-
const point_plane_signed_distance = (plane, point) => plane.normal.x * point.x + plane.normal.y * point.y + plane.normal.z * point.z + plane.offset;
|
|
565
|
-
const compute_centroid = (p1, p2, p3) => ({
|
|
566
|
-
x: (p1.x + p2.x + p3.x) / 3,
|
|
567
|
-
y: (p1.y + p2.y + p3.y) / 3,
|
|
568
|
-
z: (p1.z + p2.z + p3.z) / 3,
|
|
569
|
-
});
|
|
570
|
-
function distance_point_to_line(line_start, line_end, point) {
|
|
571
|
-
const line_vec = subtract(line_end, line_start);
|
|
572
|
-
const to_point = subtract(point, line_start);
|
|
573
|
-
const cross_prod = cross(line_vec, to_point);
|
|
574
|
-
const line_len = norm(line_vec);
|
|
575
|
-
if (line_len < EPS)
|
|
576
|
-
return 0;
|
|
577
|
-
return norm(cross_prod) / line_len;
|
|
578
|
-
}
|
|
579
|
-
function choose_initial_tetrahedron(points) {
|
|
580
|
-
if (points.length < 4)
|
|
581
|
-
return null;
|
|
582
|
-
let idx_min_x = 0;
|
|
583
|
-
let idx_max_x = 0;
|
|
584
|
-
for (let idx = 1; idx < points.length; idx++) {
|
|
585
|
-
if (points[idx].x < points[idx_min_x].x)
|
|
586
|
-
idx_min_x = idx;
|
|
587
|
-
if (points[idx].x > points[idx_max_x].x)
|
|
588
|
-
idx_max_x = idx;
|
|
589
|
-
}
|
|
590
|
-
if (idx_min_x === idx_max_x)
|
|
591
|
-
return null;
|
|
592
|
-
let idx_far_line = -1;
|
|
593
|
-
let best_dist_line = -1;
|
|
594
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
595
|
-
if (idx === idx_min_x || idx === idx_max_x)
|
|
596
|
-
continue;
|
|
597
|
-
const dist = distance_point_to_line(points[idx_min_x], points[idx_max_x], points[idx]);
|
|
598
|
-
if (dist > best_dist_line) {
|
|
599
|
-
best_dist_line = dist;
|
|
600
|
-
idx_far_line = idx;
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
if (idx_far_line === -1 || best_dist_line < EPS)
|
|
604
|
-
return null;
|
|
605
|
-
const plane0 = compute_plane(points[idx_min_x], points[idx_max_x], points[idx_far_line]);
|
|
606
|
-
let idx_far_plane = -1;
|
|
607
|
-
let best_dist_plane = -1;
|
|
608
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
609
|
-
if (idx === idx_min_x || idx === idx_max_x || idx === idx_far_line)
|
|
610
|
-
continue;
|
|
611
|
-
const dist = Math.abs(point_plane_signed_distance(plane0, points[idx]));
|
|
612
|
-
if (dist > best_dist_plane) {
|
|
613
|
-
best_dist_plane = dist;
|
|
614
|
-
idx_far_plane = idx;
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
if (idx_far_plane === -1 || best_dist_plane < EPS)
|
|
618
|
-
return null;
|
|
619
|
-
return [idx_min_x, idx_max_x, idx_far_line, idx_far_plane];
|
|
620
|
-
}
|
|
621
|
-
function make_face(points, a, b, c, interior_point) {
|
|
622
|
-
let plane = compute_plane(points[a], points[b], points[c]);
|
|
623
|
-
let centroid = compute_centroid(points[a], points[b], points[c]);
|
|
624
|
-
const dist_interior = point_plane_signed_distance(plane, interior_point);
|
|
625
|
-
if (dist_interior > 0) {
|
|
626
|
-
plane = compute_plane(points[a], points[c], points[b]);
|
|
627
|
-
centroid = compute_centroid(points[a], points[c], points[b]);
|
|
628
|
-
return { vertices: [a, c, b], plane, centroid, outside_points: new Set() };
|
|
629
|
-
}
|
|
630
|
-
return { vertices: [a, b, c], plane, centroid, outside_points: new Set() };
|
|
631
|
-
}
|
|
632
|
-
function assign_outside_points(face, points, candidate_indices) {
|
|
633
|
-
face.outside_points.clear();
|
|
634
|
-
for (const idx of candidate_indices) {
|
|
635
|
-
const distance = point_plane_signed_distance(face.plane, points[idx]);
|
|
636
|
-
if (distance > EPS)
|
|
637
|
-
face.outside_points.add(idx);
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
function collect_candidate_points(faces) {
|
|
641
|
-
const set = new Set();
|
|
642
|
-
for (const face of faces)
|
|
643
|
-
for (const idx of face.outside_points)
|
|
644
|
-
set.add(idx);
|
|
645
|
-
return Array.from(set);
|
|
646
|
-
}
|
|
647
|
-
function farthest_point_for_face(points, face) {
|
|
648
|
-
let best_idx = -1;
|
|
649
|
-
let best_distance = -1;
|
|
650
|
-
for (const idx of face.outside_points) {
|
|
651
|
-
const distance = point_plane_signed_distance(face.plane, points[idx]);
|
|
652
|
-
if (distance > best_distance) {
|
|
653
|
-
best_distance = distance;
|
|
654
|
-
best_idx = idx;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (best_idx === -1)
|
|
658
|
-
return null;
|
|
659
|
-
return { idx: best_idx, distance: best_distance };
|
|
660
|
-
}
|
|
661
|
-
function build_horizon(faces, visible_face_indices) {
|
|
662
|
-
const edge_count = new Map();
|
|
663
|
-
for (const face_idx of visible_face_indices) {
|
|
664
|
-
const face = faces[face_idx];
|
|
665
|
-
const [a, b, c] = face.vertices;
|
|
666
|
-
const edges = [
|
|
667
|
-
[a, b],
|
|
668
|
-
[b, c],
|
|
669
|
-
[c, a],
|
|
670
|
-
];
|
|
671
|
-
for (const [u, v] of edges) {
|
|
672
|
-
const key = u < v ? `${u}|${v}` : `${v}|${u}`;
|
|
673
|
-
if (!edge_count.has(key))
|
|
674
|
-
edge_count.set(key, [u, v]);
|
|
675
|
-
else
|
|
676
|
-
edge_count.set(key, [Number.NaN, Number.NaN]);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
const horizon = [];
|
|
680
|
-
for (const uv of edge_count.values()) {
|
|
681
|
-
if (Number.isNaN(uv[0]))
|
|
682
|
-
continue;
|
|
683
|
-
horizon.push(uv);
|
|
684
|
-
}
|
|
685
|
-
return horizon;
|
|
686
|
-
}
|
|
434
|
+
// 3D quickhull (thin adapter over the N-dimensional implementation)
|
|
687
435
|
export function compute_quickhull_triangles(points) {
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
const initial = choose_initial_tetrahedron(points);
|
|
691
|
-
if (!initial)
|
|
692
|
-
return [];
|
|
693
|
-
const [i0, i1, i2, i3] = initial;
|
|
694
|
-
const interior_point = {
|
|
695
|
-
x: (points[i0].x + points[i1].x + points[i2].x + points[i3].x) / 4,
|
|
696
|
-
y: (points[i0].y + points[i1].y + points[i2].y + points[i3].y) / 4,
|
|
697
|
-
z: (points[i0].z + points[i1].z + points[i2].z + points[i3].z) / 4,
|
|
698
|
-
};
|
|
699
|
-
const faces = [
|
|
700
|
-
make_face(points, i0, i1, i2, interior_point),
|
|
701
|
-
make_face(points, i0, i2, i3, interior_point),
|
|
702
|
-
make_face(points, i0, i3, i1, interior_point),
|
|
703
|
-
make_face(points, i1, i3, i2, interior_point),
|
|
704
|
-
];
|
|
705
|
-
const all_indices = [];
|
|
706
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
707
|
-
if (idx === i0 || idx === i1 || idx === i2 || idx === i3)
|
|
708
|
-
continue;
|
|
709
|
-
all_indices.push(idx);
|
|
710
|
-
}
|
|
711
|
-
for (const face of faces)
|
|
712
|
-
assign_outside_points(face, points, all_indices);
|
|
713
|
-
while (true) {
|
|
714
|
-
let chosen_face_idx = -1;
|
|
715
|
-
let chosen_point_idx = -1;
|
|
716
|
-
let max_distance = -1;
|
|
717
|
-
for (let face_idx = 0; face_idx < faces.length; face_idx++) {
|
|
718
|
-
const face = faces[face_idx];
|
|
719
|
-
if (face.outside_points.size === 0)
|
|
720
|
-
continue;
|
|
721
|
-
const far = farthest_point_for_face(points, face);
|
|
722
|
-
if (far && far.distance > max_distance) {
|
|
723
|
-
max_distance = far.distance;
|
|
724
|
-
chosen_face_idx = face_idx;
|
|
725
|
-
chosen_point_idx = far.idx;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (chosen_face_idx === -1)
|
|
729
|
-
break;
|
|
730
|
-
const eye_idx = chosen_point_idx;
|
|
731
|
-
const visible_face_indices = new Set();
|
|
732
|
-
for (let face_idx = 0; face_idx < faces.length; face_idx++) {
|
|
733
|
-
const face = faces[face_idx];
|
|
734
|
-
const dist = point_plane_signed_distance(face.plane, points[eye_idx]);
|
|
735
|
-
if (dist > EPS)
|
|
736
|
-
visible_face_indices.add(face_idx);
|
|
737
|
-
}
|
|
738
|
-
const horizon_edges = build_horizon(faces, visible_face_indices);
|
|
739
|
-
const visible_faces = Array.from(visible_face_indices).sort((a, b) => b - a);
|
|
740
|
-
const candidate_points = collect_candidate_points(visible_faces.map((idx) => faces[idx]));
|
|
741
|
-
for (const idx of visible_faces)
|
|
742
|
-
faces.splice(idx, 1);
|
|
743
|
-
const new_faces = [];
|
|
744
|
-
for (const [u, v] of horizon_edges) {
|
|
745
|
-
const new_face = make_face(points, u, v, eye_idx, interior_point);
|
|
746
|
-
new_faces.push(new_face);
|
|
747
|
-
}
|
|
748
|
-
for (const face of new_faces)
|
|
749
|
-
face.outside_points.clear();
|
|
750
|
-
for (const idx of candidate_points) {
|
|
751
|
-
if (idx === eye_idx)
|
|
752
|
-
continue;
|
|
753
|
-
let best_face = null;
|
|
754
|
-
let best_distance = EPS;
|
|
755
|
-
for (const face of new_faces) {
|
|
756
|
-
const dist = point_plane_signed_distance(face.plane, points[idx]);
|
|
757
|
-
if (dist > best_distance) {
|
|
758
|
-
best_distance = dist;
|
|
759
|
-
best_face = face;
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
if (best_face)
|
|
763
|
-
best_face.outside_points.add(idx);
|
|
764
|
-
}
|
|
765
|
-
faces.push(...new_faces);
|
|
766
|
-
}
|
|
767
|
-
return faces.map((face) => {
|
|
768
|
-
const [a, b, c] = face.vertices;
|
|
769
|
-
const normal = face.plane.normal;
|
|
770
|
-
const centroid = face.centroid;
|
|
771
|
-
return {
|
|
772
|
-
vertices: [points[a], points[b], points[c]],
|
|
773
|
-
normal,
|
|
774
|
-
centroid,
|
|
775
|
-
};
|
|
776
|
-
});
|
|
436
|
+
const facets = compute_quickhull_nd(points.map(p3_to_nd));
|
|
437
|
+
return facets.map((facet) => facet_to_triangle(facet, points));
|
|
777
438
|
}
|
|
778
439
|
export function compute_lower_hull_triangles(points) {
|
|
779
|
-
|
|
780
|
-
return
|
|
440
|
+
// Lower hull faces point "down" in the z (energy) direction
|
|
441
|
+
return compute_quickhull_triangles(points).filter((face) => face.normal.z < 0 - EPS);
|
|
781
442
|
}
|
|
782
443
|
export const build_lower_hull_model = (faces) => faces.map((tri) => {
|
|
783
444
|
const [p1, p2, p3] = tri.vertices;
|
|
@@ -847,496 +508,37 @@ export const compute_e_above_hull_for_points = (points, models) => points.map((p
|
|
|
847
508
|
return 0;
|
|
848
509
|
return Math.max(0, point.z - z_hull);
|
|
849
510
|
});
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
w: pt1.w - pt2.w,
|
|
855
|
-
});
|
|
856
|
-
const dot_4d = (vec_a, vec_b) => vec_a.x * vec_b.x + vec_a.y * vec_b.y + vec_a.z * vec_b.z + vec_a.w * vec_b.w;
|
|
857
|
-
const norm_4d = (point) => Math.sqrt(point.x * point.x + point.y * point.y + point.z * point.z + point.w * point.w);
|
|
858
|
-
function normalize_4d(point) {
|
|
859
|
-
const length = norm_4d(point);
|
|
860
|
-
if (length < EPS)
|
|
861
|
-
return { x: 0, y: 0, z: 0, w: 0 };
|
|
511
|
+
// Map an ND facet (vertex indices into `points`) back to the public tetrahedron shape
|
|
512
|
+
const facet_to_tetrahedron = (facet, points) => {
|
|
513
|
+
const [nx, ny, nz, nw] = facet.plane.normal;
|
|
514
|
+
const [cx, cy, cz, cw] = facet.centroid;
|
|
862
515
|
return {
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
w: point.w / length,
|
|
867
|
-
};
|
|
868
|
-
}
|
|
869
|
-
// Compute normal to a 3D hyperplane in 4D space defined by 4 points
|
|
870
|
-
//
|
|
871
|
-
// Mathematical Background:
|
|
872
|
-
// A 3D hyperplane (tetrahedral facet) in 4D is defined by 4 points. The normal vector
|
|
873
|
-
// must be orthogonal to all three edge vectors spanning the hyperplane. This is the
|
|
874
|
-
// 4D analog of computing a cross product of two vectors in 3D.
|
|
875
|
-
//
|
|
876
|
-
// Approach:
|
|
877
|
-
// 1. Form three edge vectors v1, v2, v3 from point p1
|
|
878
|
-
// 2. Find vector n such that: n · v1 = 0, n · v2 = 0, n · v3 = 0
|
|
879
|
-
// 3. This is equivalent to finding the null space of the 3×4 matrix [v1; v2; v3]
|
|
880
|
-
//
|
|
881
|
-
// Implementation:
|
|
882
|
-
// The normal components (nx, ny, nz, nw) are computed using Laplace expansion
|
|
883
|
-
// (cofactor method) along each column of the matrix. Each component is the determinant
|
|
884
|
-
// of the 3×3 submatrix obtained by removing that column, with alternating signs.
|
|
885
|
-
//
|
|
886
|
-
// References:
|
|
887
|
-
// - Barber et al. (1996) "The Quickhull Algorithm for Convex Hulls"
|
|
888
|
-
// - https://en.wikipedia.org/wiki/Cross_product#Multilinear_algebra
|
|
889
|
-
// - https://mathworld.wolfram.com/Nullspace.html
|
|
890
|
-
function compute_plane_4d(p1, p2, p3, p4) {
|
|
891
|
-
// Three edge vectors from p1
|
|
892
|
-
const v1 = subtract_4d(p2, p1);
|
|
893
|
-
const v2 = subtract_4d(p3, p1);
|
|
894
|
-
const v3 = subtract_4d(p4, p1);
|
|
895
|
-
// Build matrix [v1; v2; v3] and compute normal via cofactor expansion
|
|
896
|
-
const matrix = [
|
|
897
|
-
[v1.x, v1.y, v1.z, v1.w],
|
|
898
|
-
[v2.x, v2.y, v2.z, v2.w],
|
|
899
|
-
[v3.x, v3.y, v3.z, v3.w],
|
|
900
|
-
];
|
|
901
|
-
// Helper: extract 3×3 submatrix by removing column col_skip, then compute determinant
|
|
902
|
-
const det_submatrix = (col_skip) => {
|
|
903
|
-
const cols = [0, 1, 2, 3].filter((col) => col !== col_skip);
|
|
904
|
-
const submatrix = [
|
|
905
|
-
[matrix[0][cols[0]], matrix[0][cols[1]], matrix[0][cols[2]]],
|
|
906
|
-
[matrix[1][cols[0]], matrix[1][cols[1]], matrix[1][cols[2]]],
|
|
907
|
-
[matrix[2][cols[0]], matrix[2][cols[1]], matrix[2][cols[2]]],
|
|
908
|
-
];
|
|
909
|
-
return math.det_3x3(submatrix);
|
|
910
|
-
};
|
|
911
|
-
// Compute normal components using Laplace expansion along each column
|
|
912
|
-
// Alternating signs: +, -, +, -
|
|
913
|
-
const signs = [1, -1, 1, -1];
|
|
914
|
-
const normal_components = [0, 1, 2, 3].map((col_idx) => signs[col_idx] * det_submatrix(col_idx));
|
|
915
|
-
const [x, y, z, w] = normal_components;
|
|
916
|
-
const normal = normalize_4d({ x, y, z, w });
|
|
917
|
-
// Guard against degenerate (nearly co-planar) points
|
|
918
|
-
const normal_magnitude = Math.abs(normal.x) + Math.abs(normal.y) + Math.abs(normal.z) + Math.abs(normal.w);
|
|
919
|
-
if (normal_magnitude < EPS) {
|
|
920
|
-
return { normal: { x: 0, y: 0, z: 0, w: 0 }, offset: 0 };
|
|
921
|
-
}
|
|
922
|
-
const offset = -dot_4d(normal, p1);
|
|
923
|
-
return { normal, offset };
|
|
924
|
-
}
|
|
925
|
-
const point_plane_signed_distance_4d = (plane, point) => dot_4d(plane.normal, point) + plane.offset;
|
|
926
|
-
const compute_centroid_4d = (p1, p2, p3, p4) => ({
|
|
927
|
-
x: (p1.x + p2.x + p3.x + p4.x) / 4,
|
|
928
|
-
y: (p1.y + p2.y + p3.y + p4.y) / 4,
|
|
929
|
-
z: (p1.z + p2.z + p3.z + p4.z) / 4,
|
|
930
|
-
w: (p1.w + p2.w + p3.w + p4.w) / 4,
|
|
931
|
-
});
|
|
932
|
-
function distance_point_to_hyperplane_4d(p1, p2, p3, point) {
|
|
933
|
-
// Distance from point to the 2D hyperplane spanned by p1, p2, p3
|
|
934
|
-
const v1 = subtract_4d(p2, p1);
|
|
935
|
-
const v2 = subtract_4d(p3, p1);
|
|
936
|
-
const vp = subtract_4d(point, p1);
|
|
937
|
-
// Project vp onto the plane spanned by v1 and v2
|
|
938
|
-
// Use Gram-Schmidt to find orthogonal component
|
|
939
|
-
const v1_norm_sq = dot_4d(v1, v1);
|
|
940
|
-
const v2_norm_sq = dot_4d(v2, v2);
|
|
941
|
-
const v1_dot_v2 = dot_4d(v1, v2);
|
|
942
|
-
if (v1_norm_sq < EPS || v2_norm_sq < EPS)
|
|
943
|
-
return 0;
|
|
944
|
-
const vp_dot_v1 = dot_4d(vp, v1);
|
|
945
|
-
const vp_dot_v2 = dot_4d(vp, v2);
|
|
946
|
-
// Solve linear system for projection coefficients
|
|
947
|
-
const det = v1_norm_sq * v2_norm_sq - v1_dot_v2 * v1_dot_v2;
|
|
948
|
-
if (Math.abs(det) < EPS)
|
|
949
|
-
return 0;
|
|
950
|
-
const alpha = (v2_norm_sq * vp_dot_v1 - v1_dot_v2 * vp_dot_v2) / det;
|
|
951
|
-
const beta = (v1_norm_sq * vp_dot_v2 - v1_dot_v2 * vp_dot_v1) / det;
|
|
952
|
-
// Compute projection
|
|
953
|
-
const proj_x = p1.x + alpha * v1.x + beta * v2.x;
|
|
954
|
-
const proj_y = p1.y + alpha * v1.y + beta * v2.y;
|
|
955
|
-
const proj_z = p1.z + alpha * v1.z + beta * v2.z;
|
|
956
|
-
const proj_w = p1.w + alpha * v1.w + beta * v2.w;
|
|
957
|
-
// Distance is the length of (point - projection)
|
|
958
|
-
const dx = point.x - proj_x;
|
|
959
|
-
const dy = point.y - proj_y;
|
|
960
|
-
const dz = point.z - proj_z;
|
|
961
|
-
const dw = point.w - proj_w;
|
|
962
|
-
return Math.sqrt(dx * dx + dy * dy + dz * dz + dw * dw);
|
|
963
|
-
}
|
|
964
|
-
// Distance from point to line in 4D
|
|
965
|
-
function distance_point_to_line_4d(a, b, p) {
|
|
966
|
-
const ab = subtract_4d(b, a);
|
|
967
|
-
const ap = subtract_4d(p, a);
|
|
968
|
-
const ab_len_sq = dot_4d(ab, ab);
|
|
969
|
-
if (ab_len_sq < EPS)
|
|
970
|
-
return norm_4d(ap);
|
|
971
|
-
// Project ap onto ab
|
|
972
|
-
const t = dot_4d(ap, ab) / ab_len_sq;
|
|
973
|
-
const projection = {
|
|
974
|
-
x: a.x + t * ab.x,
|
|
975
|
-
y: a.y + t * ab.y,
|
|
976
|
-
z: a.z + t * ab.z,
|
|
977
|
-
w: a.w + t * ab.w,
|
|
516
|
+
vertices: facet.vertex_indices.map((idx) => points[idx]),
|
|
517
|
+
normal: { x: nx, y: ny, z: nz, w: nw },
|
|
518
|
+
centroid: { x: cx, y: cy, z: cz, w: cw },
|
|
978
519
|
};
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
// Maximum sample size for initial simplex selection in 4D hulls (avoids O(n²) for large datasets)
|
|
982
|
-
const INITIAL_SIMPLEX_SAMPLE_SIZE = 100;
|
|
983
|
-
function choose_initial_4_simplex(points) {
|
|
984
|
-
if (points.length < 5)
|
|
985
|
-
return null;
|
|
986
|
-
// Find two points farthest apart across all dimensions for better numerical stability
|
|
987
|
-
// Sample a small subset if dataset is large to avoid O(n²) scaling
|
|
988
|
-
const sample_size = Math.min(points.length, INITIAL_SIMPLEX_SAMPLE_SIZE);
|
|
989
|
-
const sample_indices = points.length <= sample_size
|
|
990
|
-
? points.map((_, idx) => idx)
|
|
991
|
-
: Array.from({ length: sample_size }, (_, idx) => Math.floor((idx * points.length) / sample_size));
|
|
992
|
-
let idx_far_a = 0;
|
|
993
|
-
let idx_far_b = 0;
|
|
994
|
-
let max_dist_sq = -1;
|
|
995
|
-
for (const idx_a of sample_indices) {
|
|
996
|
-
for (const idx_b of sample_indices) {
|
|
997
|
-
if (idx_a >= idx_b)
|
|
998
|
-
continue;
|
|
999
|
-
const pa = points[idx_a];
|
|
1000
|
-
const pb = points[idx_b];
|
|
1001
|
-
const dist_sq = (pa.x - pb.x) ** 2 + (pa.y - pb.y) ** 2 + (pa.z - pb.z) ** 2 + (pa.w - pb.w) ** 2;
|
|
1002
|
-
if (dist_sq > max_dist_sq) {
|
|
1003
|
-
max_dist_sq = dist_sq;
|
|
1004
|
-
idx_far_a = idx_a;
|
|
1005
|
-
idx_far_b = idx_b;
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
if (idx_far_a === idx_far_b || max_dist_sq < EPS)
|
|
1010
|
-
return null;
|
|
1011
|
-
// Find point farthest from line through idx_far_a and idx_far_b
|
|
1012
|
-
let idx_far_line = -1;
|
|
1013
|
-
let best_dist_line = -1;
|
|
1014
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
1015
|
-
if (idx === idx_far_a || idx === idx_far_b)
|
|
1016
|
-
continue;
|
|
1017
|
-
const dist = distance_point_to_line_4d(points[idx_far_a], points[idx_far_b], points[idx]);
|
|
1018
|
-
if (dist > best_dist_line) {
|
|
1019
|
-
best_dist_line = dist;
|
|
1020
|
-
idx_far_line = idx;
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
if (idx_far_line === -1 || best_dist_line < EPS)
|
|
1024
|
-
return null;
|
|
1025
|
-
// Find point farthest from 2D plane through first three points
|
|
1026
|
-
let idx_far_plane = -1;
|
|
1027
|
-
let best_dist_plane = -1;
|
|
1028
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
1029
|
-
if (idx === idx_far_a || idx === idx_far_b || idx === idx_far_line)
|
|
1030
|
-
continue;
|
|
1031
|
-
const dist = distance_point_to_hyperplane_4d(points[idx_far_a], points[idx_far_b], points[idx_far_line], points[idx]);
|
|
1032
|
-
if (dist > best_dist_plane) {
|
|
1033
|
-
best_dist_plane = dist;
|
|
1034
|
-
idx_far_plane = idx;
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
if (idx_far_plane === -1 || best_dist_plane < EPS)
|
|
1038
|
-
return null;
|
|
1039
|
-
// Find point farthest from 3D hyperplane through first four points
|
|
1040
|
-
const plane0 = compute_plane_4d(points[idx_far_a], points[idx_far_b], points[idx_far_line], points[idx_far_plane]);
|
|
1041
|
-
let idx_far_hyperplane = -1;
|
|
1042
|
-
let best_dist_hyperplane = -1;
|
|
1043
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
1044
|
-
if (idx === idx_far_a ||
|
|
1045
|
-
idx === idx_far_b ||
|
|
1046
|
-
idx === idx_far_line ||
|
|
1047
|
-
idx === idx_far_plane)
|
|
1048
|
-
continue;
|
|
1049
|
-
const dist = Math.abs(point_plane_signed_distance_4d(plane0, points[idx]));
|
|
1050
|
-
if (dist > best_dist_hyperplane) {
|
|
1051
|
-
best_dist_hyperplane = dist;
|
|
1052
|
-
idx_far_hyperplane = idx;
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (idx_far_hyperplane === -1 || best_dist_hyperplane < EPS)
|
|
1056
|
-
return null;
|
|
1057
|
-
return [idx_far_a, idx_far_b, idx_far_line, idx_far_plane, idx_far_hyperplane];
|
|
1058
|
-
}
|
|
1059
|
-
function make_face_4d(points, a, b, c, d, interior_point) {
|
|
1060
|
-
let plane = compute_plane_4d(points[a], points[b], points[c], points[d]);
|
|
1061
|
-
let centroid = compute_centroid_4d(points[a], points[b], points[c], points[d]);
|
|
1062
|
-
const dist_interior = point_plane_signed_distance_4d(plane, interior_point);
|
|
1063
|
-
// Ensure normal points outward (away from interior)
|
|
1064
|
-
if (dist_interior > 0) {
|
|
1065
|
-
// Swap two vertices to flip normal
|
|
1066
|
-
plane = compute_plane_4d(points[a], points[c], points[b], points[d]);
|
|
1067
|
-
centroid = compute_centroid_4d(points[a], points[c], points[b], points[d]);
|
|
1068
|
-
return { vertices: [a, c, b, d], plane, centroid, outside_points: new Set() };
|
|
1069
|
-
}
|
|
1070
|
-
return { vertices: [a, b, c, d], plane, centroid, outside_points: new Set() };
|
|
1071
|
-
}
|
|
1072
|
-
function assign_outside_points_4d(face, points, candidate_indices) {
|
|
1073
|
-
face.outside_points.clear();
|
|
1074
|
-
for (const idx of candidate_indices) {
|
|
1075
|
-
const distance = point_plane_signed_distance_4d(face.plane, points[idx]);
|
|
1076
|
-
if (distance > EPS)
|
|
1077
|
-
face.outside_points.add(idx);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
function collect_candidate_points_4d(faces) {
|
|
1081
|
-
const set = new Set();
|
|
1082
|
-
for (const face of faces) {
|
|
1083
|
-
for (const idx of face.outside_points)
|
|
1084
|
-
set.add(idx);
|
|
1085
|
-
}
|
|
1086
|
-
return Array.from(set);
|
|
1087
|
-
}
|
|
1088
|
-
function farthest_point_for_face_4d(points, face) {
|
|
1089
|
-
let best_idx = -1;
|
|
1090
|
-
let best_distance = -1;
|
|
1091
|
-
for (const idx of face.outside_points) {
|
|
1092
|
-
const distance = point_plane_signed_distance_4d(face.plane, points[idx]);
|
|
1093
|
-
if (distance > best_distance) {
|
|
1094
|
-
best_distance = distance;
|
|
1095
|
-
best_idx = idx;
|
|
1096
|
-
}
|
|
1097
|
-
}
|
|
1098
|
-
if (best_idx === -1)
|
|
1099
|
-
return null;
|
|
1100
|
-
return { idx: best_idx, distance: best_distance };
|
|
1101
|
-
}
|
|
1102
|
-
function build_horizon_4d(faces, visible_face_indices) {
|
|
1103
|
-
// In 4D, horizon "ridges" are triangles (3 vertices)
|
|
1104
|
-
const ridge_count = new Map();
|
|
1105
|
-
for (const face_idx of visible_face_indices) {
|
|
1106
|
-
const face = faces[face_idx];
|
|
1107
|
-
const [a, b, c, d] = face.vertices;
|
|
1108
|
-
// Each tetrahedron face has 4 triangular ridges
|
|
1109
|
-
const ridges = [
|
|
1110
|
-
[a, b, c],
|
|
1111
|
-
[a, b, d],
|
|
1112
|
-
[a, c, d],
|
|
1113
|
-
[b, c, d],
|
|
1114
|
-
];
|
|
1115
|
-
for (const ridge of ridges) {
|
|
1116
|
-
const sorted = ridge.slice().sort((x, y) => x - y);
|
|
1117
|
-
const key = sorted.join(`|`);
|
|
1118
|
-
if (!ridge_count.has(key)) {
|
|
1119
|
-
ridge_count.set(key, ridge);
|
|
1120
|
-
}
|
|
1121
|
-
else {
|
|
1122
|
-
// Mark as seen twice (internal ridge)
|
|
1123
|
-
ridge_count.set(key, [Number.NaN, Number.NaN, Number.NaN]);
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
const horizon = [];
|
|
1128
|
-
for (const ridge of ridge_count.values()) {
|
|
1129
|
-
if (!Number.isNaN(ridge[0])) {
|
|
1130
|
-
horizon.push(ridge);
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
return horizon;
|
|
1134
|
-
}
|
|
520
|
+
};
|
|
521
|
+
// 4D quickhull (thin adapter over the N-dimensional implementation)
|
|
1135
522
|
export function compute_quickhull_4d(points) {
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
const initial = choose_initial_4_simplex(points);
|
|
1139
|
-
if (!initial)
|
|
1140
|
-
return [];
|
|
1141
|
-
const [i0, i1, i2, i3, i4] = initial;
|
|
1142
|
-
// Interior point for orientation
|
|
1143
|
-
const interior_point = {
|
|
1144
|
-
x: (points[i0].x + points[i1].x + points[i2].x + points[i3].x + points[i4].x) / 5,
|
|
1145
|
-
y: (points[i0].y + points[i1].y + points[i2].y + points[i3].y + points[i4].y) / 5,
|
|
1146
|
-
z: (points[i0].z + points[i1].z + points[i2].z + points[i3].z + points[i4].z) / 5,
|
|
1147
|
-
w: (points[i0].w + points[i1].w + points[i2].w + points[i3].w + points[i4].w) / 5,
|
|
1148
|
-
};
|
|
1149
|
-
// Initial 4-simplex has 5 tetrahedral faces
|
|
1150
|
-
const faces = [
|
|
1151
|
-
make_face_4d(points, i0, i1, i2, i3, interior_point),
|
|
1152
|
-
make_face_4d(points, i0, i1, i2, i4, interior_point),
|
|
1153
|
-
make_face_4d(points, i0, i1, i3, i4, interior_point),
|
|
1154
|
-
make_face_4d(points, i0, i2, i3, i4, interior_point),
|
|
1155
|
-
make_face_4d(points, i1, i2, i3, i4, interior_point),
|
|
1156
|
-
];
|
|
1157
|
-
const all_indices = [];
|
|
1158
|
-
for (let idx = 0; idx < points.length; idx++) {
|
|
1159
|
-
if (idx === i0 || idx === i1 || idx === i2 || idx === i3 || idx === i4)
|
|
1160
|
-
continue;
|
|
1161
|
-
all_indices.push(idx);
|
|
1162
|
-
}
|
|
1163
|
-
for (const face of faces) {
|
|
1164
|
-
assign_outside_points_4d(face, points, all_indices);
|
|
1165
|
-
}
|
|
1166
|
-
// Main Quick Hull iteration
|
|
1167
|
-
while (true) {
|
|
1168
|
-
// Step 1: Find face with farthest outside point (the "eye" point)
|
|
1169
|
-
let chosen_face_idx = -1;
|
|
1170
|
-
let chosen_point_idx = -1;
|
|
1171
|
-
let max_distance = -1;
|
|
1172
|
-
for (let face_idx = 0; face_idx < faces.length; face_idx++) {
|
|
1173
|
-
const face = faces[face_idx];
|
|
1174
|
-
if (face.outside_points.size === 0)
|
|
1175
|
-
continue;
|
|
1176
|
-
const far = farthest_point_for_face_4d(points, face);
|
|
1177
|
-
if (far && far.distance > max_distance) {
|
|
1178
|
-
max_distance = far.distance;
|
|
1179
|
-
chosen_face_idx = face_idx;
|
|
1180
|
-
chosen_point_idx = far.idx;
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
if (chosen_face_idx === -1)
|
|
1184
|
-
break; // All points processed
|
|
1185
|
-
const eye_idx = chosen_point_idx;
|
|
1186
|
-
// Step 2: Find all faces visible from the eye point
|
|
1187
|
-
const visible_face_indices = new Set();
|
|
1188
|
-
for (let face_idx = 0; face_idx < faces.length; face_idx++) {
|
|
1189
|
-
const face = faces[face_idx];
|
|
1190
|
-
const dist = point_plane_signed_distance_4d(face.plane, points[eye_idx]);
|
|
1191
|
-
if (dist > EPS)
|
|
1192
|
-
visible_face_indices.add(face_idx);
|
|
1193
|
-
}
|
|
1194
|
-
// Step 3: Build horizon ridges (boundary between visible and non-visible faces)
|
|
1195
|
-
const horizon_ridges = build_horizon_4d(faces, visible_face_indices);
|
|
1196
|
-
const visible_faces = Array.from(visible_face_indices).sort((a, b) => b - a);
|
|
1197
|
-
const candidate_points = collect_candidate_points_4d(visible_faces.map((idx) => faces[idx]));
|
|
1198
|
-
// Step 4: Remove visible faces (they'll be replaced by new ones through eye point)
|
|
1199
|
-
for (const idx of visible_faces) {
|
|
1200
|
-
faces.splice(idx, 1);
|
|
1201
|
-
}
|
|
1202
|
-
// Step 5: Create new faces connecting horizon ridges to eye point
|
|
1203
|
-
const new_faces = [];
|
|
1204
|
-
for (const [u, v, w] of horizon_ridges) {
|
|
1205
|
-
const new_face = make_face_4d(points, u, v, w, eye_idx, interior_point);
|
|
1206
|
-
new_faces.push(new_face);
|
|
1207
|
-
}
|
|
1208
|
-
// Step 6: Reassign outside points from removed faces to new faces
|
|
1209
|
-
for (const face of new_faces)
|
|
1210
|
-
face.outside_points.clear();
|
|
1211
|
-
for (const idx of candidate_points) {
|
|
1212
|
-
if (idx === eye_idx)
|
|
1213
|
-
continue;
|
|
1214
|
-
let best_face = null;
|
|
1215
|
-
let best_distance = EPS;
|
|
1216
|
-
for (const face of new_faces) {
|
|
1217
|
-
const dist = point_plane_signed_distance_4d(face.plane, points[idx]);
|
|
1218
|
-
if (dist > best_distance) {
|
|
1219
|
-
best_distance = dist;
|
|
1220
|
-
best_face = face;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
if (best_face)
|
|
1224
|
-
best_face.outside_points.add(idx);
|
|
1225
|
-
}
|
|
1226
|
-
faces.push(...new_faces);
|
|
1227
|
-
}
|
|
1228
|
-
return faces.map((face) => {
|
|
1229
|
-
const [a, b, c, d] = face.vertices;
|
|
1230
|
-
return {
|
|
1231
|
-
vertices: [points[a], points[b], points[c], points[d]],
|
|
1232
|
-
normal: face.plane.normal,
|
|
1233
|
-
centroid: face.centroid,
|
|
1234
|
-
};
|
|
1235
|
-
});
|
|
523
|
+
const facets = compute_quickhull_nd(points.map(p4_to_nd));
|
|
524
|
+
return facets.map((facet) => facet_to_tetrahedron(facet, points));
|
|
1236
525
|
}
|
|
1237
526
|
export function compute_lower_hull_4d(points) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
//
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
[p0.x, p1.x, p2.x, p3.x],
|
|
1249
|
-
[p0.y, p1.y, p2.y, p3.y],
|
|
1250
|
-
[p0.z, p1.z, p2.z, p3.z],
|
|
1251
|
-
[1, 1, 1, 1],
|
|
1252
|
-
];
|
|
1253
|
-
const rhs = [point.x, point.y, point.z, 1];
|
|
1254
|
-
// Solve using Cramer's rule with 4x4 determinants
|
|
1255
|
-
const det_main = math.det_4x4(matrix);
|
|
1256
|
-
if (Math.abs(det_main) < EPS) {
|
|
1257
|
-
return { inside: false, bary: [0, 0, 0, 0] };
|
|
1258
|
-
}
|
|
1259
|
-
// Compute barycentric coordinates using Cramer's rule
|
|
1260
|
-
const bary = [0, 0, 0, 0];
|
|
1261
|
-
for (let idx = 0; idx < 4; idx++) {
|
|
1262
|
-
const m_i = matrix.map((row) => [...row]);
|
|
1263
|
-
for (let row = 0; row < 4; row++) {
|
|
1264
|
-
m_i[row][idx] = rhs[row];
|
|
1265
|
-
}
|
|
1266
|
-
bary[idx] = math.det_4x4(m_i) / det_main;
|
|
1267
|
-
}
|
|
1268
|
-
// Check if inside: all barycentric coords must be >= 0 and sum to 1
|
|
1269
|
-
const eps_bary = -1e-9;
|
|
1270
|
-
const inside = bary.every((coord) => coord >= eps_bary) &&
|
|
1271
|
-
Math.abs(bary.reduce((sum, coord) => sum + coord, 0) - 1) < 1e-6;
|
|
1272
|
-
return { inside, bary };
|
|
1273
|
-
}
|
|
1274
|
-
const build_tetrahedron_models = (hull_tetrahedra) => hull_tetrahedra.map((tet) => {
|
|
1275
|
-
const [p0, p1, p2, p3] = tet.vertices;
|
|
1276
|
-
const vertices_3d = [
|
|
1277
|
-
{ x: p0.x, y: p0.y, z: p0.z },
|
|
1278
|
-
{ x: p1.x, y: p1.y, z: p1.z },
|
|
1279
|
-
{ x: p2.x, y: p2.y, z: p2.z },
|
|
1280
|
-
{ x: p3.x, y: p3.y, z: p3.z },
|
|
1281
|
-
];
|
|
1282
|
-
const xs = [p0.x, p1.x, p2.x, p3.x];
|
|
1283
|
-
const ys = [p0.y, p1.y, p2.y, p3.y];
|
|
1284
|
-
const zs = [p0.z, p1.z, p2.z, p3.z];
|
|
1285
|
-
return {
|
|
1286
|
-
vertices: tet.vertices,
|
|
1287
|
-
vertices_3d,
|
|
1288
|
-
min_x: Math.min(...xs),
|
|
1289
|
-
max_x: Math.max(...xs),
|
|
1290
|
-
min_y: Math.min(...ys),
|
|
1291
|
-
max_y: Math.max(...ys),
|
|
1292
|
-
min_z: Math.min(...zs),
|
|
1293
|
-
max_z: Math.max(...zs),
|
|
1294
|
-
};
|
|
1295
|
-
});
|
|
1296
|
-
// Compute distance from point to lower hull in 4D
|
|
1297
|
-
export const compute_e_above_hull_4d = (points, hull_tetrahedra) => {
|
|
1298
|
-
// Precompute bounding boxes for fast prefiltering
|
|
1299
|
-
const models = build_tetrahedron_models(hull_tetrahedra);
|
|
1300
|
-
return points.map(({ x, y, z, w }) => {
|
|
1301
|
-
let hull_w = null;
|
|
1302
|
-
for (const model of models) {
|
|
1303
|
-
// Fast bounding box prefilter
|
|
1304
|
-
if (x < model.min_x - EPS ||
|
|
1305
|
-
x > model.max_x + EPS ||
|
|
1306
|
-
y < model.min_y - EPS ||
|
|
1307
|
-
y > model.max_y + EPS ||
|
|
1308
|
-
z < model.min_z - EPS ||
|
|
1309
|
-
z > model.max_z + EPS)
|
|
1310
|
-
continue;
|
|
1311
|
-
// Check if point's (x,y,z) is inside the 3D projection of the tetrahedron
|
|
1312
|
-
const { inside, bary } = point_in_tetrahedron_3d(model.vertices_3d[0], model.vertices_3d[1], model.vertices_3d[2], model.vertices_3d[3], { x, y, z });
|
|
1313
|
-
if (inside) {
|
|
1314
|
-
// Compute w on the hull at this (x,y,z) using barycentric interpolation
|
|
1315
|
-
const [p0, p1, p2, p3] = model.vertices;
|
|
1316
|
-
const w_on_hull = bary[0] * p0.w + bary[1] * p1.w + bary[2] * p2.w + bary[3] * p3.w;
|
|
1317
|
-
hull_w = hull_w === null ? w_on_hull : Math.min(hull_w, w_on_hull);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
// If no tetrahedron contains this point's spatial projection, it's outside the valid
|
|
1321
|
-
// composition domain. Return NaN to indicate invalid input.
|
|
1322
|
-
if (hull_w === null)
|
|
1323
|
-
return NaN;
|
|
1324
|
-
return w - hull_w;
|
|
1325
|
-
});
|
|
1326
|
-
};
|
|
1327
|
-
// --- N-Dimensional Convex Hull (for 5+ element systems) ---
|
|
1328
|
-
// N-dimensional vector operations with dimension validation
|
|
1329
|
-
const subtract_nd = (vec_a, vec_b) => {
|
|
1330
|
-
if (vec_a.length !== vec_b.length) {
|
|
1331
|
-
throw new Error(`Vector dimension mismatch: ${vec_a.length} vs ${vec_b.length}`);
|
|
1332
|
-
}
|
|
1333
|
-
return vec_a.map((val, idx) => val - vec_b[idx]);
|
|
1334
|
-
};
|
|
527
|
+
// Filter for "lower" faces: those with normal pointing down in w (energy) direction
|
|
528
|
+
return compute_quickhull_4d(points).filter((tet) => tet.normal.w < 0 - EPS);
|
|
529
|
+
}
|
|
530
|
+
// Compute distance from point to lower hull in 4D (w is the energy dimension).
|
|
531
|
+
// Returns raw (unclamped) distances; NaN for points outside the composition domain.
|
|
532
|
+
export const compute_e_above_hull_4d = (points, hull_tetrahedra) => e_above_hull_from_simplices(points.map(p4_to_nd), hull_tetrahedra.map((tet) => tet.vertices.map(p4_to_nd)));
|
|
533
|
+
// --- N-Dimensional Convex Hull (single quickhull core; the 3D/4D APIs above adapt to it) ---
|
|
534
|
+
// N-dimensional vector operations. These run in quickhull's hot loops, so dimension
|
|
535
|
+
// agreement is validated once per compute_quickhull_nd call instead of per operation.
|
|
536
|
+
const subtract_nd = (vec_a, vec_b) => vec_a.map((val, idx) => val - vec_b[idx]);
|
|
1335
537
|
const dot_nd = (vec_a, vec_b) => {
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
return
|
|
538
|
+
let sum = 0;
|
|
539
|
+
for (let idx = 0; idx < vec_a.length; idx++)
|
|
540
|
+
sum += vec_a[idx] * vec_b[idx];
|
|
541
|
+
return sum;
|
|
1340
542
|
};
|
|
1341
543
|
const norm_nd = (vec) => Math.sqrt(dot_nd(vec, vec));
|
|
1342
544
|
const normalize_nd = (vec) => {
|
|
@@ -1348,8 +550,8 @@ const normalize_nd = (vec) => {
|
|
|
1348
550
|
// Compute normal to hyperplane through N points in N-dimensional space
|
|
1349
551
|
// Uses null space computation via cofactor expansion
|
|
1350
552
|
function compute_hyperplane_nd(points) {
|
|
1351
|
-
const
|
|
1352
|
-
if (
|
|
553
|
+
const n_points = points.length;
|
|
554
|
+
if (n_points < 2)
|
|
1353
555
|
return { normal: [], offset: 0 };
|
|
1354
556
|
// Build (N-1) edge vectors from points[0]
|
|
1355
557
|
const edges = points.slice(1).map((pt) => subtract_nd(pt, points[0]));
|
|
@@ -1377,16 +579,18 @@ const compute_centroid_nd = (points) => {
|
|
|
1377
579
|
const dim = points[0].length;
|
|
1378
580
|
return Array.from({ length: dim }, (_, idx) => points.reduce((sum, pt) => sum + pt[idx], 0) / points.length);
|
|
1379
581
|
};
|
|
582
|
+
// Maximum sample size for initial simplex selection (avoids O(n²) for large datasets)
|
|
583
|
+
const INITIAL_SIMPLEX_SAMPLE_SIZE = 100;
|
|
1380
584
|
// Find N+1 points that span N dimensions (initial simplex for quickhull)
|
|
1381
585
|
function choose_initial_simplex_nd(points) {
|
|
1382
|
-
const
|
|
1383
|
-
if (!
|
|
586
|
+
const dim = points[0]?.length;
|
|
587
|
+
if (!dim || points.length < dim + 1)
|
|
1384
588
|
return null;
|
|
1385
589
|
const chosen = [];
|
|
1386
590
|
// Greedily pick points that maximize distance from current affine hull
|
|
1387
591
|
// Start with two points that are farthest apart
|
|
1388
592
|
let [best_i, best_j, best_dist] = [0, 1, -1];
|
|
1389
|
-
const sample_size = Math.min(points.length,
|
|
593
|
+
const sample_size = Math.min(points.length, INITIAL_SIMPLEX_SAMPLE_SIZE);
|
|
1390
594
|
const sample_indices = points.length <= sample_size
|
|
1391
595
|
? points.map((_, idx) => idx)
|
|
1392
596
|
: Array.from({ length: sample_size }, (_, idx) => Math.floor((idx * points.length) / sample_size));
|
|
@@ -1407,7 +611,7 @@ function choose_initial_simplex_nd(points) {
|
|
|
1407
611
|
chosen.push(best_i, best_j);
|
|
1408
612
|
const chosen_set = new Set(chosen);
|
|
1409
613
|
// Add remaining points to span higher dimensions
|
|
1410
|
-
while (chosen.length <
|
|
614
|
+
while (chosen.length < dim + 1) {
|
|
1411
615
|
let [best_idx, best_distance] = [-1, -1];
|
|
1412
616
|
// Hoist chosen_points computation outside inner loop for O(n) instead of O(n²)
|
|
1413
617
|
const chosen_points = chosen.map((idx_c) => points[idx_c]);
|
|
@@ -1441,8 +645,8 @@ function distance_to_affine_hull_nd(point, hull_points) {
|
|
|
1441
645
|
const ab_len_sq = dot_nd(ab, ab);
|
|
1442
646
|
if (ab_len_sq < EPS)
|
|
1443
647
|
return norm_nd(ap);
|
|
1444
|
-
const
|
|
1445
|
-
const proj = pt_a.map((val, idx) => val +
|
|
648
|
+
const proj_frac = dot_nd(ap, ab) / ab_len_sq;
|
|
649
|
+
const proj = pt_a.map((val, idx) => val + proj_frac * ab[idx]);
|
|
1446
650
|
return norm_nd(subtract_nd(point, proj));
|
|
1447
651
|
}
|
|
1448
652
|
// For 3+ points, use orthogonal projection
|
|
@@ -1453,8 +657,8 @@ function distance_to_affine_hull_nd(point, hull_points) {
|
|
|
1453
657
|
// Solve least squares using Gram matrix G[i][j] = dot(edge_i, edge_j)
|
|
1454
658
|
const gram = edges.map((edge_i) => edges.map((edge_j) => dot_nd(edge_i, edge_j)));
|
|
1455
659
|
const rhs = edges.map((edge) => dot_nd(edge, vp));
|
|
1456
|
-
// Solve Gram * coeffs = rhs
|
|
1457
|
-
const coeffs = solve_linear_system(gram, rhs);
|
|
660
|
+
// Solve Gram * coeffs = rhs
|
|
661
|
+
const coeffs = math.solve_linear_system(gram, rhs);
|
|
1458
662
|
if (!coeffs) {
|
|
1459
663
|
// Fallback: Gram-Schmidt when Gram matrix is singular (linearly dependent edges)
|
|
1460
664
|
// Build orthogonal basis and accumulate projection in single pass
|
|
@@ -1484,50 +688,6 @@ function distance_to_affine_hull_nd(point, hull_points) {
|
|
|
1484
688
|
const proj = origin.map((val, dim) => val + coeffs.reduce((sum, coeff, idx) => sum + coeff * edges[idx][dim], 0));
|
|
1485
689
|
return norm_nd(subtract_nd(point, proj));
|
|
1486
690
|
}
|
|
1487
|
-
// Solve linear system Ax = b using Gaussian elimination with partial pivoting
|
|
1488
|
-
function solve_linear_system(matrix_a, vec_b) {
|
|
1489
|
-
const n = matrix_a.length;
|
|
1490
|
-
if (n === 0)
|
|
1491
|
-
return [];
|
|
1492
|
-
if (vec_b.length !== n)
|
|
1493
|
-
return null; // Dimension mismatch
|
|
1494
|
-
// Augmented matrix
|
|
1495
|
-
const aug = matrix_a.map((row, idx) => [...row, vec_b[idx]]);
|
|
1496
|
-
for (let col = 0; col < n; col++) {
|
|
1497
|
-
// Find pivot
|
|
1498
|
-
let max_row = col;
|
|
1499
|
-
for (let row = col + 1; row < n; row++) {
|
|
1500
|
-
if (Math.abs(aug[row][col]) > Math.abs(aug[max_row][col])) {
|
|
1501
|
-
max_row = row;
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
if (Math.abs(aug[max_row][col]) < EPS)
|
|
1505
|
-
return null; // Singular
|
|
1506
|
-
// Swap rows if needed
|
|
1507
|
-
if (max_row !== col) {
|
|
1508
|
-
const temp = aug[col];
|
|
1509
|
-
aug[col] = aug[max_row];
|
|
1510
|
-
aug[max_row] = temp;
|
|
1511
|
-
}
|
|
1512
|
-
// Eliminate
|
|
1513
|
-
for (let row = col + 1; row < n; row++) {
|
|
1514
|
-
const factor = aug[row][col] / aug[col][col];
|
|
1515
|
-
for (let elim_col = col; elim_col <= n; elim_col++) {
|
|
1516
|
-
aug[row][elim_col] -= factor * aug[col][elim_col];
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
// Back substitution
|
|
1521
|
-
const result = Array(n).fill(0);
|
|
1522
|
-
for (let row = n - 1; row >= 0; row--) {
|
|
1523
|
-
let sum = aug[row][n];
|
|
1524
|
-
for (let col = row + 1; col < n; col++) {
|
|
1525
|
-
sum -= aug[row][col] * result[col];
|
|
1526
|
-
}
|
|
1527
|
-
result[row] = sum / aug[row][row];
|
|
1528
|
-
}
|
|
1529
|
-
return result;
|
|
1530
|
-
}
|
|
1531
691
|
// Create a simplex face with correct normal orientation (outward from interior)
|
|
1532
692
|
function make_face_nd(points, vertex_indices, interior_point) {
|
|
1533
693
|
const face_points = vertex_indices.map((idx) => points[idx]);
|
|
@@ -1565,9 +725,9 @@ function build_horizon_nd(faces, visible_indices) {
|
|
|
1565
725
|
for (const face_idx of visible_indices) {
|
|
1566
726
|
const face = faces[face_idx];
|
|
1567
727
|
const verts = face.vertex_indices;
|
|
1568
|
-
const
|
|
1569
|
-
// Each face has
|
|
1570
|
-
for (let skip = 0; skip <
|
|
728
|
+
const n_verts = verts.length;
|
|
729
|
+
// Each face has n_verts ridges, each ridge omits one vertex
|
|
730
|
+
for (let skip = 0; skip < n_verts; skip++) {
|
|
1571
731
|
const ridge = verts.filter((_, idx) => idx !== skip);
|
|
1572
732
|
const sorted = [...ridge].sort((a, b) => a - b);
|
|
1573
733
|
const key = sorted.join(`|`);
|
|
@@ -1587,8 +747,14 @@ function build_horizon_nd(faces, visible_indices) {
|
|
|
1587
747
|
export function compute_quickhull_nd(points) {
|
|
1588
748
|
if (points.length === 0)
|
|
1589
749
|
return [];
|
|
1590
|
-
const
|
|
1591
|
-
|
|
750
|
+
const dim = points[0].length;
|
|
751
|
+
// Validate dimensions once up front; vector ops in the hot loops assume agreement
|
|
752
|
+
for (const pt of points) {
|
|
753
|
+
if (pt.length !== dim) {
|
|
754
|
+
throw new Error(`Vector dimension mismatch: ${pt.length} vs ${dim}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (points.length < dim + 1)
|
|
1592
758
|
return [];
|
|
1593
759
|
// Find initial n-simplex
|
|
1594
760
|
const initial = choose_initial_simplex_nd(points);
|
|
@@ -1596,9 +762,9 @@ export function compute_quickhull_nd(points) {
|
|
|
1596
762
|
return [];
|
|
1597
763
|
// Interior point for normal orientation
|
|
1598
764
|
const interior = compute_centroid_nd(initial.map((idx) => points[idx]));
|
|
1599
|
-
// Create initial
|
|
765
|
+
// Create initial dim+1 facets (each omits one vertex from the simplex)
|
|
1600
766
|
const faces = [];
|
|
1601
|
-
for (let skip = 0; skip <=
|
|
767
|
+
for (let skip = 0; skip <= dim; skip++) {
|
|
1602
768
|
const verts = initial.filter((_, idx) => idx !== skip);
|
|
1603
769
|
faces.push(make_face_nd(points, verts, interior));
|
|
1604
770
|
}
|
|
@@ -1679,13 +845,12 @@ export function compute_lower_hull_nd(faces) {
|
|
|
1679
845
|
// Last dimension is energy; negative normal means "downward"
|
|
1680
846
|
return faces.filter((face) => (face.plane.normal.at(-1) ?? 0) < -EPS);
|
|
1681
847
|
}
|
|
1682
|
-
const build_simplex_models_nd = (
|
|
1683
|
-
const
|
|
1684
|
-
const n = vertices[0].length;
|
|
848
|
+
const build_simplex_models_nd = (simplices) => simplices.map((vertices) => {
|
|
849
|
+
const dim = vertices[0].length;
|
|
1685
850
|
// Spatial coords are all except last (energy)
|
|
1686
|
-
const vertices_spatial = vertices.map((pt) => pt.slice(0,
|
|
851
|
+
const vertices_spatial = vertices.map((pt) => pt.slice(0, dim - 1));
|
|
1687
852
|
// Compute bounding box in spatial dimensions
|
|
1688
|
-
const spatial_dim =
|
|
853
|
+
const spatial_dim = dim - 1;
|
|
1689
854
|
const bbox_min = Array.from({ length: spatial_dim }, (_, idx) => Math.min(...vertices_spatial.map((pt) => pt[idx])));
|
|
1690
855
|
const bbox_max = Array.from({ length: spatial_dim }, (_, idx) => Math.max(...vertices_spatial.map((pt) => pt[idx])));
|
|
1691
856
|
return { vertices, vertices_spatial, bbox_min, bbox_max };
|
|
@@ -1693,11 +858,11 @@ const build_simplex_models_nd = (faces, points) => faces.map((face) => {
|
|
|
1693
858
|
// Check if point is inside simplex and return barycentric coordinates
|
|
1694
859
|
// Uses linear system solution: point = sum(bary[i] * vertex[i]) with sum(bary) = 1
|
|
1695
860
|
function point_in_simplex_nd(point, simplex_vertices) {
|
|
1696
|
-
const
|
|
1697
|
-
if (
|
|
861
|
+
const n_verts = simplex_vertices.length; // Number of vertices = spatial_dim + 1
|
|
862
|
+
if (n_verts === 0)
|
|
1698
863
|
return null;
|
|
1699
864
|
const dim = point.length;
|
|
1700
|
-
if (dim !==
|
|
865
|
+
if (dim !== n_verts - 1)
|
|
1701
866
|
return null; // Spatial dim should be one less than vertex count
|
|
1702
867
|
// Build linear system: [v1-v0, v2-v0, ..., vn-v0] * [b1, b2, ..., bn] = point - v0
|
|
1703
868
|
// Then b0 = 1 - sum(b1..bn)
|
|
@@ -1710,7 +875,7 @@ function point_in_simplex_nd(point, simplex_vertices) {
|
|
|
1710
875
|
for (let row = 0; row < dim; row++) {
|
|
1711
876
|
matrix.push(edges.map((edge) => edge[row]));
|
|
1712
877
|
}
|
|
1713
|
-
const coeffs = solve_linear_system(matrix, rhs);
|
|
878
|
+
const coeffs = math.solve_linear_system(matrix, rhs);
|
|
1714
879
|
if (!coeffs)
|
|
1715
880
|
return null;
|
|
1716
881
|
// Compute b0 = 1 - sum(coeffs), ensuring sum(bary) = 1 by construction
|
|
@@ -1721,11 +886,17 @@ function point_in_simplex_nd(point, simplex_vertices) {
|
|
|
1721
886
|
}
|
|
1722
887
|
// Compute energy above hull for N-dimensional points
|
|
1723
888
|
export function compute_e_above_hull_nd(query_points, hull_facets, all_points) {
|
|
1724
|
-
|
|
889
|
+
return e_above_hull_from_simplices(query_points, hull_facets.map((facet) => facet.vertex_indices.map((idx) => all_points[idx])));
|
|
890
|
+
}
|
|
891
|
+
// Shared e-above-hull core: query points against hull simplices given as vertex
|
|
892
|
+
// coordinate lists (last coordinate = energy). Returns raw (unclamped) distances;
|
|
893
|
+
// NaN for queries whose spatial projection lies outside all simplices.
|
|
894
|
+
function e_above_hull_from_simplices(query_points, simplices) {
|
|
895
|
+
const models = build_simplex_models_nd(simplices);
|
|
1725
896
|
return query_points.map((query) => {
|
|
1726
|
-
const
|
|
1727
|
-
const spatial = query.slice(0,
|
|
1728
|
-
const energy = query[
|
|
897
|
+
const dim = query.length;
|
|
898
|
+
const spatial = query.slice(0, dim - 1); // All but last coord
|
|
899
|
+
const energy = query[dim - 1];
|
|
1729
900
|
let hull_energy = null;
|
|
1730
901
|
for (const model of models) {
|
|
1731
902
|
// Fast bounding box rejection
|
|
@@ -1744,7 +915,7 @@ export function compute_e_above_hull_nd(query_points, hull_facets, all_points) {
|
|
|
1744
915
|
if (!bary)
|
|
1745
916
|
continue;
|
|
1746
917
|
// Interpolate energy using barycentric coords
|
|
1747
|
-
const e_hull = bary.reduce((sum, coeff, idx) => sum + coeff * model.vertices[idx][
|
|
918
|
+
const e_hull = bary.reduce((sum, coeff, idx) => sum + coeff * model.vertices[idx][dim - 1], 0);
|
|
1748
919
|
hull_energy = hull_energy === null ? e_hull : Math.min(hull_energy, e_hull);
|
|
1749
920
|
}
|
|
1750
921
|
// If no facet contains this point's spatial projection, it's outside the valid
|