matterviz 0.4.2 → 0.4.3

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