matterviz 0.4.0 → 0.4.2

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.
Files changed (527) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -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 { barycentric_to_ternary_xyz, barycentric_to_tetrahedral, composition_to_barycentric_3d, composition_to_barycentric_4d, composition_to_barycentric_nd, } from './barycentric-coords';
4
- import { get_arity, HULL_STABILITY_TOL, is_on_hull, is_unary_entry } from './helpers';
3
+ import { composition_to_barycentric_nd } from './barycentric-coords';
4
+ import { array_max, array_min, 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).
@@ -51,13 +49,7 @@ export function process_hull_entries(entries) {
51
49
  const el_refs = Object.fromEntries(stable_entries
52
50
  .filter(is_unary_entry)
53
51
  .map((entry) => [Object.keys(entry.composition)[0], entry]));
54
- return {
55
- entries: normalized_entries,
56
- stable_entries,
57
- unstable_entries,
58
- elements,
59
- el_refs,
60
- };
52
+ return { entries: normalized_entries, stable_entries, unstable_entries, elements, el_refs };
61
53
  }
62
54
  // Get energy per atom with correction applied, or fallback to raw energy_per_atom/energy.
63
55
  // Note: correction is expected to be a total-entry value (eV), not per-atom.
@@ -101,8 +93,14 @@ export function find_lowest_energy_unary_refs(entries) {
101
93
  }
102
94
  return refs;
103
95
  }
104
- // Result key for an entry: entry_id, falling back to its serialized composition
105
- const id_of = (entry) => entry.entry_id ?? JSON.stringify(entry.composition);
96
+ // Result key: entry_id, else composition|energy|structure. Composition alone collides for
97
+ // same-stoichiometry polymorphs (last distance would win), so add energy + structure.
98
+ const id_of = (entry) => {
99
+ if (entry.entry_id)
100
+ return entry.entry_id;
101
+ const structure_hash = entry.structure ? JSON.stringify(entry.structure) : ``;
102
+ return `${JSON.stringify(entry.composition)}|${entry.energy}|${structure_hash}`;
103
+ };
106
104
  export function calculate_e_above_hull(input, reference_entries) {
107
105
  const is_single = !Array.isArray(input);
108
106
  const entries_of_interest = is_single ? [input] : input;
@@ -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 5+ uses generalized N-dimensional convex hull
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.
@@ -411,11 +302,12 @@ export function get_convex_hull_stats(processed_entries, elements, max_arity = 4
411
302
  const energies = processed_entries
412
303
  .map((entry) => entry.e_form_per_atom ?? entry.energy_per_atom ?? get_energy_per_atom(entry))
413
304
  .filter(Number.isFinite);
414
- // Use reduce instead of Math.min/max(...arr) to avoid stack overflow on large datasets
305
+ // array_min/array_max reduce instead of Math.min/max(...arr) to avoid stack
306
+ // overflow on large datasets
415
307
  const energy_range = energies.length > 0
416
308
  ? {
417
- min: energies.reduce((min, val) => (val < min ? val : min), Infinity),
418
- max: energies.reduce((max, val) => (val > max ? val : max), -Infinity),
309
+ min: array_min(energies),
310
+ max: array_max(energies),
419
311
  avg: energies.reduce((sum, val) => sum + val, 0) / energies.length,
420
312
  }
421
313
  : { min: 0, max: 0, avg: 0 };
@@ -424,7 +316,7 @@ export function get_convex_hull_stats(processed_entries, elements, max_arity = 4
424
316
  .filter((val) => typeof val === `number` && val >= 0);
425
317
  const hull_distance = hull_distances.length > 0
426
318
  ? {
427
- max: hull_distances.reduce((max, val) => (val > max ? val : max), -Infinity),
319
+ max: array_max(hull_distances),
428
320
  avg: hull_distances.reduce((sum, val) => sum + val, 0) / hull_distances.length,
429
321
  }
430
322
  : { max: 0, avg: 0 };
@@ -505,17 +397,11 @@ export function process_hull_for_stats(entries, elements) {
505
397
  }
506
398
  // --- 2D Convex Hull (Binary Phase Diagrams) ---
507
399
  export function compute_lower_hull_2d(points) {
508
- // Andrew's monotone chain for lower hull
509
- // Sort by x then y
510
- const sorted = [...points].sort((p1, p2) => p1.x - p2.x || p1.y - p2.y);
511
- const lower = [];
512
- for (const point of sorted) {
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;
400
+ // Andrew's monotone chain lower hull (Point2D adapter over math.monotone_chain)
401
+ const sorted = points
402
+ .map((pt) => [pt.x, pt.y])
403
+ .toSorted((a, b) => a[0] - b[0] || a[1] - b[1]);
404
+ return math.monotone_chain(sorted).map(([x, y]) => ({ x, y }));
519
405
  }
520
406
  export function interpolate_hull_2d(hull, x) {
521
407
  if (hull.length < 2)
@@ -529,255 +415,31 @@ export function interpolate_hull_2d(hull, x) {
529
415
  const p1 = hull[idx];
530
416
  const p2 = hull[idx + 1];
531
417
  if (x >= p1.x && x <= p2.x) {
532
- const t = (x - p1.x) / Math.max(1e-12, p2.x - p1.x);
533
- return p1.y * (1 - t) + p2.y * t;
418
+ const frac = (x - p1.x) / Math.max(1e-12, p2.x - p1.x);
419
+ return p1.y * (1 - frac) + p2.y * frac;
534
420
  }
535
421
  }
536
422
  return null;
537
423
  }
538
424
  // --- Convex hull geometry ---
539
425
  const EPS = 1e-9;
540
- const subtract = (pt1, pt2) => ({
541
- x: pt1.x - pt2.x,
542
- y: pt1.y - pt2.y,
543
- z: pt1.z - pt2.z,
544
- });
545
- const cross = (vec1, vec2) => ({
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,
426
+ // Point conversions between object-shaped public API points and ND number[] points
427
+ const p3_to_nd = (pt) => [pt.x, pt.y, pt.z];
428
+ const p4_to_nd = (pt) => [pt.x, pt.y, pt.z, pt.w];
429
+ // Map an ND facet (vertex indices into `points`) back to the public triangle shape
430
+ const facet_to_triangle = (facet, points) => ({
431
+ vertices: facet.vertex_indices.map((idx) => points[idx]),
432
+ normal: { x: facet.plane.normal[0], y: facet.plane.normal[1], z: facet.plane.normal[2] },
433
+ centroid: { x: facet.centroid[0], y: facet.centroid[1], z: facet.centroid[2] },
549
434
  });
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
- }
435
+ // 3D quickhull (thin adapter over the N-dimensional implementation)
687
436
  export function compute_quickhull_triangles(points) {
688
- if (points.length < 4)
689
- return []; // hull needs at least 4 non-coplanar points, bail if not provided
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
- });
437
+ const facets = compute_quickhull_nd(points.map(p3_to_nd));
438
+ return facets.map((facet) => facet_to_triangle(facet, points));
777
439
  }
778
440
  export function compute_lower_hull_triangles(points) {
779
- const all_faces = compute_quickhull_triangles(points);
780
- return all_faces.filter((face) => face.normal.z < 0 - EPS);
441
+ // Lower hull faces point "down" in the z (energy) direction
442
+ return compute_quickhull_triangles(points).filter((face) => face.normal.z < 0 - EPS);
781
443
  }
782
444
  export const build_lower_hull_model = (faces) => faces.map((tri) => {
783
445
  const [p1, p2, p3] = tri.vertices;
@@ -844,499 +506,40 @@ export function e_hull_at_xy(models, x, y) {
844
506
  export const compute_e_above_hull_for_points = (points, models) => points.map((point) => {
845
507
  const z_hull = e_hull_at_xy(models, point.x, point.y);
846
508
  if (z_hull === null)
847
- return 0;
509
+ return NaN; // unknown: no hull face covers this point
848
510
  return Math.max(0, point.z - z_hull);
849
511
  });
850
- const subtract_4d = (pt1, pt2) => ({
851
- x: pt1.x - pt2.x,
852
- y: pt1.y - pt2.y,
853
- z: pt1.z - pt2.z,
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 };
512
+ // Map an ND facet (vertex indices into `points`) back to the public tetrahedron shape
513
+ const facet_to_tetrahedron = (facet, points) => {
514
+ const [nx, ny, nz, nw] = facet.plane.normal;
515
+ const [cx, cy, cz, cw] = facet.centroid;
862
516
  return {
863
- x: point.x / length,
864
- y: point.y / length,
865
- z: point.z / length,
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);
517
+ vertices: facet.vertex_indices.map((idx) => points[idx]),
518
+ normal: { x: nx, y: ny, z: nz, w: nw },
519
+ centroid: { x: cx, y: cy, z: cz, w: cw },
910
520
  };
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,
978
- };
979
- return norm_4d(subtract_4d(p, projection));
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
- }
521
+ };
522
+ // 4D quickhull (thin adapter over the N-dimensional implementation)
1135
523
  export function compute_quickhull_4d(points) {
1136
- if (points.length < 5)
1137
- return []; // Need at least 5 non-coplanar points for 4D hull
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
- });
524
+ const facets = compute_quickhull_nd(points.map(p4_to_nd));
525
+ return facets.map((facet) => facet_to_tetrahedron(facet, points));
1236
526
  }
1237
527
  export function compute_lower_hull_4d(points) {
1238
- const all_faces = compute_quickhull_4d(points);
1239
- // Filter for "lower" faces: those with normal pointing down in w direction
1240
- return all_faces.filter((face) => face.normal.w < 0 - EPS);
1241
- }
1242
- // Check if 3D point (x,y,z) is inside 3D tetrahedron using barycentric coordinates
1243
- function point_in_tetrahedron_3d(p0, p1, p2, p3, point) {
1244
- // Solve for barycentric coordinates: point = l0*p0 + l1*p1 + l2*p2 + l3*p3
1245
- // with l0 + l1 + l2 + l3 = 1
1246
- // Build the linear system
1247
- const matrix = [
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
- };
528
+ // Filter for "lower" faces: those with normal pointing down in w (energy) direction
529
+ return compute_quickhull_4d(points).filter((tet) => tet.normal.w < 0 - EPS);
530
+ }
531
+ // Compute distance from point to lower hull in 4D (w is the energy dimension).
532
+ // Returns raw (unclamped) distances; NaN for points outside the composition domain.
533
+ 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)));
534
+ // --- N-Dimensional Convex Hull (single quickhull core; the 3D/4D APIs above adapt to it) ---
535
+ // N-dimensional vector operations. These run in quickhull's hot loops, so dimension
536
+ // agreement is validated once per compute_quickhull_nd call instead of per operation.
537
+ const subtract_nd = (vec_a, vec_b) => vec_a.map((val, idx) => val - vec_b[idx]);
1335
538
  const dot_nd = (vec_a, vec_b) => {
1336
- if (vec_a.length !== vec_b.length) {
1337
- throw new Error(`Vector dimension mismatch: ${vec_a.length} vs ${vec_b.length}`);
1338
- }
1339
- return vec_a.reduce((sum, val, idx) => sum + val * vec_b[idx], 0);
539
+ let sum = 0;
540
+ for (let idx = 0; idx < vec_a.length; idx++)
541
+ sum += vec_a[idx] * vec_b[idx];
542
+ return sum;
1340
543
  };
1341
544
  const norm_nd = (vec) => Math.sqrt(dot_nd(vec, vec));
1342
545
  const normalize_nd = (vec) => {
@@ -1348,8 +551,8 @@ const normalize_nd = (vec) => {
1348
551
  // Compute normal to hyperplane through N points in N-dimensional space
1349
552
  // Uses null space computation via cofactor expansion
1350
553
  function compute_hyperplane_nd(points) {
1351
- const n = points.length;
1352
- if (n < 2)
554
+ const n_points = points.length;
555
+ if (n_points < 2)
1353
556
  return { normal: [], offset: 0 };
1354
557
  // Build (N-1) edge vectors from points[0]
1355
558
  const edges = points.slice(1).map((pt) => subtract_nd(pt, points[0]));
@@ -1377,16 +580,18 @@ const compute_centroid_nd = (points) => {
1377
580
  const dim = points[0].length;
1378
581
  return Array.from({ length: dim }, (_, idx) => points.reduce((sum, pt) => sum + pt[idx], 0) / points.length);
1379
582
  };
583
+ // Maximum sample size for initial simplex selection (avoids O(n²) for large datasets)
584
+ const INITIAL_SIMPLEX_SAMPLE_SIZE = 100;
1380
585
  // Find N+1 points that span N dimensions (initial simplex for quickhull)
1381
586
  function choose_initial_simplex_nd(points) {
1382
- const n = points[0]?.length;
1383
- if (!n || points.length < n + 1)
587
+ const dim = points[0]?.length;
588
+ if (!dim || points.length < dim + 1)
1384
589
  return null;
1385
590
  const chosen = [];
1386
591
  // Greedily pick points that maximize distance from current affine hull
1387
592
  // Start with two points that are farthest apart
1388
593
  let [best_i, best_j, best_dist] = [0, 1, -1];
1389
- const sample_size = Math.min(points.length, 100);
594
+ const sample_size = Math.min(points.length, INITIAL_SIMPLEX_SAMPLE_SIZE);
1390
595
  const sample_indices = points.length <= sample_size
1391
596
  ? points.map((_, idx) => idx)
1392
597
  : Array.from({ length: sample_size }, (_, idx) => Math.floor((idx * points.length) / sample_size));
@@ -1407,7 +612,7 @@ function choose_initial_simplex_nd(points) {
1407
612
  chosen.push(best_i, best_j);
1408
613
  const chosen_set = new Set(chosen);
1409
614
  // Add remaining points to span higher dimensions
1410
- while (chosen.length < n + 1) {
615
+ while (chosen.length < dim + 1) {
1411
616
  let [best_idx, best_distance] = [-1, -1];
1412
617
  // Hoist chosen_points computation outside inner loop for O(n) instead of O(n²)
1413
618
  const chosen_points = chosen.map((idx_c) => points[idx_c]);
@@ -1441,8 +646,8 @@ function distance_to_affine_hull_nd(point, hull_points) {
1441
646
  const ab_len_sq = dot_nd(ab, ab);
1442
647
  if (ab_len_sq < EPS)
1443
648
  return norm_nd(ap);
1444
- const t = dot_nd(ap, ab) / ab_len_sq;
1445
- const proj = pt_a.map((val, idx) => val + t * ab[idx]);
649
+ const proj_frac = dot_nd(ap, ab) / ab_len_sq;
650
+ const proj = pt_a.map((val, idx) => val + proj_frac * ab[idx]);
1446
651
  return norm_nd(subtract_nd(point, proj));
1447
652
  }
1448
653
  // For 3+ points, use orthogonal projection
@@ -1453,8 +658,8 @@ function distance_to_affine_hull_nd(point, hull_points) {
1453
658
  // Solve least squares using Gram matrix G[i][j] = dot(edge_i, edge_j)
1454
659
  const gram = edges.map((edge_i) => edges.map((edge_j) => dot_nd(edge_i, edge_j)));
1455
660
  const rhs = edges.map((edge) => dot_nd(edge, vp));
1456
- // Solve Gram * coeffs = rhs using simple Gaussian elimination
1457
- const coeffs = solve_linear_system(gram, rhs);
661
+ // Solve Gram * coeffs = rhs
662
+ const coeffs = math.solve_linear_system(gram, rhs);
1458
663
  if (!coeffs) {
1459
664
  // Fallback: Gram-Schmidt when Gram matrix is singular (linearly dependent edges)
1460
665
  // Build orthogonal basis and accumulate projection in single pass
@@ -1484,50 +689,6 @@ function distance_to_affine_hull_nd(point, hull_points) {
1484
689
  const proj = origin.map((val, dim) => val + coeffs.reduce((sum, coeff, idx) => sum + coeff * edges[idx][dim], 0));
1485
690
  return norm_nd(subtract_nd(point, proj));
1486
691
  }
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
692
  // Create a simplex face with correct normal orientation (outward from interior)
1532
693
  function make_face_nd(points, vertex_indices, interior_point) {
1533
694
  const face_points = vertex_indices.map((idx) => points[idx]);
@@ -1565,9 +726,9 @@ function build_horizon_nd(faces, visible_indices) {
1565
726
  for (const face_idx of visible_indices) {
1566
727
  const face = faces[face_idx];
1567
728
  const verts = face.vertex_indices;
1568
- const n = verts.length;
1569
- // Each face has n ridges, each ridge omits one vertex
1570
- for (let skip = 0; skip < n; skip++) {
729
+ const n_verts = verts.length;
730
+ // Each face has n_verts ridges, each ridge omits one vertex
731
+ for (let skip = 0; skip < n_verts; skip++) {
1571
732
  const ridge = verts.filter((_, idx) => idx !== skip);
1572
733
  const sorted = [...ridge].sort((a, b) => a - b);
1573
734
  const key = sorted.join(`|`);
@@ -1587,8 +748,14 @@ function build_horizon_nd(faces, visible_indices) {
1587
748
  export function compute_quickhull_nd(points) {
1588
749
  if (points.length === 0)
1589
750
  return [];
1590
- const n = points[0].length;
1591
- if (points.length < n + 1)
751
+ const dim = points[0].length;
752
+ // Validate dimensions once up front; vector ops in the hot loops assume agreement
753
+ for (const pt of points) {
754
+ if (pt.length !== dim) {
755
+ throw new Error(`Vector dimension mismatch: ${pt.length} vs ${dim}`);
756
+ }
757
+ }
758
+ if (points.length < dim + 1)
1592
759
  return [];
1593
760
  // Find initial n-simplex
1594
761
  const initial = choose_initial_simplex_nd(points);
@@ -1596,9 +763,9 @@ export function compute_quickhull_nd(points) {
1596
763
  return [];
1597
764
  // Interior point for normal orientation
1598
765
  const interior = compute_centroid_nd(initial.map((idx) => points[idx]));
1599
- // Create initial n+1 facets (each omits one vertex from the simplex)
766
+ // Create initial dim+1 facets (each omits one vertex from the simplex)
1600
767
  const faces = [];
1601
- for (let skip = 0; skip <= n; skip++) {
768
+ for (let skip = 0; skip <= dim; skip++) {
1602
769
  const verts = initial.filter((_, idx) => idx !== skip);
1603
770
  faces.push(make_face_nd(points, verts, interior));
1604
771
  }
@@ -1679,25 +846,24 @@ export function compute_lower_hull_nd(faces) {
1679
846
  // Last dimension is energy; negative normal means "downward"
1680
847
  return faces.filter((face) => (face.plane.normal.at(-1) ?? 0) < -EPS);
1681
848
  }
1682
- const build_simplex_models_nd = (faces, points) => faces.map((face) => {
1683
- const vertices = face.vertex_indices.map((idx) => points[idx]);
1684
- const n = vertices[0].length;
849
+ const build_simplex_models_nd = (simplices) => simplices.map((vertices) => {
850
+ const dim = vertices[0].length;
1685
851
  // Spatial coords are all except last (energy)
1686
- const vertices_spatial = vertices.map((pt) => pt.slice(0, n - 1));
852
+ const vertices_spatial = vertices.map((pt) => pt.slice(0, dim - 1));
1687
853
  // Compute bounding box in spatial dimensions
1688
- const spatial_dim = n - 1;
1689
- const bbox_min = Array.from({ length: spatial_dim }, (_, idx) => Math.min(...vertices_spatial.map((pt) => pt[idx])));
1690
- const bbox_max = Array.from({ length: spatial_dim }, (_, idx) => Math.max(...vertices_spatial.map((pt) => pt[idx])));
854
+ const spatial_dim = dim - 1;
855
+ const bbox_min = Array.from({ length: spatial_dim }, (_, idx) => array_min(vertices_spatial.map((pt) => pt[idx])));
856
+ const bbox_max = Array.from({ length: spatial_dim }, (_, idx) => array_max(vertices_spatial.map((pt) => pt[idx])));
1691
857
  return { vertices, vertices_spatial, bbox_min, bbox_max };
1692
858
  });
1693
859
  // Check if point is inside simplex and return barycentric coordinates
1694
860
  // Uses linear system solution: point = sum(bary[i] * vertex[i]) with sum(bary) = 1
1695
861
  function point_in_simplex_nd(point, simplex_vertices) {
1696
- const n = simplex_vertices.length; // Number of vertices = spatial_dim + 1
1697
- if (n === 0)
862
+ const n_verts = simplex_vertices.length; // Number of vertices = spatial_dim + 1
863
+ if (n_verts === 0)
1698
864
  return null;
1699
865
  const dim = point.length;
1700
- if (dim !== n - 1)
866
+ if (dim !== n_verts - 1)
1701
867
  return null; // Spatial dim should be one less than vertex count
1702
868
  // Build linear system: [v1-v0, v2-v0, ..., vn-v0] * [b1, b2, ..., bn] = point - v0
1703
869
  // Then b0 = 1 - sum(b1..bn)
@@ -1710,7 +876,7 @@ function point_in_simplex_nd(point, simplex_vertices) {
1710
876
  for (let row = 0; row < dim; row++) {
1711
877
  matrix.push(edges.map((edge) => edge[row]));
1712
878
  }
1713
- const coeffs = solve_linear_system(matrix, rhs);
879
+ const coeffs = math.solve_linear_system(matrix, rhs);
1714
880
  if (!coeffs)
1715
881
  return null;
1716
882
  // Compute b0 = 1 - sum(coeffs), ensuring sum(bary) = 1 by construction
@@ -1721,11 +887,17 @@ function point_in_simplex_nd(point, simplex_vertices) {
1721
887
  }
1722
888
  // Compute energy above hull for N-dimensional points
1723
889
  export function compute_e_above_hull_nd(query_points, hull_facets, all_points) {
1724
- const models = build_simplex_models_nd(hull_facets, all_points);
890
+ return e_above_hull_from_simplices(query_points, hull_facets.map((facet) => facet.vertex_indices.map((idx) => all_points[idx])));
891
+ }
892
+ // Shared e-above-hull core: query points against hull simplices given as vertex
893
+ // coordinate lists (last coordinate = energy). Returns raw (unclamped) distances;
894
+ // NaN for queries whose spatial projection lies outside all simplices.
895
+ function e_above_hull_from_simplices(query_points, simplices) {
896
+ const models = build_simplex_models_nd(simplices);
1725
897
  return query_points.map((query) => {
1726
- const n = query.length;
1727
- const spatial = query.slice(0, n - 1); // All but last coord
1728
- const energy = query[n - 1];
898
+ const dim = query.length;
899
+ const spatial = query.slice(0, dim - 1); // All but last coord
900
+ const energy = query[dim - 1];
1729
901
  let hull_energy = null;
1730
902
  for (const model of models) {
1731
903
  // Fast bounding box rejection
@@ -1744,7 +916,7 @@ export function compute_e_above_hull_nd(query_points, hull_facets, all_points) {
1744
916
  if (!bary)
1745
917
  continue;
1746
918
  // Interpolate energy using barycentric coords
1747
- const e_hull = bary.reduce((sum, coeff, idx) => sum + coeff * model.vertices[idx][n - 1], 0);
919
+ const e_hull = bary.reduce((sum, coeff, idx) => sum + coeff * model.vertices[idx][dim - 1], 0);
1748
920
  hull_energy = hull_energy === null ? e_hull : Math.min(hull_energy, e_hull);
1749
921
  }
1750
922
  // If no facet contains this point's spatial projection, it's outside the valid