matterviz 0.3.5 → 0.3.7

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 (229) hide show
  1. package/dist/MillerIndexInput.svelte +5 -5
  2. package/dist/api/optimade.js +3 -3
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneExportPane.svelte +1 -3
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte +1 -1
  7. package/dist/brillouin/BrillouinZoneScene.svelte +5 -5
  8. package/dist/brillouin/compute.js +21 -21
  9. package/dist/brillouin/index.d.ts +1 -1
  10. package/dist/brillouin/index.js +0 -1
  11. package/dist/brillouin/types.d.ts +8 -13
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte +3 -3
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +3 -4
  14. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +33 -34
  15. package/dist/chempot-diagram/compute.js +1 -7
  16. package/dist/chempot-diagram/temperature.d.ts +1 -1
  17. package/dist/chempot-diagram/temperature.js +1 -3
  18. package/dist/chempot-diagram/types.d.ts +4 -9
  19. package/dist/colors/index.js +5 -5
  20. package/dist/composition/Composition.svelte +2 -1
  21. package/dist/composition/Formula.svelte +7 -4
  22. package/dist/composition/FormulaFilter.svelte +1 -3
  23. package/dist/composition/format.js +4 -4
  24. package/dist/composition/parse.d.ts +2 -1
  25. package/dist/composition/parse.js +61 -46
  26. package/dist/convex-hull/ConvexHull2D.svelte +62 -51
  27. package/dist/convex-hull/ConvexHull3D.svelte +101 -90
  28. package/dist/convex-hull/ConvexHull4D.svelte +70 -58
  29. package/dist/convex-hull/ConvexHullControls.svelte +24 -35
  30. package/dist/convex-hull/ConvexHullInfoPane.svelte +8 -5
  31. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +2 -0
  32. package/dist/convex-hull/ConvexHullStats.svelte +9 -2
  33. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +2 -0
  34. package/dist/convex-hull/GasPressureControls.svelte +7 -7
  35. package/dist/convex-hull/StructurePopup.svelte +65 -30
  36. package/dist/convex-hull/StructurePopup.svelte.d.ts +6 -6
  37. package/dist/convex-hull/TemperatureSlider.svelte +8 -5
  38. package/dist/convex-hull/barycentric-coords.d.ts +2 -2
  39. package/dist/convex-hull/barycentric-coords.js +2 -2
  40. package/dist/convex-hull/gas-thermodynamics.js +2 -4
  41. package/dist/convex-hull/helpers.d.ts +13 -2
  42. package/dist/convex-hull/helpers.js +37 -16
  43. package/dist/convex-hull/index.d.ts +1 -0
  44. package/dist/convex-hull/index.js +1 -0
  45. package/dist/convex-hull/thermodynamics.d.ts +2 -1
  46. package/dist/convex-hull/thermodynamics.js +7 -7
  47. package/dist/convex-hull/types.d.ts +15 -15
  48. package/dist/effects.svelte.d.ts +12 -0
  49. package/dist/effects.svelte.js +37 -0
  50. package/dist/element/BohrAtom.svelte +4 -4
  51. package/dist/element/data.json.gz.d.ts +3 -1
  52. package/dist/element/index.d.ts +1 -1
  53. package/dist/element/index.js +0 -1
  54. package/dist/fermi-surface/FermiSurface.svelte +4 -4
  55. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  56. package/dist/fermi-surface/FermiSurfaceControls.svelte +15 -19
  57. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  58. package/dist/fermi-surface/FermiSurfaceScene.svelte +8 -6
  59. package/dist/fermi-surface/compute.js +2 -2
  60. package/dist/fermi-surface/export.js +13 -26
  61. package/dist/fermi-surface/parse.js +8 -12
  62. package/dist/fermi-surface/types.d.ts +2 -5
  63. package/dist/heatmap-matrix/HeatmapMatrix.svelte +21 -3
  64. package/dist/heatmap-matrix/index.js +6 -6
  65. package/dist/io/decompress.d.ts +2 -1
  66. package/dist/io/decompress.js +1 -1
  67. package/dist/io/export.js +1 -1
  68. package/dist/io/index.d.ts +1 -1
  69. package/dist/io/index.js +0 -1
  70. package/dist/io/url-drop.js +7 -1
  71. package/dist/isosurface/IsosurfaceControls.svelte +11 -25
  72. package/dist/isosurface/slice.js +1 -1
  73. package/dist/isosurface/types.js +12 -12
  74. package/dist/labels.d.ts +1 -1
  75. package/dist/labels.js +14 -11
  76. package/dist/layout/InfoTag.svelte +6 -4
  77. package/dist/layout/PropertyFilter.svelte +4 -2
  78. package/dist/layout/json-tree/JsonTree.svelte +22 -14
  79. package/dist/layout/json-tree/JsonValue.svelte +2 -2
  80. package/dist/layout/json-tree/types.d.ts +3 -2
  81. package/dist/layout/json-tree/types.js +0 -1
  82. package/dist/layout/json-tree/utils.d.ts +4 -4
  83. package/dist/layout/json-tree/utils.js +12 -20
  84. package/dist/marching-cubes.js +13 -15
  85. package/dist/math.d.ts +11 -1
  86. package/dist/math.js +15 -6
  87. package/dist/overlays/DragControlTab.svelte +98 -0
  88. package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
  89. package/dist/overlays/DraggablePane.svelte +7 -84
  90. package/dist/overlays/index.d.ts +1 -0
  91. package/dist/overlays/index.js +1 -0
  92. package/dist/periodic-table/PeriodicTable.svelte +11 -11
  93. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +4 -2
  94. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  95. package/dist/phase-diagram/PhaseDiagramControls.svelte +4 -9
  96. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  97. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +2 -10
  98. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +2 -3
  99. package/dist/phase-diagram/TdbInfoPanel.svelte +3 -3
  100. package/dist/phase-diagram/build-diagram.js +11 -18
  101. package/dist/phase-diagram/diagram-input.d.ts +5 -9
  102. package/dist/phase-diagram/index.d.ts +2 -2
  103. package/dist/phase-diagram/index.js +0 -2
  104. package/dist/phase-diagram/parse.d.ts +2 -2
  105. package/dist/phase-diagram/parse.js +6 -10
  106. package/dist/phase-diagram/svg-to-diagram.js +15 -15
  107. package/dist/phase-diagram/types.d.ts +5 -11
  108. package/dist/phase-diagram/utils.d.ts +2 -2
  109. package/dist/phase-diagram/utils.js +9 -11
  110. package/dist/plot/BarPlot.svelte +162 -314
  111. package/dist/plot/BarPlot.svelte.d.ts +5 -4
  112. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  113. package/dist/plot/BinnedScatterPlot.svelte +1114 -0
  114. package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
  115. package/dist/plot/ColorBar.svelte +19 -17
  116. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  117. package/dist/plot/FillArea.svelte +2 -4
  118. package/dist/plot/FillArea.svelte.d.ts +1 -1
  119. package/dist/plot/Histogram.svelte +167 -281
  120. package/dist/plot/Histogram.svelte.d.ts +1 -1
  121. package/dist/plot/HistogramControls.svelte.d.ts +1 -1
  122. package/dist/plot/InteractiveAxisLabel.svelte +5 -3
  123. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  124. package/dist/plot/PlotAxis.svelte +169 -0
  125. package/dist/plot/PlotAxis.svelte.d.ts +24 -0
  126. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  127. package/dist/plot/ReferenceLine3D.svelte +53 -51
  128. package/dist/plot/ReferencePlane.svelte +39 -42
  129. package/dist/plot/ScatterPlot.svelte +300 -367
  130. package/dist/plot/ScatterPlot.svelte.d.ts +8 -5
  131. package/dist/plot/ScatterPlot3D.svelte +33 -6
  132. package/dist/plot/ScatterPlot3D.svelte.d.ts +3 -2
  133. package/dist/plot/ScatterPlot3DControls.svelte +9 -9
  134. package/dist/plot/ScatterPlotControls.svelte +3 -4
  135. package/dist/plot/ScatterPoint.svelte +18 -27
  136. package/dist/plot/ScatterPoint.svelte.d.ts +4 -3
  137. package/dist/plot/Surface3D.svelte +4 -7
  138. package/dist/plot/ZeroLines.svelte +2 -1
  139. package/dist/plot/ZeroLines.svelte.d.ts +2 -1
  140. package/dist/plot/ZoomRect.svelte +2 -2
  141. package/dist/plot/ZoomRect.svelte.d.ts +3 -3
  142. package/dist/plot/adaptive-density.d.ts +69 -0
  143. package/dist/plot/adaptive-density.js +191 -0
  144. package/dist/plot/auto-place.d.ts +43 -0
  145. package/dist/plot/auto-place.js +122 -0
  146. package/dist/plot/axis-utils.js +3 -5
  147. package/dist/plot/binned-scatter-types.d.ts +59 -0
  148. package/dist/plot/binned-scatter-types.js +1 -0
  149. package/dist/plot/data-cleaning.js +1 -1
  150. package/dist/plot/data-transform.js +1 -1
  151. package/dist/plot/fill-utils.d.ts +4 -9
  152. package/dist/plot/fill-utils.js +29 -44
  153. package/dist/plot/index.d.ts +4 -0
  154. package/dist/plot/index.js +2 -0
  155. package/dist/plot/interactions.d.ts +4 -4
  156. package/dist/plot/interactions.js +4 -3
  157. package/dist/plot/layout.d.ts +20 -2
  158. package/dist/plot/layout.js +59 -16
  159. package/dist/plot/reference-line.d.ts +1 -1
  160. package/dist/plot/reference-line.js +9 -11
  161. package/dist/plot/scales.d.ts +1 -1
  162. package/dist/plot/scales.js +20 -23
  163. package/dist/plot/types.d.ts +30 -58
  164. package/dist/plot/types.js +2 -6
  165. package/dist/plot/utils/label-placement.d.ts +24 -3
  166. package/dist/plot/utils/label-placement.js +82 -12
  167. package/dist/plot/utils/series-visibility.d.ts +8 -2
  168. package/dist/plot/utils/series-visibility.js +23 -5
  169. package/dist/rdf/RdfPlot.svelte +5 -5
  170. package/dist/rdf/calc-rdf.js +3 -3
  171. package/dist/sanitize.d.ts +2 -0
  172. package/dist/sanitize.js +2 -0
  173. package/dist/spectral/Bands.svelte +1 -1
  174. package/dist/spectral/BandsAndDos.svelte +22 -16
  175. package/dist/spectral/BrillouinBandsDos.svelte +20 -16
  176. package/dist/spectral/Dos.svelte +1 -1
  177. package/dist/spectral/helpers.d.ts +4 -2
  178. package/dist/spectral/helpers.js +44 -35
  179. package/dist/spectral/index.d.ts +1 -1
  180. package/dist/spectral/index.js +0 -1
  181. package/dist/structure/AtomLegend.svelte +23 -6
  182. package/dist/structure/AtomLegend.svelte.d.ts +1 -0
  183. package/dist/structure/CanvasTooltip.svelte +9 -9
  184. package/dist/structure/CanvasTooltip.svelte.d.ts +1 -1
  185. package/dist/structure/CellSelect.svelte +14 -16
  186. package/dist/structure/Structure.svelte +317 -68
  187. package/dist/structure/Structure.svelte.d.ts +4 -2
  188. package/dist/structure/StructureControls.svelte +20 -45
  189. package/dist/structure/StructureExportPane.svelte +2 -1
  190. package/dist/structure/StructureInfoPane.svelte +10 -8
  191. package/dist/structure/StructureScene.svelte +527 -177
  192. package/dist/structure/StructureScene.svelte.d.ts +5 -2
  193. package/dist/structure/atom-properties.js +4 -4
  194. package/dist/structure/bond-order-perception.js +115 -98
  195. package/dist/structure/bonding.d.ts +27 -1
  196. package/dist/structure/bonding.js +187 -16
  197. package/dist/structure/export.js +1 -1
  198. package/dist/structure/index.d.ts +3 -2
  199. package/dist/structure/index.js +0 -2
  200. package/dist/structure/parse.js +88 -59
  201. package/dist/symmetry/WyckoffTable.svelte +7 -0
  202. package/dist/symmetry/index.js +13 -14
  203. package/dist/table/HeatmapTable.svelte +45 -66
  204. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  205. package/dist/table/ToggleMenu.svelte +19 -10
  206. package/dist/theme/themes.mjs +12 -0
  207. package/dist/tooltip/index.d.ts +1 -1
  208. package/dist/tooltip/index.js +0 -1
  209. package/dist/trajectory/Trajectory.svelte +43 -15
  210. package/dist/trajectory/TrajectoryInfoPane.svelte +2 -2
  211. package/dist/trajectory/extract.js +1 -1
  212. package/dist/trajectory/frame-reader.js +4 -4
  213. package/dist/trajectory/helpers.d.ts +5 -4
  214. package/dist/trajectory/helpers.js +9 -17
  215. package/dist/trajectory/index.d.ts +2 -2
  216. package/dist/trajectory/index.js +2 -2
  217. package/dist/trajectory/parse/ase.js +4 -4
  218. package/dist/trajectory/parse/hdf5.js +1 -1
  219. package/dist/trajectory/parse/index.js +2 -3
  220. package/dist/trajectory/parse/lammps.js +1 -1
  221. package/dist/trajectory/parse/vasp.js +1 -1
  222. package/dist/trajectory/plotting.d.ts +1 -1
  223. package/dist/trajectory/plotting.js +38 -38
  224. package/dist/trajectory/types.d.ts +1 -1
  225. package/dist/utils.d.ts +1 -0
  226. package/dist/utils.js +9 -0
  227. package/dist/xrd/calc-xrd.js +3 -4
  228. package/dist/xrd/parse.js +1 -1
  229. package/package.json +42 -22
@@ -2,17 +2,31 @@
2
2
  import { element_data } from '../element';
3
3
  import * as math from '../math';
4
4
  const element_lookup = new Map(element_data.map((el) => [el.symbol, el]));
5
- const covalent_radii = new Map(element_data
6
- .filter((el) => el.covalent_radius !== null)
7
- .map((el) => [el.symbol, el.covalent_radius]));
5
+ const covalent_radii = new Map(element_data.flatMap((el) => el.covalent_radius === null ? [] : [[el.symbol, el.covalent_radius]]));
8
6
  const is_zero_cell_shift = (cell_shift) => cell_shift === undefined || cell_shift.every((val) => val === 0);
9
7
  const format_cell_shift = (cell_shift) => {
10
8
  if (cell_shift === undefined || is_zero_cell_shift(cell_shift))
11
9
  return ``;
12
10
  return `@${cell_shift.join(`,`)}`;
13
11
  };
14
- const negate_cell_shift = (cell_shift) => cell_shift.map((val) => (val === 0 ? 0 : -val));
12
+ const negate_cell_shift = (cell_shift) => [
13
+ cell_shift[0] === 0 ? 0 : -cell_shift[0],
14
+ cell_shift[1] === 0 ? 0 : -cell_shift[1],
15
+ cell_shift[2] === 0 ? 0 : -cell_shift[2],
16
+ ];
17
+ const canonical_self_bond_shift = (cell_shift) => {
18
+ const first_non_zero = cell_shift.find((val) => val !== 0);
19
+ return first_non_zero !== undefined && first_non_zero < 0
20
+ ? negate_cell_shift(cell_shift)
21
+ : cell_shift;
22
+ };
15
23
  const normalize_bond_endpoints = (site_idx_1, site_idx_2, cell_shift) => {
24
+ if (site_idx_1 === site_idx_2) {
25
+ const ordered = { site_idx_1, site_idx_2 };
26
+ if (cell_shift === undefined || is_zero_cell_shift(cell_shift))
27
+ return ordered;
28
+ return { ...ordered, cell_shift: canonical_self_bond_shift(cell_shift) };
29
+ }
16
30
  const ordered = site_idx_1 < site_idx_2
17
31
  ? { site_idx_1, site_idx_2 }
18
32
  : { site_idx_1: site_idx_2, site_idx_2: site_idx_1 };
@@ -31,17 +45,174 @@ export const get_bond_key = (idx_1, idx_2, cell_shift) => {
31
45
  const normalized = normalize_bond_endpoints(idx_1, idx_2, cell_shift);
32
46
  return `${normalized.site_idx_1}-${normalized.site_idx_2}${format_cell_shift(normalized.cell_shift)}`;
33
47
  };
48
+ export const BOND_ORDER_OPTIONS = [
49
+ { order: 1, label: `Single` },
50
+ { order: 1.5, label: `1.5` },
51
+ { order: 2, label: `Double` },
52
+ { order: 3, label: `Triple` },
53
+ { order: `aromatic`, label: `Aromatic` },
54
+ ];
55
+ const site_image_shift = (sites, site_idx) => {
56
+ const site = sites?.[site_idx];
57
+ const orig_site_idx = site?.properties?.orig_site_idx;
58
+ if (typeof orig_site_idx !== `number`)
59
+ return [0, 0, 0];
60
+ const orig_site = sites?.[orig_site_idx];
61
+ if (!site?.abc || !orig_site?.abc)
62
+ return [0, 0, 0];
63
+ return [
64
+ Math.round(site.abc[0] - orig_site.abc[0]),
65
+ Math.round(site.abc[1] - orig_site.abc[1]),
66
+ Math.round(site.abc[2] - orig_site.abc[2]),
67
+ ];
68
+ };
69
+ const original_site_idx = (sites, site_idx) => {
70
+ const orig_site_idx = sites?.[site_idx]?.properties?.orig_site_idx;
71
+ return typeof orig_site_idx === `number` ? orig_site_idx : site_idx;
72
+ };
73
+ export const canonicalize_bond_target = (bond, sites) => {
74
+ const shift_1 = site_image_shift(sites, bond.site_idx_1);
75
+ const shift_2 = site_image_shift(sites, bond.site_idx_2);
76
+ const base_shift = bond.cell_shift ?? [0, 0, 0];
77
+ const cell_shift = [
78
+ base_shift[0] + shift_2[0] - shift_1[0],
79
+ base_shift[1] + shift_2[1] - shift_1[1],
80
+ base_shift[2] + shift_2[2] - shift_1[2],
81
+ ];
82
+ return normalize_bond_endpoints(original_site_idx(sites, bond.site_idx_1), original_site_idx(sites, bond.site_idx_2), cell_shift);
83
+ };
84
+ const bond_key_for = (bond) => get_bond_key(bond.site_idx_1, bond.site_idx_2, bond.cell_shift);
85
+ const matches_bond_key = (bond, key) => bond_key_for(bond) === key;
86
+ const replace_bond = (bonds, next_bond) => {
87
+ const key = bond_key_for(next_bond);
88
+ return [...bonds.filter((bond) => !matches_bond_key(bond, key)), next_bond];
89
+ };
90
+ const remove_bond_key = (bonds, key) => bonds.filter((bond) => !matches_bond_key(bond, key));
91
+ const includes_bond_key = (bonds, key) => bonds.some((bond) => matches_bond_key(bond, key));
92
+ const get_bond_order = (bond) => bond?.bond_order ?? bond?.order ?? 1;
93
+ const find_bond_by_key = (bonds, key) => bonds.find((bond) => matches_bond_key(bond, key));
94
+ const make_bond_record = (bond, order) => normalize_structure_bond(bond.site_idx_1, bond.site_idx_2, order, bond.cell_shift);
95
+ export function has_visible_bond(edit_state, bond, calculated_bonds) {
96
+ const key = bond_key_for(bond);
97
+ if (includes_bond_key(edit_state.removed_bonds, key)) {
98
+ return false;
99
+ }
100
+ if (includes_bond_key(edit_state.added_bonds, key))
101
+ return true;
102
+ return includes_bond_key(calculated_bonds, key);
103
+ }
104
+ export function add_or_restore_bond(edit_state, bond, calculated_bonds, order) {
105
+ const record = make_bond_record(bond, order);
106
+ const key = bond_key_for(record);
107
+ const removed_bond = find_bond_by_key(edit_state.removed_bonds, key);
108
+ if (removed_bond) {
109
+ return {
110
+ action: `restored`,
111
+ changed: true,
112
+ state: {
113
+ ...edit_state,
114
+ added_bonds: remove_bond_key(edit_state.added_bonds, key),
115
+ removed_bonds: remove_bond_key(edit_state.removed_bonds, key),
116
+ bond_order_overrides: removed_bond.order === order
117
+ ? remove_bond_key(edit_state.bond_order_overrides, key)
118
+ : replace_bond(edit_state.bond_order_overrides, record),
119
+ },
120
+ };
121
+ }
122
+ if (has_visible_bond(edit_state, record, calculated_bonds)) {
123
+ return { action: `already-visible`, changed: false, state: edit_state };
124
+ }
125
+ return {
126
+ action: `added`,
127
+ changed: true,
128
+ state: {
129
+ ...edit_state,
130
+ added_bonds: replace_bond(edit_state.added_bonds, record),
131
+ bond_order_overrides: remove_bond_key(edit_state.bond_order_overrides, key),
132
+ },
133
+ };
134
+ }
135
+ export function delete_bond(edit_state, bond, calculated_bonds) {
136
+ const record = make_bond_record(bond, 1);
137
+ const key = bond_key_for(record);
138
+ if (includes_bond_key(edit_state.added_bonds, key)) {
139
+ return {
140
+ action: `deleted-added`,
141
+ changed: true,
142
+ state: {
143
+ ...edit_state,
144
+ added_bonds: remove_bond_key(edit_state.added_bonds, key),
145
+ bond_order_overrides: remove_bond_key(edit_state.bond_order_overrides, key),
146
+ },
147
+ };
148
+ }
149
+ const calculated = find_bond_by_key(calculated_bonds, key);
150
+ if (!calculated || includes_bond_key(edit_state.removed_bonds, key)) {
151
+ return { action: `not-visible`, changed: false, state: edit_state };
152
+ }
153
+ return {
154
+ action: `deleted-calculated`,
155
+ changed: true,
156
+ state: {
157
+ ...edit_state,
158
+ removed_bonds: replace_bond(edit_state.removed_bonds, {
159
+ ...record,
160
+ order: get_bond_order(calculated),
161
+ }),
162
+ bond_order_overrides: remove_bond_key(edit_state.bond_order_overrides, key),
163
+ },
164
+ };
165
+ }
166
+ export function set_bond_order(edit_state, bond, calculated_bonds, order) {
167
+ const record = make_bond_record(bond, order);
168
+ const key = bond_key_for(record);
169
+ const calculated = find_bond_by_key(calculated_bonds, key);
170
+ if (calculated) {
171
+ const visible_order = get_bond_order(calculated);
172
+ const has_existing_edit = includes_bond_key(edit_state.added_bonds, key) ||
173
+ includes_bond_key(edit_state.removed_bonds, key) ||
174
+ includes_bond_key(edit_state.bond_order_overrides, key);
175
+ const next_overrides = order === visible_order
176
+ ? remove_bond_key(edit_state.bond_order_overrides, key)
177
+ : replace_bond(edit_state.bond_order_overrides, record);
178
+ const next_state = {
179
+ added_bonds: remove_bond_key(edit_state.added_bonds, key),
180
+ removed_bonds: remove_bond_key(edit_state.removed_bonds, key),
181
+ bond_order_overrides: next_overrides,
182
+ };
183
+ return {
184
+ action: `ordered-calculated`,
185
+ changed: has_existing_edit || order !== visible_order,
186
+ state: next_state,
187
+ };
188
+ }
189
+ return {
190
+ action: `ordered-added`,
191
+ changed: true,
192
+ state: {
193
+ ...edit_state,
194
+ added_bonds: replace_bond(edit_state.added_bonds, record),
195
+ bond_order_overrides: remove_bond_key(edit_state.bond_order_overrides, key),
196
+ },
197
+ };
198
+ }
34
199
  export const merge_bond_edits = (base_bonds, added, removed, overrides) => {
35
200
  const key_for = (bond) => get_bond_key(bond.site_idx_1, bond.site_idx_2, bond.cell_shift);
36
- const merged = new Map(base_bonds.map((bond) => [key_for(bond), bond]));
37
- for (const bond of removed)
38
- merged.delete(key_for(bond));
201
+ const normalize_record = (bond) => normalize_structure_bond(bond.site_idx_1, bond.site_idx_2, bond.order, bond.cell_shift);
202
+ const removed_keys = new Set(removed.map(key_for));
203
+ const merged = new Map(base_bonds
204
+ .filter((bond) => !removed_keys.has(key_for(bond)))
205
+ .map((bond) => [key_for(bond), normalize_record(bond)]));
39
206
  // Apply additions before overrides so user-set bond orders win even if
40
207
  // callers accidentally pass overlapping edit lists.
41
- for (const bond of added)
42
- merged.set(key_for(bond), bond);
43
- for (const bond of overrides)
44
- merged.set(key_for(bond), bond);
208
+ for (const bond of added) {
209
+ if (!removed_keys.has(key_for(bond)))
210
+ merged.set(key_for(bond), normalize_record(bond));
211
+ }
212
+ for (const bond of overrides) {
213
+ if (!removed_keys.has(key_for(bond)))
214
+ merged.set(key_for(bond), normalize_record(bond));
215
+ }
45
216
  return [...merged.values()];
46
217
  };
47
218
  const is_record = (value) => typeof value === `object` && value !== null;
@@ -59,7 +230,7 @@ function normalize_cell_shift(cell_shift) {
59
230
  return null;
60
231
  return cell_shift.some((val) => typeof val !== `number` || !Number.isInteger(val))
61
232
  ? null
62
- : cell_shift;
233
+ : [cell_shift[0], cell_shift[1], cell_shift[2]];
63
234
  }
64
235
  function lattice_translation(structure, cell_shift) {
65
236
  if (cell_shift === undefined || is_zero_cell_shift(cell_shift))
@@ -123,10 +294,6 @@ export function get_explicit_bond_metadata(structure) {
123
294
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: site indices ${site_idx_1}, ${site_idx_2} are out of range for ${structure.sites.length} sites`);
124
295
  continue;
125
296
  }
126
- if (site_idx_1 === site_idx_2) {
127
- console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: endpoints match`);
128
- continue;
129
- }
130
297
  const bond_order = normalize_bond_order(order);
131
298
  if (bond_order === null) {
132
299
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: unsupported order ${String(order)}`);
@@ -137,6 +304,10 @@ export function get_explicit_bond_metadata(structure) {
137
304
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: cell_shift must be three integers`);
138
305
  continue;
139
306
  }
307
+ if (site_idx_1 === site_idx_2 && is_zero_cell_shift(cell_shift)) {
308
+ console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: endpoints match`);
309
+ continue;
310
+ }
140
311
  if (cell_shift !== undefined &&
141
312
  !is_zero_cell_shift(cell_shift) &&
142
313
  !(`lattice` in structure)) {
@@ -164,7 +164,7 @@ function convert_instanced_meshes_to_regular(scene) {
164
164
  // Check if this is a shader material (bonds)
165
165
  const mat = instanced_mesh.material;
166
166
  const is_shader = Array.isArray(mat)
167
- ? mat.some((m) => m instanceof ShaderMaterial)
167
+ ? mat.some((material) => material instanceof ShaderMaterial)
168
168
  : mat instanceof ShaderMaterial;
169
169
  if (is_shader) {
170
170
  // Extract bond colors for each instance from geometry attributes
@@ -3,9 +3,9 @@ import type { ElementSymbol } from '../element';
3
3
  import type { Vec3 } from '../math';
4
4
  import * as math from '../math';
5
5
  import type { ComponentProps } from 'svelte';
6
- import LatticeComponent from './Lattice.svelte';
6
+ import type LatticeComponent from './Lattice.svelte';
7
7
  import type { Pbc } from './pbc';
8
- import StructureSceneComponent from './StructureScene.svelte';
8
+ import type StructureSceneComponent from './StructureScene.svelte';
9
9
  export { default as Arrow } from './Arrow.svelte';
10
10
  export * from './atom-properties';
11
11
  export { default as AtomLegend } from './AtomLegend.svelte';
@@ -22,6 +22,7 @@ export { default as StructureInfoPane } from './StructureInfoPane.svelte';
22
22
  export { default as StructureScene } from './StructureScene.svelte';
23
23
  export * from './supercell';
24
24
  export type MeasureMode = `distance` | `angle` | `edit-bonds` | `edit-atoms`;
25
+ export type BondEditMode = `add` | `delete`;
25
26
  export type Species = {
26
27
  element: ElementSymbol;
27
28
  occu: number;
@@ -1,8 +1,6 @@
1
1
  import { ATOMIC_WEIGHTS } from '../composition/parse';
2
2
  import { element_data } from '../element';
3
3
  import * as math from '../math';
4
- import LatticeComponent from './Lattice.svelte';
5
- import StructureSceneComponent from './StructureScene.svelte';
6
4
  export { default as Arrow } from './Arrow.svelte';
7
5
  export * from './atom-properties';
8
6
  export { default as AtomLegend } from './AtomLegend.svelte';
@@ -4,6 +4,26 @@ import * as math from '../math';
4
4
  import { wrap_to_unit_cell } from './pbc';
5
5
  import { normalize_scientific_notation } from '../utils';
6
6
  import { load as yaml_load } from 'js-yaml';
7
+ const cif_coords_key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
8
+ const cif_site_key = (element, abc, label) => `${element}|${label}|${cif_coords_key(abc)}`;
9
+ const clone_structure_properties = (properties) => structuredClone(properties);
10
+ const FALLBACK_ELEMENTS = [`H`, `He`, `Li`, `Be`, `B`, `C`, `N`, `O`, `F`, `Ne`];
11
+ const is_known_element_symbol = (symbol) => ELEM_SYMBOLS.includes(symbol);
12
+ const vec3_from_values = (values, context) => {
13
+ if (values?.length !== 3) {
14
+ throw new Error(`Invalid ${context}: expected 3 coordinates, got ${values?.length ?? 0}`);
15
+ }
16
+ const coords = math.finite_vec3_from_values(values);
17
+ if (coords)
18
+ return coords;
19
+ for (let idx = 0; idx < 3; idx++) {
20
+ const value = values[idx];
21
+ if (typeof value !== `number` || !Number.isFinite(value)) {
22
+ throw new TypeError(`Invalid ${context}: coordinate ${idx} must be finite, got ${String(value)}`);
23
+ }
24
+ }
25
+ throw new Error(`Invalid ${context}: expected 3 finite coordinates`);
26
+ };
7
27
  // Parse a coordinate value that might be in various scientific notation formats
8
28
  function parse_coordinate(str) {
9
29
  const normalized = normalize_scientific_notation(str.trim());
@@ -34,12 +54,10 @@ function parse_coordinate_line(line) {
34
54
  function validate_element_symbol(symbol, index) {
35
55
  // Clean symbol (remove suffixes like _pv, /hash)
36
56
  const clean_symbol = symbol.split(/[_/]/)[0];
37
- if (ELEM_SYMBOLS && ELEM_SYMBOLS.includes(clean_symbol)) {
57
+ if (is_known_element_symbol(clean_symbol))
38
58
  return clean_symbol;
39
- }
40
59
  // Fallback to default elements by atomic number
41
- const fallback_elements = [`H`, `He`, `Li`, `Be`, `B`, `C`, `N`, `O`, `F`, `Ne`];
42
- const fallback = fallback_elements[index % fallback_elements.length];
60
+ const fallback = FALLBACK_ELEMENTS[index % FALLBACK_ELEMENTS.length] ?? `H`;
43
61
  console.warn(`Invalid element symbol '${symbol}', using fallback '${fallback}'`);
44
62
  return fallback;
45
63
  }
@@ -73,10 +91,7 @@ export function parse_poscar(content) {
73
91
  // Parse lattice vectors (lines 3-5)
74
92
  const parse_vector = (line, line_num) => {
75
93
  const coords = line.trim().split(/\s+/).map(parse_coordinate);
76
- if (coords.length !== 3) {
77
- throw new Error(`Invalid lattice vector on line ${line_num}: expected 3 coordinates, got ${coords.length}`);
78
- }
79
- return coords;
94
+ return vec3_from_values(coords, `lattice vector on line ${line_num}`);
80
95
  };
81
96
  const lattice_vecs = [
82
97
  parse_vector(lines[2], 3),
@@ -168,7 +183,11 @@ export function parse_poscar(content) {
168
183
  return null;
169
184
  }
170
185
  // Parse atomic positions
171
- const poscar_axis_lengths = scaled_lattice.map((lattice_vec) => Math.hypot(...lattice_vec));
186
+ const poscar_axis_lengths = [
187
+ Math.hypot(...scaled_lattice[0]),
188
+ Math.hypot(...scaled_lattice[1]),
189
+ Math.hypot(...scaled_lattice[2]),
190
+ ];
172
191
  const poscar_frac_to_cart = math.create_frac_to_cart(scaled_lattice);
173
192
  const poscar_cart_to_frac = try_create_cart_to_frac(scaled_lattice);
174
193
  if (!is_direct && !poscar_cart_to_frac) {
@@ -185,7 +204,7 @@ export function parse_poscar(content) {
185
204
  console.error(`Not enough coordinate lines in POSCAR`);
186
205
  return null;
187
206
  }
188
- const coords = parse_coordinate_line(lines[coord_line_idx]);
207
+ const coords = vec3_from_values(parse_coordinate_line(lines[coord_line_idx]), `POSCAR atom coordinates on line ${coord_line_idx + 1}`);
189
208
  // Parse selective dynamics if present
190
209
  let selective_dynamics;
191
210
  if (has_selective_dynamics) {
@@ -196,15 +215,14 @@ export function parse_poscar(content) {
196
215
  }
197
216
  let xyz;
198
217
  let abc;
199
- const [x, y, z] = coords;
200
218
  if (is_direct) {
201
219
  // Store fractional coordinates, wrapping to [0, 1) range
202
- abc = wrap_to_unit_cell([x, y, z]);
220
+ abc = wrap_to_unit_cell(coords);
203
221
  xyz = poscar_frac_to_cart(abc);
204
222
  }
205
223
  else {
206
224
  // Already Cartesian, scale if needed
207
- xyz = math.scale([x, y, z], scale_factor);
225
+ xyz = math.scale(coords, scale_factor);
208
226
  const raw_abc = poscar_cart_to_frac
209
227
  ? poscar_cart_to_frac(xyz)
210
228
  : approximate_cart_to_frac(xyz, poscar_axis_lengths);
@@ -288,9 +306,9 @@ export function parse_xyz(content) {
288
306
  const lattice_values = lattice_match[1].split(/\s+/).map(parse_coordinate);
289
307
  if (lattice_values.length === 9) {
290
308
  const lattice_vectors = [
291
- [lattice_values[0], lattice_values[1], lattice_values[2]],
292
- [lattice_values[3], lattice_values[4], lattice_values[5]],
293
- [lattice_values[6], lattice_values[7], lattice_values[8]],
309
+ vec3_from_values(lattice_values.slice(0, 3), `XYZ lattice vector 1`),
310
+ vec3_from_values(lattice_values.slice(3, 6), `XYZ lattice vector 2`),
311
+ vec3_from_values(lattice_values.slice(6, 9), `XYZ lattice vector 3`),
294
312
  ];
295
313
  const lattice_params = math.calc_lattice_params(lattice_vectors);
296
314
  lattice = { matrix: lattice_vectors, ...lattice_params };
@@ -317,13 +335,7 @@ export function parse_xyz(content) {
317
335
  return null;
318
336
  }
319
337
  const element = validate_element_symbol(parts[0], atom_idx);
320
- const coords = [
321
- parse_coordinate(parts[1]),
322
- parse_coordinate(parts[2]),
323
- parse_coordinate(parts[3]),
324
- ];
325
- // For XYZ files, coordinates are typically in Cartesian
326
- const xyz = [coords[0], coords[1], coords[2]];
338
+ const xyz = vec3_from_values(parts.slice(1, 4).map(parse_coordinate), `XYZ atom position ${atom_idx + 1}`);
327
339
  // Calculate fractional coordinates if lattice is available
328
340
  let abc = [0, 0, 0];
329
341
  if (lattice && xyz_frac_to_cart && xyz_axis_lengths) {
@@ -430,10 +442,9 @@ const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords) => {
430
442
  const wrap = (coords) => wrap_fractional_coords ? wrap_to_unit_cell(coords) : coords;
431
443
  // Use 6 decimal places for deduplication to handle floating point imprecision
432
444
  // from compound symmetry operations like x-y, -x+y which can produce small errors
433
- const key = (coords) => `${coords[0].toFixed(6)},${coords[1].toFixed(6)},${coords[2].toFixed(6)}`;
434
445
  // Always include base atom (optionally wrapped)
435
446
  const base_coords = wrap(atom.coords);
436
- seen.add(key(base_coords));
447
+ seen.add(cif_coords_key(base_coords));
437
448
  equivalent_atoms.push({ ...atom, coords: base_coords });
438
449
  for (const operation of symmetry_ops) {
439
450
  const operation_match = /['"]([^'"]+)['"]/.exec(operation);
@@ -453,7 +464,7 @@ const apply_symmetry_ops = (atom, symmetry_ops, wrap_fractional_coords) => {
453
464
  }
454
465
  // Wrap and deduplicate transformed coordinates
455
466
  const wrapped = wrap(new_coords);
456
- const cache_key = key(wrapped);
467
+ const cache_key = cif_coords_key(wrapped);
457
468
  if (seen.has(cache_key))
458
469
  continue;
459
470
  seen.add(cache_key);
@@ -516,7 +527,7 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
516
527
  if (coord_indices.some((idx) => idx === undefined)) {
517
528
  throw new Error(`Missing coordinate indices`);
518
529
  }
519
- const coords_triplet = coord_indices.map((idx) => {
530
+ const coords_triplet = vec3_from_values(coord_indices.map((idx) => {
520
531
  if (idx === undefined)
521
532
  throw new Error(`Invalid coordinate index`);
522
533
  const coord_str = raw_data[idx];
@@ -526,7 +537,7 @@ const parse_cif_atom_data = (raw_data, indices, coords_type) => {
526
537
  if (isNaN(coord))
527
538
  throw new Error(`Invalid coordinate: ${coord_str}`);
528
539
  return coord;
529
- });
540
+ }), `CIF atom coordinates`);
530
541
  const occu = occupancy >= 0 && raw_data[occupancy]
531
542
  ? parseFloat(raw_data[occupancy].split(`(`)[0]) || 1.0
532
543
  : 1.0;
@@ -750,7 +761,6 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
750
761
  // Global deduplication of final sites (per element + coordinates + label)
751
762
  // Use 6 decimal places to handle floating point imprecision from compound symmetry ops
752
763
  const seen_site_keys = new Set();
753
- const site_key = (element, abc, label) => `${element}|${label}|${abc[0].toFixed(6)},${abc[1].toFixed(6)},${abc[2].toFixed(6)}`;
754
764
  for (const atom of atoms) {
755
765
  const element = validate_element_symbol(atom.element, all_sites.length);
756
766
  // Convert to fractional coordinates if needed
@@ -779,7 +789,7 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
779
789
  equiv_atom.coords[1] + cv[1],
780
790
  equiv_atom.coords[2] + cv[2],
781
791
  ]);
782
- const key = site_key(element, abc, equiv_atom.id);
792
+ const key = cif_site_key(element, abc, equiv_atom.id);
783
793
  if (seen_site_keys.has(key))
784
794
  continue;
785
795
  seen_site_keys.add(key);
@@ -806,12 +816,16 @@ export function parse_cif(content, wrap_fractional_coords = true, strict = true)
806
816
  function convert_phonopy_cell(cell) {
807
817
  const sites = [];
808
818
  // Phonopy stores lattice vectors as rows, use them directly
809
- const lattice_matrix = cell.lattice;
819
+ const lattice_matrix = [
820
+ vec3_from_values(cell.lattice[0], `phonopy lattice vector 1`),
821
+ vec3_from_values(cell.lattice[1], `phonopy lattice vector 2`),
822
+ vec3_from_values(cell.lattice[2], `phonopy lattice vector 3`),
823
+ ];
810
824
  // Process each atomic site
811
825
  const phonopy_frac_to_cart = math.create_frac_to_cart(lattice_matrix);
812
826
  for (const point of cell.points) {
813
827
  const element = validate_element_symbol(point.symbol, sites.length);
814
- const abc = [point.coordinates[0], point.coordinates[1], point.coordinates[2]];
828
+ const abc = vec3_from_values(point.coordinates, `phonopy point coordinates`);
815
829
  const xyz = phonopy_frac_to_cart(abc);
816
830
  const properties = {
817
831
  mass: point.mass,
@@ -825,6 +839,19 @@ function convert_phonopy_cell(cell) {
825
839
  const calculated_lattice_params = math.calc_lattice_params(lattice_matrix);
826
840
  return { sites, lattice: { matrix: lattice_matrix, ...calculated_lattice_params } };
827
841
  }
842
+ const is_phonopy_cell = (value) => {
843
+ if (!value || typeof value !== `object`)
844
+ return false;
845
+ const lattice = `lattice` in value ? value.lattice : undefined;
846
+ const points = `points` in value ? value.points : undefined;
847
+ return Array.isArray(lattice) && Array.isArray(points);
848
+ };
849
+ const get_phonopy_cell = (data, cell_type) => {
850
+ if (!data || typeof data !== `object`)
851
+ return undefined;
852
+ const cell = Reflect.get(data, cell_type);
853
+ return is_phonopy_cell(cell) ? cell : undefined;
854
+ };
828
855
  // Parse phonopy YAML file and return the requested cell type (or preferred single structure)
829
856
  export function parse_phonopy_yaml(content, cell_type) {
830
857
  try {
@@ -857,7 +884,7 @@ export function parse_phonopy_yaml(content, cell_type) {
857
884
  }
858
885
  // If specific cell type requested, parse only that one
859
886
  if (cell_type && cell_type !== `auto`) {
860
- const cell = data[cell_type];
887
+ const cell = get_phonopy_cell(data, cell_type);
861
888
  if (cell)
862
889
  return convert_phonopy_cell(cell);
863
890
  else {
@@ -871,17 +898,13 @@ export function parse_phonopy_yaml(content, cell_type) {
871
898
  // 3. unit_cell
872
899
  // 4. phonon_primitive_cell
873
900
  // 5. primitive_cell
874
- if (data.supercell)
875
- return convert_phonopy_cell(data.supercell);
876
- else if (data.phonon_supercell)
877
- return convert_phonopy_cell(data.phonon_supercell);
878
- else if (data.unit_cell)
879
- return convert_phonopy_cell(data.unit_cell);
880
- else if (data.phonon_primitive_cell) {
881
- return convert_phonopy_cell(data.phonon_primitive_cell);
882
- }
883
- else if (data.primitive_cell)
884
- return convert_phonopy_cell(data.primitive_cell);
901
+ const auto_cell = get_phonopy_cell(data, `supercell`) ??
902
+ get_phonopy_cell(data, `phonon_supercell`) ??
903
+ get_phonopy_cell(data, `unit_cell`) ??
904
+ get_phonopy_cell(data, `phonon_primitive_cell`) ??
905
+ get_phonopy_cell(data, `primitive_cell`);
906
+ if (auto_cell)
907
+ return convert_phonopy_cell(auto_cell);
885
908
  console.error(`No valid cells found in phonopy YAML`);
886
909
  return null;
887
910
  }
@@ -924,17 +947,15 @@ function find_structure_in_json(obj, visited = new WeakSet()) {
924
947
  function is_parsed_structure(obj) {
925
948
  if (!obj || typeof obj !== `object`)
926
949
  return false;
927
- const parsed_obj = obj;
928
- const sites = parsed_obj.sites;
950
+ const sites = `sites` in obj ? obj.sites : undefined;
929
951
  if (!Array.isArray(sites) || sites.length === 0)
930
952
  return false;
931
953
  const first_site = sites[0];
932
954
  if (!first_site || typeof first_site !== `object`)
933
955
  return false;
934
- const first_site_obj = first_site;
935
- const species = first_site_obj.species;
936
- const abc = first_site_obj.abc;
937
- const xyz = first_site_obj.xyz;
956
+ const species = `species` in first_site ? first_site.species : undefined;
957
+ const abc = `abc` in first_site ? first_site.abc : undefined;
958
+ const xyz = `xyz` in first_site ? first_site.xyz : undefined;
938
959
  const has_species = Array.isArray(species) && species.length > 0;
939
960
  const has_coords = Array.isArray(abc) || Array.isArray(xyz);
940
961
  return has_species && has_coords;
@@ -1082,7 +1103,6 @@ export function parse_structure_file(content, filename) {
1082
1103
  }
1083
1104
  // Universal parser that handles JSON and structure files
1084
1105
  export function parse_any_structure(content, filename) {
1085
- const clone_structure_properties = (properties) => structuredClone(properties);
1086
1106
  const finalize_structure = (structure) => ({
1087
1107
  sites: structure.sites,
1088
1108
  charge: 0,
@@ -1145,7 +1165,13 @@ export function parse_optimade_from_raw(raw) {
1145
1165
  const positions = positions_raw;
1146
1166
  const species = species_raw;
1147
1167
  // Optimade stores lattice vectors as rows, so use as is
1148
- const lattice_matrix = attrs.lattice_vectors;
1168
+ const lattice_matrix = attrs.lattice_vectors
1169
+ ? [
1170
+ vec3_from_values(attrs.lattice_vectors[0], `OPTIMADE lattice vector 1`),
1171
+ vec3_from_values(attrs.lattice_vectors[1], `OPTIMADE lattice vector 2`),
1172
+ vec3_from_values(attrs.lattice_vectors[2], `OPTIMADE lattice vector 3`),
1173
+ ]
1174
+ : undefined;
1149
1175
  const optimade_lattice_params = lattice_matrix
1150
1176
  ? math.calc_lattice_params(lattice_matrix)
1151
1177
  : null;
@@ -1168,12 +1194,15 @@ export function parse_optimade_from_raw(raw) {
1168
1194
  for (let idx = 0; idx < positions.length; idx++) {
1169
1195
  const pos = positions[idx];
1170
1196
  const element_symbol = species[idx];
1171
- if (!pos || pos.length < 3) {
1172
- console.warn(`Invalid position data at site ${idx}`);
1197
+ let xyz;
1198
+ try {
1199
+ xyz = vec3_from_values(pos, `OPTIMADE site ${idx} position`);
1200
+ }
1201
+ catch (error) {
1202
+ console.warn(`Invalid position data at site ${idx}: ${error}`);
1173
1203
  continue;
1174
1204
  }
1175
1205
  const element = validate_element_symbol(element_symbol, idx);
1176
- const xyz = [pos[0], pos[1], pos[2]];
1177
1206
  // Calculate fractional coordinates if lattice is available
1178
1207
  const abc = optimade_cart_to_frac ? optimade_cart_to_frac(xyz) : [0, 0, 0];
1179
1208
  const site = {
@@ -1248,9 +1277,9 @@ export function optimade_to_crystal(optimade_structure) {
1248
1277
  }
1249
1278
  try {
1250
1279
  const lattice_matrix = [
1251
- lattice_vectors[0],
1252
- lattice_vectors[1],
1253
- lattice_vectors[2],
1280
+ vec3_from_values(lattice_vectors[0], `OPTIMADE lattice vector 1`),
1281
+ vec3_from_values(lattice_vectors[1], `OPTIMADE lattice vector 2`),
1282
+ vec3_from_values(lattice_vectors[2], `OPTIMADE lattice vector 3`),
1254
1283
  ];
1255
1284
  const lattice_params = math.calc_lattice_params(lattice_matrix);
1256
1285
  // Build species lookup for site properties (mass, concentration, etc.)
@@ -1262,7 +1291,7 @@ export function optimade_to_crystal(optimade_structure) {
1262
1291
  if (!element_symbol)
1263
1292
  throw new Error(`Missing species for site ${idx}`);
1264
1293
  const element = validate_element_symbol(element_symbol, idx);
1265
- const xyz = [pos[0], pos[1], pos[2]];
1294
+ const xyz = vec3_from_values(pos, `OPTIMADE atom position ${idx + 1}`);
1266
1295
  const abc = crystal_cart_to_frac ? crystal_cart_to_frac(xyz) : [0, 0, 0];
1267
1296
  // Extract mass/concentration from species data
1268
1297
  const spec = species_map.get(element_symbol);
@@ -24,6 +24,13 @@
24
24
  `${wyckoff_pos.wyckoff}-${wyckoff_pos.elem}-${
25
25
  wyckoff_pos.site_indices?.join(`,`) ?? `none`
26
26
  }-${row_idx}`
27
+ $effect(() => {
28
+ if (!selected_key || wyckoff_positions?.some((pos, idx) => get_row_key(pos, idx) === selected_key)) {
29
+ return
30
+ }
31
+ selected_key = null
32
+ on_click?.(null)
33
+ })
27
34
  </script>
28
35
 
29
36
  {#if wyckoff_positions && wyckoff_positions.length > 0}