matterviz 0.3.0 → 0.3.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 (286) hide show
  1. package/dist/FilePicker.svelte +37 -20
  2. package/dist/Icon.svelte +2 -2
  3. package/dist/MillerIndexInput.svelte +60 -0
  4. package/dist/MillerIndexInput.svelte.d.ts +7 -0
  5. package/dist/app.css +38 -2
  6. package/dist/brillouin/BrillouinZone.svelte +20 -62
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  8. package/dist/brillouin/BrillouinZoneExportPane.svelte +12 -20
  9. package/dist/brillouin/BrillouinZoneScene.svelte +2 -2
  10. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  11. package/dist/chempot-diagram/ChemPotDiagram.svelte +192 -0
  12. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  13. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +677 -0
  14. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  15. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +2688 -0
  16. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  17. package/dist/chempot-diagram/ChemPotScene3D.svelte +8 -0
  18. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  19. package/dist/chempot-diagram/color.d.ts +10 -0
  20. package/dist/chempot-diagram/color.js +33 -0
  21. package/dist/chempot-diagram/compute.d.ts +38 -0
  22. package/dist/chempot-diagram/compute.js +650 -0
  23. package/dist/chempot-diagram/index.d.ts +5 -0
  24. package/dist/chempot-diagram/index.js +5 -0
  25. package/dist/chempot-diagram/pointer.d.ts +16 -0
  26. package/dist/chempot-diagram/pointer.js +40 -0
  27. package/dist/chempot-diagram/temperature.d.ts +15 -0
  28. package/dist/chempot-diagram/temperature.js +37 -0
  29. package/dist/chempot-diagram/types.d.ts +83 -0
  30. package/dist/chempot-diagram/types.js +27 -0
  31. package/dist/colors/index.d.ts +3 -1
  32. package/dist/colors/index.js +4 -0
  33. package/dist/composition/BarChart.svelte +13 -22
  34. package/dist/composition/BubbleChart.svelte +5 -3
  35. package/dist/composition/FormulaFilter.svelte +770 -90
  36. package/dist/composition/FormulaFilter.svelte.d.ts +37 -1
  37. package/dist/composition/PieChart.svelte +43 -18
  38. package/dist/composition/PieChart.svelte.d.ts +1 -1
  39. package/dist/constants.d.ts +1 -0
  40. package/dist/constants.js +2 -0
  41. package/dist/convex-hull/ConvexHull.svelte +14 -1
  42. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  43. package/dist/convex-hull/ConvexHull2D.svelte +14 -45
  44. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  45. package/dist/convex-hull/ConvexHull3D.svelte +396 -134
  46. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  47. package/dist/convex-hull/ConvexHull4D.svelte +93 -42
  48. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHullControls.svelte +94 -31
  50. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +4 -2
  51. package/dist/convex-hull/ConvexHullStats.svelte +697 -128
  52. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  53. package/dist/convex-hull/ConvexHullTooltip.svelte +1 -0
  54. package/dist/convex-hull/GasPressureControls.svelte +72 -38
  55. package/dist/convex-hull/GasPressureControls.svelte.d.ts +2 -1
  56. package/dist/convex-hull/TemperatureSlider.svelte +46 -19
  57. package/dist/convex-hull/TemperatureSlider.svelte.d.ts +2 -1
  58. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  59. package/dist/convex-hull/demo-temperature.js +36 -0
  60. package/dist/convex-hull/gas-thermodynamics.js +16 -5
  61. package/dist/convex-hull/helpers.d.ts +7 -1
  62. package/dist/convex-hull/helpers.js +45 -15
  63. package/dist/convex-hull/index.d.ts +15 -1
  64. package/dist/convex-hull/index.js +1 -0
  65. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  66. package/dist/convex-hull/thermodynamics.js +106 -17
  67. package/dist/convex-hull/types.d.ts +7 -0
  68. package/dist/convex-hull/types.js +11 -0
  69. package/dist/coordination/CoordinationBarPlot.svelte +29 -46
  70. package/dist/element/BohrAtom.svelte +1 -1
  71. package/dist/element/data.js +2 -14
  72. package/dist/element/data.json.gz +0 -0
  73. package/dist/element/index.d.ts +1 -1
  74. package/dist/element/index.js +1 -0
  75. package/dist/element/types.d.ts +1 -0
  76. package/dist/fermi-surface/FermiSurface.svelte +21 -65
  77. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  78. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  79. package/dist/fermi-surface/FermiSurfaceScene.svelte +1 -1
  80. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  81. package/dist/fermi-surface/compute.js +1 -21
  82. package/dist/fermi-surface/marching-cubes.d.ts +2 -13
  83. package/dist/fermi-surface/marching-cubes.js +2 -519
  84. package/dist/fermi-surface/parse.js +17 -23
  85. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1273 -0
  86. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  87. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +171 -0
  88. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +31 -0
  89. package/dist/heatmap-matrix/index.d.ts +53 -0
  90. package/dist/heatmap-matrix/index.js +100 -0
  91. package/dist/heatmap-matrix/shared.d.ts +2 -0
  92. package/dist/heatmap-matrix/shared.js +4 -0
  93. package/dist/icons.d.ts +119 -0
  94. package/dist/icons.js +119 -0
  95. package/dist/index.d.ts +6 -1
  96. package/dist/index.js +6 -1
  97. package/dist/io/export.js +15 -3
  98. package/dist/io/file-drop.d.ts +7 -0
  99. package/dist/io/file-drop.js +43 -0
  100. package/dist/io/index.d.ts +2 -2
  101. package/dist/io/index.js +2 -112
  102. package/dist/io/types.d.ts +1 -0
  103. package/dist/io/url-drop.d.ts +2 -0
  104. package/dist/io/url-drop.js +118 -0
  105. package/dist/isosurface/Isosurface.svelte +231 -0
  106. package/dist/isosurface/Isosurface.svelte.d.ts +8 -0
  107. package/dist/isosurface/IsosurfaceControls.svelte +273 -0
  108. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +9 -0
  109. package/dist/isosurface/index.d.ts +5 -0
  110. package/dist/isosurface/index.js +6 -0
  111. package/dist/isosurface/parse.d.ts +6 -0
  112. package/dist/isosurface/parse.js +548 -0
  113. package/dist/isosurface/slice.d.ts +11 -0
  114. package/dist/isosurface/slice.js +145 -0
  115. package/dist/isosurface/types.d.ts +55 -0
  116. package/dist/isosurface/types.js +178 -0
  117. package/dist/labels.d.ts +2 -1
  118. package/dist/labels.js +1 -0
  119. package/dist/layout/InfoTag.svelte +62 -62
  120. package/dist/layout/SubpageGrid.svelte +74 -0
  121. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  122. package/dist/layout/index.d.ts +1 -0
  123. package/dist/layout/index.js +1 -0
  124. package/dist/layout/json-tree/JsonNode.svelte +226 -53
  125. package/dist/layout/json-tree/JsonTree.svelte +425 -51
  126. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  127. package/dist/layout/json-tree/JsonValue.svelte +218 -97
  128. package/dist/layout/json-tree/types.d.ts +27 -2
  129. package/dist/layout/json-tree/utils.d.ts +14 -1
  130. package/dist/layout/json-tree/utils.js +254 -0
  131. package/dist/marching-cubes.d.ts +14 -0
  132. package/dist/marching-cubes.js +519 -0
  133. package/dist/math.d.ts +8 -0
  134. package/dist/math.js +374 -7
  135. package/dist/overlays/ContextMenu.svelte +3 -2
  136. package/dist/overlays/DraggablePane.svelte +163 -58
  137. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  138. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +232 -77
  139. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  140. package/dist/phase-diagram/PhaseDiagramControls.svelte +32 -11
  141. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  142. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +103 -0
  143. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  144. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +102 -95
  145. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +7 -0
  146. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +100 -26
  147. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  148. package/dist/phase-diagram/index.d.ts +2 -0
  149. package/dist/phase-diagram/index.js +2 -0
  150. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  151. package/dist/phase-diagram/svg-to-diagram.js +865 -0
  152. package/dist/phase-diagram/types.d.ts +10 -0
  153. package/dist/phase-diagram/utils.d.ts +7 -4
  154. package/dist/phase-diagram/utils.js +149 -59
  155. package/dist/plot/AxisLabel.svelte +26 -0
  156. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  157. package/dist/plot/BarPlot.svelte +473 -228
  158. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  159. package/dist/plot/BarPlotControls.svelte +3 -2
  160. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  161. package/dist/plot/ColorBar.svelte +54 -54
  162. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  163. package/dist/plot/ElementScatter.svelte +4 -3
  164. package/dist/plot/FillArea.svelte +4 -1
  165. package/dist/plot/Histogram.svelte +320 -230
  166. package/dist/plot/Histogram.svelte.d.ts +2 -2
  167. package/dist/plot/HistogramControls.svelte +29 -10
  168. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  169. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +2 -2
  170. package/dist/plot/PlotControls.svelte +109 -27
  171. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  172. package/dist/plot/PlotLegend.svelte +1 -1
  173. package/dist/plot/PortalSelect.svelte +2 -1
  174. package/dist/plot/ReferenceLine.svelte +2 -1
  175. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  176. package/dist/plot/ReferencePlane.svelte +1 -3
  177. package/dist/plot/ScatterPlot.svelte +343 -209
  178. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  179. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  180. package/dist/plot/ScatterPlot3DControls.svelte +203 -250
  181. package/dist/plot/ScatterPlot3DScene.svelte +4 -7
  182. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  183. package/dist/plot/ScatterPlotControls.svelte +95 -55
  184. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  185. package/dist/plot/ZeroLines.svelte +44 -0
  186. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  187. package/dist/plot/ZoomRect.svelte +21 -0
  188. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  189. package/dist/plot/axis-utils.d.ts +1 -1
  190. package/dist/plot/data-cleaning.js +1 -5
  191. package/dist/plot/index.d.ts +6 -2
  192. package/dist/plot/index.js +6 -2
  193. package/dist/plot/interactions.d.ts +8 -10
  194. package/dist/plot/interactions.js +10 -19
  195. package/dist/plot/layout.d.ts +7 -1
  196. package/dist/plot/layout.js +12 -4
  197. package/dist/plot/reference-line.d.ts +4 -21
  198. package/dist/plot/reference-line.js +7 -81
  199. package/dist/plot/types.d.ts +42 -17
  200. package/dist/plot/types.js +10 -0
  201. package/dist/plot/utils/label-placement.js +14 -11
  202. package/dist/plot/utils.d.ts +1 -0
  203. package/dist/plot/utils.js +14 -0
  204. package/dist/rdf/RdfPlot.svelte +55 -66
  205. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  206. package/dist/rdf/index.d.ts +1 -1
  207. package/dist/rdf/index.js +1 -1
  208. package/dist/settings.d.ts +5 -0
  209. package/dist/settings.js +37 -3
  210. package/dist/spectral/Bands.svelte +515 -143
  211. package/dist/spectral/Bands.svelte.d.ts +22 -2
  212. package/dist/spectral/helpers.d.ts +23 -1
  213. package/dist/spectral/helpers.js +65 -9
  214. package/dist/spectral/types.d.ts +2 -0
  215. package/dist/structure/AtomLegend.svelte +31 -10
  216. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  217. package/dist/structure/CellSelect.svelte +92 -22
  218. package/dist/structure/Lattice.svelte +2 -0
  219. package/dist/structure/Structure.svelte +716 -173
  220. package/dist/structure/Structure.svelte.d.ts +7 -2
  221. package/dist/structure/StructureControls.svelte +26 -14
  222. package/dist/structure/StructureControls.svelte.d.ts +5 -1
  223. package/dist/structure/StructureInfoPane.svelte +7 -1
  224. package/dist/structure/StructureScene.svelte +386 -95
  225. package/dist/structure/StructureScene.svelte.d.ts +15 -4
  226. package/dist/structure/atom-properties.d.ts +6 -2
  227. package/dist/structure/atom-properties.js +38 -25
  228. package/dist/structure/export.js +10 -7
  229. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  230. package/dist/structure/ferrox-wasm-types.js +0 -3
  231. package/dist/structure/ferrox-wasm.d.ts +3 -2
  232. package/dist/structure/ferrox-wasm.js +1 -2
  233. package/dist/structure/index.d.ts +7 -0
  234. package/dist/structure/index.js +22 -0
  235. package/dist/structure/parse.js +19 -16
  236. package/dist/structure/partial-occupancy.d.ts +25 -0
  237. package/dist/structure/partial-occupancy.js +102 -0
  238. package/dist/structure/validation.js +6 -3
  239. package/dist/symmetry/SymmetryStats.svelte +18 -4
  240. package/dist/symmetry/WyckoffTable.svelte +18 -10
  241. package/dist/symmetry/index.d.ts +7 -4
  242. package/dist/symmetry/index.js +83 -18
  243. package/dist/table/HeatmapTable.svelte +468 -69
  244. package/dist/table/HeatmapTable.svelte.d.ts +13 -1
  245. package/dist/table/ToggleMenu.svelte +291 -44
  246. package/dist/table/ToggleMenu.svelte.d.ts +4 -1
  247. package/dist/table/index.d.ts +3 -0
  248. package/dist/tooltip/index.d.ts +1 -1
  249. package/dist/tooltip/index.js +1 -0
  250. package/dist/trajectory/Trajectory.svelte +147 -145
  251. package/dist/trajectory/TrajectoryExportPane.svelte +13 -9
  252. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  253. package/dist/trajectory/constants.d.ts +6 -0
  254. package/dist/trajectory/constants.js +7 -0
  255. package/dist/trajectory/extract.js +3 -5
  256. package/dist/trajectory/format-detect.d.ts +9 -0
  257. package/dist/trajectory/format-detect.js +76 -0
  258. package/dist/trajectory/frame-reader.d.ts +17 -0
  259. package/dist/trajectory/frame-reader.js +339 -0
  260. package/dist/trajectory/helpers.d.ts +15 -0
  261. package/dist/trajectory/helpers.js +187 -0
  262. package/dist/trajectory/index.d.ts +1 -0
  263. package/dist/trajectory/index.js +11 -4
  264. package/dist/trajectory/parse/ase.d.ts +2 -0
  265. package/dist/trajectory/parse/ase.js +76 -0
  266. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  267. package/dist/trajectory/parse/hdf5.js +121 -0
  268. package/dist/trajectory/parse/index.d.ts +12 -0
  269. package/dist/trajectory/parse/index.js +304 -0
  270. package/dist/trajectory/parse/lammps.d.ts +5 -0
  271. package/dist/trajectory/parse/lammps.js +169 -0
  272. package/dist/trajectory/parse/vasp.d.ts +2 -0
  273. package/dist/trajectory/parse/vasp.js +65 -0
  274. package/dist/trajectory/parse/xyz.d.ts +2 -0
  275. package/dist/trajectory/parse/xyz.js +109 -0
  276. package/dist/trajectory/types.d.ts +11 -0
  277. package/dist/trajectory/types.js +1 -0
  278. package/dist/utils.d.ts +2 -0
  279. package/dist/utils.js +4 -0
  280. package/dist/xrd/XrdPlot.svelte +6 -4
  281. package/dist/xrd/calc-xrd.js +0 -1
  282. package/package.json +33 -23
  283. package/readme.md +4 -4
  284. package/dist/trajectory/parse.d.ts +0 -42
  285. package/dist/trajectory/parse.js +0 -1267
  286. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
@@ -1,16 +1,20 @@
1
1
  <script lang="ts">import { AXIS_COLORS, NEG_AXIS_COLORS } from '../colors';
2
2
  import { element_data } from '../element';
3
+ import Isosurface from '../isosurface/Isosurface.svelte';
4
+ import { DEFAULT_ISOSURFACE_SETTINGS } from '../isosurface/types';
3
5
  import { format_num } from '../labels';
4
6
  import * as math from '../math';
5
7
  import { DEFAULTS } from '../settings';
6
8
  import { colors } from '../state.svelte';
7
- import { Arrow, atomic_radii, Cylinder, get_center_of_mass, Lattice, } from './';
9
+ import { Arrow, atomic_radii, Cylinder, get_center_of_mass, get_site_vector_info, Lattice, } from './';
8
10
  import { get_orig_site_idx, get_property_colors, } from './atom-properties';
9
11
  import * as measure from './measure';
12
+ import { compute_slice_geometry, merge_split_partial_sites, PARTIAL_OCCUPANCY_CAP_ARC, } from './partial-occupancy';
10
13
  import { T, useThrelte } from '@threlte/core';
11
14
  import * as extras from '@threlte/extras';
12
15
  import { untrack } from 'svelte';
13
- import { SvelteMap } from 'svelte/reactivity';
16
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity';
17
+ import { Color } from 'three';
14
18
  import Bond from './Bond.svelte';
15
19
  import { BONDING_STRATEGIES, compute_bond_transform } from './bonding';
16
20
  import { CanvasTooltip } from './index';
@@ -32,21 +36,21 @@ $effect(() => {
32
36
  frame_id = requestAnimationFrame(animate);
33
37
  return () => cancelAnimationFrame(frame_id);
34
38
  });
35
- let { structure = undefined, base_structure = undefined, atom_radius = DEFAULTS.structure.atom_radius, same_size_atoms = false, camera_position = DEFAULTS.structure.camera_position, camera_projection = DEFAULTS.structure.camera_projection, rotation_damping = DEFAULTS.structure.rotation_damping, max_zoom = DEFAULTS.structure.max_zoom, min_zoom = DEFAULTS.structure.min_zoom, rotate_speed = DEFAULTS.structure.rotate_speed, zoom_speed = DEFAULTS.structure.zoom_speed, pan_speed = DEFAULTS.structure.pan_speed, zoom_to_cursor = DEFAULTS.structure.zoom_to_cursor, show_atoms = DEFAULTS.structure.show_atoms, show_bonds = DEFAULTS.structure.show_bonds, show_site_labels = DEFAULTS.structure.show_site_labels, show_site_indices = DEFAULTS.structure.show_site_indices, site_label_size = DEFAULTS.structure.site_label_size, site_label_offset = $bindable(DEFAULTS.structure.site_label_offset), site_label_bg_color = `color-mix(in srgb, #000000 0%, transparent)`, site_label_color = `#ffffff`, site_label_padding = 3, show_force_vectors = DEFAULTS.structure.show_force_vectors, force_scale = DEFAULTS.structure.force_scale, force_color = DEFAULTS.structure.force_color, gizmo = DEFAULTS.structure.show_gizmo, hovered_idx = $bindable(null), hovered_site = $bindable(null), float_fmt = `.3~f`, auto_rotate = DEFAULTS.structure.auto_rotate, bond_thickness = DEFAULTS.structure.bond_thickness, bond_color = DEFAULTS.structure.bond_color, bonding_strategy = DEFAULTS.structure.bonding_strategy, bonding_options = {}, fov = DEFAULTS.structure.fov, initial_zoom = DEFAULTS.structure.initial_zoom, ambient_light = DEFAULTS.structure.ambient_light, directional_light = DEFAULTS.structure.directional_light, sphere_segments = DEFAULTS.structure.sphere_segments, lattice_props = {}, atom_label, camera_is_moving = $bindable(false), width = 0, height = 0, measure_mode = `distance`, selected_sites = $bindable([]), measured_sites = $bindable([]), added_bonds = $bindable([]), removed_bonds = $bindable([]), selection_highlight_color = `#6cf0ff`,
39
+ let { structure = undefined, base_structure = undefined, atom_radius = DEFAULTS.structure.atom_radius, same_size_atoms = false, camera_position = DEFAULTS.structure.camera_position, camera_target = undefined, camera_projection = DEFAULTS.structure.camera_projection, rotation_damping = DEFAULTS.structure.rotation_damping, max_zoom = DEFAULTS.structure.max_zoom, min_zoom = DEFAULTS.structure.min_zoom, rotate_speed = DEFAULTS.structure.rotate_speed, zoom_speed = DEFAULTS.structure.zoom_speed, pan_speed = DEFAULTS.structure.pan_speed, zoom_to_cursor = DEFAULTS.structure.zoom_to_cursor, show_atoms = DEFAULTS.structure.show_atoms, show_bonds = DEFAULTS.structure.show_bonds, show_site_labels = DEFAULTS.structure.show_site_labels, show_site_indices = DEFAULTS.structure.show_site_indices, site_label_size = DEFAULTS.structure.site_label_size, site_label_offset = $bindable(DEFAULTS.structure.site_label_offset), site_label_bg_color = `color-mix(in srgb, #000000 0%, transparent)`, site_label_color = `#ffffff`, site_label_padding = 3, show_force_vectors = DEFAULTS.structure.show_force_vectors, force_scale = DEFAULTS.structure.force_scale, force_color = DEFAULTS.structure.force_color, gizmo = DEFAULTS.structure.show_gizmo, hovered_idx = $bindable(null), hovered_site = $bindable(null), float_fmt = `.3~f`, auto_rotate = DEFAULTS.structure.auto_rotate, bond_thickness = DEFAULTS.structure.bond_thickness, bond_color = DEFAULTS.structure.bond_color, bonding_strategy = DEFAULTS.structure.bonding_strategy, bonding_options = {}, fov = DEFAULTS.structure.fov, initial_zoom = DEFAULTS.structure.initial_zoom, ambient_light = DEFAULTS.structure.ambient_light, directional_light = DEFAULTS.structure.directional_light, sphere_segments = DEFAULTS.structure.sphere_segments, lattice_props = {}, atom_label, camera_is_moving = $bindable(false), width = 0, height = 0, measure_mode = `distance`, selected_sites = $bindable([]), measured_sites = $bindable([]), added_bonds = $bindable([]), removed_bonds = $bindable([]), selection_highlight_color = `#6cf0ff`,
36
40
  // Active highlight group with different color
37
- active_sites = $bindable([]), active_highlight_color = `var(--struct-active-highlight-color, #2563eb)`, rotation = DEFAULTS.structure.rotation, scene = $bindable(), camera = $bindable(), orbit_controls = $bindable(), rotation_target_ref = $bindable(), initial_computed_zoom = $bindable(), hidden_elements = $bindable(new Set()), hidden_prop_vals = $bindable(new Set()), element_radius_overrides = $bindable({}), site_radius_overrides = $bindable(new SvelteMap()), atom_color_config = {
41
+ active_sites = $bindable([]), active_highlight_color = `var(--struct-active-highlight-color, #2563eb)`, rotation = DEFAULTS.structure.rotation, scene = $bindable(), camera = $bindable(), orbit_controls = $bindable(), rotation_target_ref = $bindable(), initial_computed_zoom = $bindable(), hidden_elements = $bindable(new SvelteSet()), hidden_prop_vals = $bindable(new SvelteSet()), element_radius_overrides = $bindable({}), site_radius_overrides = $bindable(new SvelteMap()), atom_color_config = {
38
42
  mode: DEFAULTS.structure.atom_color_mode,
39
43
  scale: DEFAULTS.structure.atom_color_scale,
40
44
  scale_type: DEFAULTS.structure.atom_color_scale_type,
41
- }, sym_data = null, } = $props();
45
+ }, sym_data = null,
46
+ // Edit-atoms mode callbacks
47
+ on_sites_moved, on_operation_start, on_add_atom, add_atom_mode = $bindable(false), add_element = $bindable(`C`), cursor = $bindable(`default`), dragging_atoms = $bindable(false), volumetric_data = undefined, isosurface_settings = DEFAULT_ISOSURFACE_SETTINGS, } = $props();
42
48
  const threlte = useThrelte();
43
49
  $effect(() => {
44
50
  scene = threlte.scene;
45
51
  camera = threlte.camera.current;
46
52
  if (threlte.renderer) {
47
- Object.assign(threlte.renderer.domElement, {
48
- __renderer: threlte.renderer,
49
- });
53
+ Object.assign(threlte.renderer.domElement, { __renderer: threlte.renderer });
50
54
  }
51
55
  });
52
56
  // Expose rotation target for external reset
@@ -63,11 +67,75 @@ $effect(() => {
63
67
  });
64
68
  let bond_pairs = $state([]);
65
69
  let active_tooltip = $state(null);
70
+ let hovered_bond_key = $state(null);
71
+ // Cursor style for the canvas, derived from mode and hover state
72
+ let canvas_cursor = $derived.by(() => {
73
+ if (measure_mode === `edit-atoms` && add_atom_mode)
74
+ return `crosshair`;
75
+ if (hovered_idx != null) {
76
+ if (measure_mode === `edit-atoms`) {
77
+ const site = structure?.sites?.[hovered_idx];
78
+ if (site?.properties?.orig_site_idx != null)
79
+ return `not-allowed`;
80
+ return `pointer`;
81
+ }
82
+ return `pointer`;
83
+ }
84
+ return `default`;
85
+ });
86
+ // Desaturate a color by blending it toward gray (for ghosting image atoms in edit mode)
87
+ const gray = new Color(0x999999);
88
+ function desaturate(hex, amount = 0.4) {
89
+ return `#${new Color(hex ?? 0x999999).lerp(gray, amount).getHexString()}`;
90
+ }
91
+ // === Edit-atoms mode state ===
92
+ let transform_object = $state(undefined);
93
+ // Plain variable — only used imperatively in TransformControls drag handlers
94
+ let drag_start_centroid = null;
95
+ // Frozen centroid set on drag start. While non-null, the TransformControls mesh
96
+ // position stays at this fixed value so Svelte's reactive centroid updates (from
97
+ // PBC wrapping) don't fight TransformControls. Cleared on mouseUp so the mesh
98
+ // snaps to the new wrapped centroid.
99
+ let frozen_centroid = $state(null);
66
100
  function get_bond_key(idx1, idx2) {
67
101
  return idx1 < idx2 ? `${idx1}-${idx2}` : `${idx2}-${idx1}`;
68
102
  }
103
+ // Toggle a bond between two atoms: cycles through add → remove → restore states
104
+ function toggle_bond(site_1, site_2) {
105
+ const idx_i = Math.min(site_1, site_2);
106
+ const idx_j = Math.max(site_1, site_2);
107
+ // added/removed pairs are stored sorted, so direct comparison works
108
+ const match = ([a, b]) => a === idx_i && b === idx_j;
109
+ const added_idx = added_bonds.findIndex(match);
110
+ if (added_idx >= 0) {
111
+ added_bonds = added_bonds.toSpliced(added_idx, 1);
112
+ return;
113
+ }
114
+ const removed_idx = removed_bonds.findIndex(match);
115
+ if (removed_idx >= 0) {
116
+ removed_bonds = removed_bonds.toSpliced(removed_idx, 1);
117
+ return;
118
+ }
119
+ // bond_pairs may not be sorted, so use get_bond_key for comparison
120
+ const key = `${idx_i}-${idx_j}`;
121
+ if (bond_pairs.some((bond) => get_bond_key(bond.site_idx_1, bond.site_idx_2) === key))
122
+ removed_bonds = [...removed_bonds, [idx_i, idx_j]];
123
+ else
124
+ added_bonds = [...added_bonds, [idx_i, idx_j]];
125
+ }
126
+ // Deduplicate clicks: when a highlight sphere and the underlying atom both
127
+ // intercept the same native click, only the first intersection should fire.
128
+ // All threlte intersection events from one click share the same nativeEvent ref.
129
+ let last_native_event = null;
69
130
  function toggle_selection(site_index, evt) {
70
131
  evt?.stopPropagation?.();
132
+ const native_event = evt
133
+ ?.nativeEvent;
134
+ if (native_event instanceof Event) {
135
+ if (native_event === last_native_event)
136
+ return;
137
+ last_native_event = native_event;
138
+ }
71
139
  if (measure_mode === `edit-bonds`) {
72
140
  // In edit-bonds mode, select atoms to add/remove bonds between them
73
141
  const new_sites = measured_sites.includes(site_index)
@@ -77,34 +145,35 @@ function toggle_selection(site_index, evt) {
77
145
  selected_sites = new_sites;
78
146
  // When two atoms are selected, toggle the bond between them
79
147
  if (measured_sites.length === 2) {
80
- const [idx_i, idx_j] = measured_sites.sort((a, b) => a - b);
81
- const key = get_bond_key(idx_i, idx_j);
82
- // Check if bond exists in calculated bonds (and not removed)
83
- const calculated_exists = bond_pairs.some((bond) => get_bond_key(bond.site_idx_1, bond.site_idx_2) === key);
84
- const is_removed = removed_bonds.some((pair) => get_bond_key(pair[0], pair[1]) === key);
85
- const is_added = added_bonds.some((pair) => get_bond_key(pair[0], pair[1]) === key);
86
- if (is_added) {
87
- // Toggle off added bond
88
- added_bonds = added_bonds.filter((pair) => get_bond_key(pair[0], pair[1]) !== key);
89
- }
90
- else if (calculated_exists && !is_removed) {
91
- // Remove calculated bond
92
- removed_bonds = [...removed_bonds, [idx_i, idx_j]];
93
- }
94
- else if (is_removed) {
95
- // Restore calculated bond
96
- removed_bonds = removed_bonds.filter((pair) => get_bond_key(pair[0], pair[1]) !== key);
97
- }
98
- else {
99
- // Add new bond
100
- added_bonds = [...added_bonds, [idx_i, idx_j]];
101
- }
102
- // Reset selection after toggling bond
148
+ toggle_bond(measured_sites[0], measured_sites[1]);
103
149
  measured_sites = [];
104
150
  selected_sites = [];
105
151
  }
106
152
  return;
107
153
  }
154
+ if (measure_mode === `edit-atoms`) {
155
+ // Block image atoms (detected by orig_site_idx property from PBC)
156
+ const site = structure?.sites?.[site_index];
157
+ if (site?.properties?.orig_site_idx != null)
158
+ return;
159
+ const is_selected = selected_sites.includes(site_index);
160
+ const is_shift = evt instanceof MouseEvent && evt.shiftKey;
161
+ // In edit-atoms mode, selected_sites and measured_sites always stay in sync
162
+ let new_sites;
163
+ if (is_shift) {
164
+ // Multi-select: toggle this site in/out of selection
165
+ new_sites = is_selected
166
+ ? selected_sites.filter((idx) => idx !== site_index)
167
+ : [...selected_sites, site_index];
168
+ }
169
+ else {
170
+ // Single-select: replace selection (or deselect if already selected)
171
+ new_sites = is_selected ? [] : [site_index];
172
+ }
173
+ selected_sites = new_sites;
174
+ measured_sites = new_sites;
175
+ return;
176
+ }
108
177
  if (!measured_sites.includes(site_index) &&
109
178
  measured_sites.length >= measure.MAX_SELECTED_SITES) {
110
179
  console.warn(`Selection size limit reached (${measure.MAX_SELECTED_SITES}). Deselect some sites first.`);
@@ -127,6 +196,9 @@ $effect(() => {
127
196
  measured_sites = measured_sites.filter((idx) => idx >= 0 && idx < count);
128
197
  });
129
198
  });
199
+ $effect(() => {
200
+ cursor = canvas_cursor;
201
+ });
130
202
  extras.interactivity();
131
203
  $effect.pre(() => {
132
204
  hovered_site = structure?.sites?.[hovered_idx ?? -1] ?? null;
@@ -159,7 +231,7 @@ $effect(() => {
159
231
  computed_zoom = new_zoom;
160
232
  });
161
233
  $effect.pre(() => {
162
- if (camera_position.every((v) => v === 0) && structure) {
234
+ if (camera_position.every((val) => val === 0) && structure) {
163
235
  const distance = Math.max(1, structure_size) * (60 / fov);
164
236
  camera_position = [distance, distance * 0.3, distance * 0.8];
165
237
  }
@@ -195,7 +267,8 @@ const calc_weighted_radius = (site) => {
195
267
  let atom_data = $derived.by(() => {
196
268
  if (!show_atoms || !structure?.sites)
197
269
  return [];
198
- return structure.sites.flatMap((site, site_idx) => {
270
+ const render_sites = merge_split_partial_sites(structure.sites, hidden_elements);
271
+ return render_sites.flatMap(({ site_idx, site, is_image_atom }) => {
199
272
  const orig_idx = get_orig_site_idx(site, site_idx);
200
273
  // Skip sites with hidden property values
201
274
  const prop_val = property_colors?.values[orig_idx];
@@ -210,20 +283,25 @@ let atom_data = $derived.by(() => {
210
283
  // Use property color if available (e.g. coordination number, Wyckoff position)
211
284
  // Otherwise, each species gets its own element color (important for disordered sites)
212
285
  const site_property_color = property_colors?.colors[orig_idx];
213
- let start_angle = 0;
214
- return site.species
215
- .filter(({ element }) => !hidden_elements.has(element))
216
- .map(({ element, occu }) => ({
217
- site_idx,
218
- element,
219
- occupancy: occu,
220
- position: site.xyz,
221
- radius,
222
- color: site_property_color ?? colors.element?.[element],
223
- has_partial_occupancy: occu < 1,
224
- start_phi: 2 * Math.PI * start_angle,
225
- end_phi: 2 * Math.PI * (start_angle += occu),
226
- }));
286
+ const visible_species = site.species.filter(({ element }) => !hidden_elements.has(element));
287
+ const slice_geometry = compute_slice_geometry(visible_species);
288
+ return slice_geometry.map((slice_data) => {
289
+ return {
290
+ site_idx,
291
+ element: slice_data.element,
292
+ occupancy: slice_data.occupancy,
293
+ position: site.xyz,
294
+ radius,
295
+ color: site_property_color ?? colors.element?.[slice_data.element],
296
+ has_partial_occupancy: slice_data.occupancy < 1,
297
+ start_phi: slice_data.start_phi,
298
+ end_phi: slice_data.end_phi,
299
+ phi_length: slice_data.phi_length,
300
+ render_start_cap: slice_data.render_start_cap,
301
+ render_end_cap: slice_data.render_end_cap,
302
+ is_image_atom,
303
+ };
304
+ });
227
305
  });
228
306
  });
229
307
  let filtered_bond_pairs = $derived.by(() => {
@@ -313,28 +391,55 @@ const get_site_radius = (site, site_idx) => {
313
391
  const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site);
314
392
  return base_radius * atom_radius;
315
393
  };
316
- let force_data = $derived.by(() => show_force_vectors && structure?.sites
317
- ? structure?.sites
394
+ // Interpolate between spin-down (#3498db blue) and spin-up (#e74c3c red)
395
+ // based on the z-component direction of a magnetic vector
396
+ function spin_direction_color(vec) {
397
+ const mag = Math.hypot(...vec);
398
+ const z_frac = mag > 1e-10 ? (vec[2] / mag + 1) / 2 : 0.5; // 0=down, 1=up
399
+ const red = Math.round(52 + (231 - 52) * z_frac);
400
+ const grn = Math.round(152 + (76 - 152) * z_frac);
401
+ const blu = Math.round(219 + (60 - 219) * z_frac);
402
+ return `#${red.toString(16).padStart(2, `0`)}${grn.toString(16).padStart(2, `0`)}${blu.toString(16).padStart(2, `0`)}`;
403
+ }
404
+ // Extract per-site vectors from force, magmom, or spin properties.
405
+ // For magmom/spin, color interpolates between spin-up (red) and spin-down (blue).
406
+ let force_data = $derived.by(() => {
407
+ if (!show_force_vectors || !structure?.sites)
408
+ return [];
409
+ return structure.sites
318
410
  .map((site) => {
319
- if (!site.properties?.force || !Array.isArray(site.properties.force))
411
+ const info = get_site_vector_info(site);
412
+ if (!info)
320
413
  return null;
321
- const majority_element = site.species.reduce((max, spec) => spec.occu > max.occu ? spec : max).element;
414
+ let arrow_color;
415
+ if (info.key !== `force`) {
416
+ arrow_color = spin_direction_color(info.vec);
417
+ }
418
+ else {
419
+ const majority_element = site.species.length > 0
420
+ ? site.species.reduce((max, spec) => spec.occu > max.occu ? spec : max)
421
+ .element
422
+ : undefined;
423
+ arrow_color = (majority_element && colors.element?.[majority_element]) ||
424
+ force_color;
425
+ }
322
426
  return {
323
427
  position: site.xyz,
324
- vector: site.properties.force,
428
+ vector: info.vec,
325
429
  scale: force_scale,
326
- color: colors.element?.[majority_element] || force_color,
430
+ color: arrow_color,
327
431
  };
328
432
  })
329
- .filter((item) => item !== null)
330
- : []);
433
+ .filter((item) => item !== null);
434
+ });
331
435
  let instanced_atom_groups = $derived(Object.values(atom_data
332
436
  .filter((atom) => !atom.has_partial_occupancy)
333
437
  .reduce((groups, atom) => {
334
- const { element, radius, color } = atom;
335
- const key = `${element}-${format_num(radius, `.3~`)}-${color}`;
438
+ const { element, radius, color, is_image_atom } = atom;
439
+ // Separate image atoms into their own groups for distinct styling in edit-atoms mode
440
+ const key = `${element}-${format_num(radius, `.3~`)}-${color}-${is_image_atom ? `img` : `base`}`;
336
441
  const bucket = groups[key] ||
337
- (groups[key] = { element, radius, color, atoms: [] });
442
+ (groups[key] = { element, radius, color, is_image_atom, atoms: [] });
338
443
  bucket.atoms.push(atom);
339
444
  return groups;
340
445
  }, {})));
@@ -375,7 +480,7 @@ let orbit_controls_props = $derived({
375
480
  zoomToCursor: zoom_to_cursor,
376
481
  enablePan: pan_speed > 0,
377
482
  panSpeed: pan_speed,
378
- target: rotation_target,
483
+ target: camera_target ?? rotation_target,
379
484
  maxZoom: max_zoom,
380
485
  minZoom: min_zoom,
381
486
  autoRotate: Boolean(auto_rotate),
@@ -421,27 +526,17 @@ let measure_line_color = $derived.by(() => {
421
526
  style:padding="{site_label_padding}px"
422
527
  style:color={site_label_color}
423
528
  >
424
- {#if show_site_labels && show_site_indices}
425
- {#if site.species.length === 1}
426
- {site.species[0].element}-{site_idx + 1}
427
- {:else}
428
- {@html site.species.map((spec) =>
429
- `${spec.element}<sub>${
430
- format_num(spec.occu, `.3~`).replace(`0.`, `.`)
431
- }</sub>`
432
- ).join(``)}-{
433
- site_idx + 1
434
- }
435
- {/if}
436
- {:else if show_site_labels}
529
+ {#if show_site_labels}
437
530
  {#if site.species.length === 1}
438
- {site.species[0].element}
531
+ {site.species[0].element}{#if show_site_indices}-{site_idx + 1}{/if}
439
532
  {:else}
440
533
  {@html site.species.map((spec) =>
441
534
  `${spec.element}<sub>${
442
535
  format_num(spec.occu, `.3~`).replace(`0.`, `.`)
443
536
  }</sub>`
444
- ).join(``)}
537
+ ).join(``)}{#if show_site_indices}-{
538
+ site_idx + 1
539
+ }{/if}
445
540
  {/if}
446
541
  {:else if show_site_indices}
447
542
  {site_idx + 1}
@@ -487,31 +582,38 @@ let measure_line_color = $derived.by(() => {
487
582
  {#if show_atoms}
488
583
  <!-- Instanced rendering for full occupancy atoms -->
489
584
  {#each instanced_atom_groups as
490
- { element, radius, color, atoms }
491
- (`${element}-${radius}-${color}`)
585
+ { element, radius, color, is_image_atom, atoms }
586
+ (`${element}-${radius}-${color}-${is_image_atom ? `img` : `base`}`)
492
587
  }
588
+ {@const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom}
493
589
  <extras.InstancedMesh
494
- key="{element}-{format_num(radius, `.3~`)}-{color}-{atoms.length}"
590
+ key="{element}-{format_num(radius, `.3~`)}-{color}-{is_image_atom ? `img` : `base`}-{edit_mode_image}"
495
591
  range={atoms.length}
496
592
  frustumCulled={false}
497
593
  >
498
594
  <T.SphereGeometry args={[0.5, sphere_segments, sphere_segments]} />
499
- <T.MeshStandardMaterial {color} />
595
+ <T.MeshStandardMaterial
596
+ color={edit_mode_image ? desaturate(color) : color}
597
+ opacity={edit_mode_image ? 0.5 : 1}
598
+ transparent={edit_mode_image}
599
+ />
500
600
  {#each atoms as atom (atom.site_idx)}
501
601
  <extras.Instance
502
602
  position={atom.position}
503
603
  scale={atom.radius}
504
604
  onpointerenter={() => {
605
+ if (edit_mode_image) return
505
606
  hovered_idx = atom.site_idx
506
607
  active_tooltip = `atom`
507
608
  }}
508
609
  onpointerleave={() => {
610
+ if (edit_mode_image) return
509
611
  hovered_idx = null
510
612
  active_tooltip = null
511
613
  }}
512
614
  onclick={(event: MouseEvent) => {
513
- const site_idx = atom.site_idx
514
- toggle_selection(site_idx, event)
615
+ if (edit_mode_image) return
616
+ toggle_selection(atom.site_idx, event)
515
617
  }}
516
618
  />
517
619
  {/each}
@@ -523,22 +625,29 @@ let measure_line_color = $derived.by(() => {
523
625
  atom
524
626
  (atom.site_idx + atom.element + atom.occupancy)
525
627
  }
628
+ {@const partial_edit_image = measure_mode === `edit-atoms` && atom.is_image_atom}
629
+ {@const ghost_opacity = partial_edit_image ? 0.5 : 1}
526
630
  <T.Group
527
631
  position={atom.position}
528
632
  scale={atom.radius}
529
633
  onpointerenter={() => {
634
+ if (partial_edit_image) return
530
635
  hovered_idx = atom.site_idx
531
636
  active_tooltip = `atom`
532
637
  }}
533
638
  onpointerleave={() => {
639
+ if (partial_edit_image) return
534
640
  hovered_idx = null
535
641
  active_tooltip = null
536
642
  }}
537
643
  onclick={(event: MouseEvent) => {
538
- const site_idx = atom.site_idx
539
- toggle_selection(site_idx, event)
644
+ if (partial_edit_image) return
645
+ toggle_selection(atom.site_idx, event)
540
646
  }}
541
647
  >
648
+ {@const partial_color = partial_edit_image
649
+ ? desaturate(atom.color)
650
+ : atom.color}
542
651
  <T.Mesh>
543
652
  <T.SphereGeometry
544
653
  args={[
@@ -546,20 +655,50 @@ let measure_line_color = $derived.by(() => {
546
655
  sphere_segments,
547
656
  sphere_segments,
548
657
  atom.start_phi,
549
- 2 * Math.PI * atom.occupancy,
658
+ atom.phi_length,
550
659
  ]}
551
660
  />
552
- <T.MeshStandardMaterial color={atom.color} />
661
+ <T.MeshStandardMaterial
662
+ color={partial_color}
663
+ opacity={ghost_opacity}
664
+ transparent={partial_edit_image}
665
+ />
553
666
  </T.Mesh>
554
667
 
555
- {#if atom.has_partial_occupancy}
668
+ {#if atom.has_partial_occupancy && atom.render_start_cap}
556
669
  <T.Mesh rotation={[0, atom.start_phi, 0]}>
557
- <T.CircleGeometry args={[0.5, sphere_segments]} />
558
- <T.MeshStandardMaterial color={atom.color} side={2} />
670
+ <T.CircleGeometry
671
+ args={[
672
+ 0.5,
673
+ sphere_segments,
674
+ PARTIAL_OCCUPANCY_CAP_ARC.start_cap_arc_start,
675
+ PARTIAL_OCCUPANCY_CAP_ARC.arc_length,
676
+ ]}
677
+ />
678
+ <T.MeshStandardMaterial
679
+ color={partial_color}
680
+ side={2}
681
+ opacity={ghost_opacity}
682
+ transparent={partial_edit_image}
683
+ />
559
684
  </T.Mesh>
685
+ {/if}
686
+ {#if atom.has_partial_occupancy && atom.render_end_cap}
560
687
  <T.Mesh rotation={[0, atom.end_phi, 0]}>
561
- <T.CircleGeometry args={[0.5, sphere_segments]} />
562
- <T.MeshStandardMaterial color={atom.color} side={2} />
688
+ <T.CircleGeometry
689
+ args={[
690
+ 0.5,
691
+ sphere_segments,
692
+ PARTIAL_OCCUPANCY_CAP_ARC.end_cap_arc_start,
693
+ PARTIAL_OCCUPANCY_CAP_ARC.arc_length,
694
+ ]}
695
+ />
696
+ <T.MeshStandardMaterial
697
+ color={partial_color}
698
+ side={2}
699
+ opacity={ghost_opacity}
700
+ transparent={partial_edit_image}
701
+ />
563
702
  </T.Mesh>
564
703
  {/if}
565
704
  </T.Group>
@@ -592,6 +731,41 @@ let measure_line_color = $derived.by(() => {
592
731
  {/each}
593
732
  {/if}
594
733
 
734
+ <!-- Clickable bond hit-test cylinders in edit-bonds mode -->
735
+ {#if measure_mode === `edit-bonds` && filtered_bond_pairs.length > 0}
736
+ {#each filtered_bond_pairs as
737
+ bond
738
+ (`bond-hit-${bond.site_idx_1}-${bond.site_idx_2}`)
739
+ }
740
+ {@const bond_key = get_bond_key(bond.site_idx_1, bond.site_idx_2)}
741
+ {@const is_hovered = hovered_bond_key === bond_key}
742
+ <T.Mesh
743
+ matrixAutoUpdate={false}
744
+ oncreate={(ref) => {
745
+ ref.matrix.fromArray(bond.transform_matrix)
746
+ ref.matrixWorldNeedsUpdate = true
747
+ }}
748
+ onclick={(event: MouseEvent) => {
749
+ event.stopPropagation()
750
+ toggle_bond(bond.site_idx_1, bond.site_idx_2)
751
+ measured_sites = []
752
+ selected_sites = []
753
+ hovered_bond_key = null
754
+ }}
755
+ onpointerenter={() => (hovered_bond_key = bond_key)}
756
+ onpointerleave={() => (hovered_bond_key = null)}
757
+ >
758
+ <T.CylinderGeometry args={[bond_thickness * 3, bond_thickness * 3, 1, 6]} />
759
+ <T.MeshBasicMaterial
760
+ transparent
761
+ opacity={is_hovered ? 0.25 : 0}
762
+ color={is_hovered ? `#ff4444` : `white`}
763
+ depthWrite={false}
764
+ />
765
+ </T.Mesh>
766
+ {/each}
767
+ {/if}
768
+
595
769
  <!-- highlight hovered, active and selected sites -->
596
770
  {#each [
597
771
  {
@@ -648,8 +822,9 @@ let measure_line_color = $derived.by(() => {
648
822
  {/if}
649
823
  {/each}
650
824
 
651
- <!-- selection order labels (1, 2, 3, ...) for measured sites -->
652
- {#if structure?.sites && (measured_sites?.length ?? 0) > 0}
825
+ <!-- selection order labels (1, 2, 3, ...) for measured sites (hidden in edit-atoms mode) -->
826
+ {#if structure?.sites && (measured_sites?.length ?? 0) > 0 &&
827
+ measure_mode !== `edit-atoms`}
653
828
  {#each measured_sites as site_index, loop_idx (site_index)}
654
829
  {@const site = structure.sites[site_index]}
655
830
  {#if site}
@@ -665,8 +840,36 @@ let measure_line_color = $derived.by(() => {
665
840
 
666
841
  <!-- hovered site tooltip -->
667
842
  {#if hovered_site && !camera_is_moving && active_tooltip === `atom`}
668
- {@const abc = hovered_site.abc.map((x) => format_num(x, float_fmt)).join(`, `)}
669
- {@const xyz = hovered_site.xyz.map((x) => format_num(x, float_fmt)).join(`, `)}
843
+ {@const abc = hovered_site.abc.map((val) => format_num(val, float_fmt)).join(
844
+ `, `,
845
+ )}
846
+ {@const xyz = hovered_site.xyz.map((val) => format_num(val, float_fmt)).join(
847
+ `, `,
848
+ )}
849
+ {@const bond_neighbors = (() => {
850
+ if (hovered_idx == null || !structure?.sites) return []
851
+ return filtered_bond_pairs
852
+ .filter((b) =>
853
+ b.site_idx_1 === hovered_idx || b.site_idx_2 === hovered_idx
854
+ )
855
+ .map((b) => {
856
+ const neighbor_idx = b.site_idx_1 === hovered_idx
857
+ ? b.site_idx_2
858
+ : b.site_idx_1
859
+ return structure.sites[neighbor_idx]?.species[0]?.element ?? `?`
860
+ })
861
+ })()}
862
+ {@const bond_summary = (() => {
863
+ if (bond_neighbors.length === 0) return ``
864
+ const counts: Record<string, number> = {}
865
+ for (const elem of bond_neighbors) {
866
+ counts[elem] = (counts[elem] ?? 0) + 1
867
+ }
868
+ const parts = Object.entries(counts)
869
+ .sort(([a], [b]) => a.localeCompare(b))
870
+ .map(([elem, count]) => `${elem}: ${count}`)
871
+ return ` (${parts.join(`, `)})`
872
+ })()}
670
873
  <CanvasTooltip position={hovered_site.xyz}>
671
874
  <!-- Element symbols with occupancies for disordered sites -->
672
875
  <div class="elements">
@@ -694,6 +897,9 @@ let measure_line_color = $derived.by(() => {
694
897
  </div>
695
898
  <div class="coordinates fractional">abc: ({abc})</div>
696
899
  <div class="coordinates cartesian">xyz: ({xyz}) Å</div>
900
+ {#if bond_neighbors.length > 0}
901
+ <div class="coordinates">Bonds: {bond_neighbors.length}{bond_summary}</div>
902
+ {/if}
697
903
  </CanvasTooltip>
698
904
  {/if}
699
905
 
@@ -701,6 +907,91 @@ let measure_line_color = $derived.by(() => {
701
907
  <Lattice matrix={visual_lattice.matrix} {...lattice_props} />
702
908
  {/if}
703
909
 
910
+ <!-- TransformControls for editing atoms in edit-atoms mode -->
911
+ {#if measure_mode === `edit-atoms` && selected_sites.length > 0 &&
912
+ structure?.sites}
913
+ {@const selected_atoms = selected_sites
914
+ .map((idx) => structure?.sites?.[idx])
915
+ .filter((site): site is Site => site != null)}
916
+ {#if selected_atoms.length > 0}
917
+ {@const avg = (dim: number) =>
918
+ selected_atoms.reduce((sum, atom) => sum + atom.xyz[dim], 0) /
919
+ selected_atoms.length}
920
+ {@const centroid = [avg(0), avg(1), avg(2)] as Vec3}
921
+ <!-- Invisible mesh at centroid for TransformControls to manipulate.
922
+ During drag, use frozen_centroid so Svelte doesn't override TransformControls
923
+ with the wrapped centroid (which jumps on PBC boundary crossings). -->
924
+ <T.Mesh
925
+ position={frozen_centroid ?? centroid}
926
+ bind:ref={transform_object}
927
+ >
928
+ <T.SphereGeometry args={[0.01, 4, 4]} />
929
+ <T.MeshBasicMaterial transparent opacity={0} />
930
+ </T.Mesh>
931
+ <extras.TransformControls
932
+ object={transform_object}
933
+ translationSnap={0.1}
934
+ size={1.2}
935
+ space="world"
936
+ onobjectChange={() => {
937
+ if (!transform_object?.position || !drag_start_centroid) return
938
+ const { x: tx, y: ty, z: tz } = transform_object.position
939
+ const delta: Vec3 = [
940
+ tx - drag_start_centroid[0],
941
+ ty - drag_start_centroid[1],
942
+ tz - drag_start_centroid[2],
943
+ ]
944
+ // Update reference point so deltas are incremental, not cumulative.
945
+ // Without this, each frame compounds: sites already moved by previous
946
+ // delta get the full cumulative delta re-applied.
947
+ drag_start_centroid = [tx, ty, tz]
948
+ on_sites_moved?.(selected_sites, delta)
949
+ }}
950
+ onmouseDown={() => {
951
+ dragging_atoms = true
952
+ drag_start_centroid = frozen_centroid = [...centroid] as Vec3
953
+ on_operation_start?.()
954
+ }}
955
+ onmouseUp={() => {
956
+ dragging_atoms = false
957
+ frozen_centroid = null
958
+ drag_start_centroid = null
959
+ }}
960
+ />
961
+ {/if}
962
+ {/if}
963
+
964
+ <!-- Invisible plane for click-to-place atom in add-atom mode -->
965
+ <!-- Uses onBeforeRender to orient normal toward camera so raycasts always hit -->
966
+ {#if measure_mode === `edit-atoms` && add_atom_mode}
967
+ {@const center = rotation_target ?? [0, 0, 0]}
968
+ <T.Mesh
969
+ position={center}
970
+ onBeforeRender={(mesh: Mesh) => {
971
+ if (camera) {
972
+ mesh.lookAt(camera.position)
973
+ }
974
+ }}
975
+ onclick={(event: { point: { x: number; y: number; z: number } }) => {
976
+ const { x, y, z } = event.point
977
+ on_add_atom?.([x, y, z] as Vec3, add_element as ElementSymbol)
978
+ }}
979
+ >
980
+ <T.PlaneGeometry
981
+ args={[
982
+ Math.max(200, structure_size * 4),
983
+ Math.max(200, structure_size * 4),
984
+ ]}
985
+ />
986
+ <T.MeshBasicMaterial transparent opacity={0} side={2} depthWrite={false} />
987
+ </T.Mesh>
988
+ {/if}
989
+
990
+ <!-- Isosurface rendering from volumetric data (CHGCAR, .cube files) -->
991
+ {#if volumetric_data && isosurface_settings}
992
+ <Isosurface volume={volumetric_data} settings={isosurface_settings} />
993
+ {/if}
994
+
704
995
  <!-- Measurement overlays for measured sites -->
705
996
  {#if structure?.sites && (measured_sites?.length ?? 0) > 0}
706
997
  {#if measure_mode === `distance`}
@@ -741,12 +1032,12 @@ let measure_line_color = $derived.by(() => {
741
1032
  {:else if measure_mode === `angle` && measured_sites.length >= 3}
742
1033
  {#each measured_sites as idx_center (idx_center)}
743
1034
  {@const center = structure.sites[idx_center]}
744
- {#each measured_sites.filter((x) => x !== idx_center) as
1035
+ {#each measured_sites.filter((idx) => idx !== idx_center) as
745
1036
  idx_a,
746
1037
  loop_idx
747
1038
  (idx_center + `-` + idx_a)
748
1039
  }
749
- {#each measured_sites.filter((x) => x !== idx_center).slice(loop_idx + 1) as
1040
+ {#each measured_sites.filter((idx) => idx !== idx_center).slice(loop_idx + 1) as
750
1041
  idx_b
751
1042
  (idx_center + `-` + idx_a + `-` + idx_b)
752
1043
  }