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
@@ -20,10 +20,11 @@
20
20
  import type { Vec3 } from '../math'
21
21
  import { create_cart_to_frac, create_frac_to_cart } from '../math'
22
22
  import { DEFAULTS } from '../settings'
23
- import { sanitize_html } from '../sanitize'
24
23
  import { colors } from '../state.svelte'
25
24
  import type {
26
25
  AnyStructure,
26
+ BondEditMode,
27
+ BondOrder,
27
28
  Crystal,
28
29
  MeasureMode,
29
30
  StructureBond,
@@ -55,7 +56,7 @@
55
56
  import { get_property_colors } from './atom-properties'
56
57
  import AtomLegend from './AtomLegend.svelte'
57
58
  import CellSelect from './CellSelect.svelte'
58
- import { merge_bond_edits } from './bonding'
59
+ import { BOND_ORDER_OPTIONS, merge_bond_edits } from './bonding'
59
60
  import type { StructureHandlerData } from './index'
60
61
  import { MAX_SELECTED_SITES } from './measure'
61
62
  import { normalize_fractional_coords, parse_any_structure } from './parse'
@@ -67,7 +68,8 @@
67
68
  // Type alias for event handlers to reduce verbosity
68
69
  type EventHandler = (data: StructureHandlerData) => void
69
70
  type BondEditContext = {
70
- structure?: AnyStructure
71
+ structure_identity: AnyStructure | undefined
72
+ source_bond_signature: string
71
73
  cell_type: CellType
72
74
  supercell_scaling: string
73
75
  show_image_atoms: boolean
@@ -76,15 +78,18 @@
76
78
  bonds: StructureBond[] | undefined
77
79
  context: BondEditContext
78
80
  }
81
+ type BondEditHistorySnapshot = {
82
+ added_bonds: StructureBond[]
83
+ removed_bonds: StructureBond[]
84
+ bond_order_overrides: StructureBond[]
85
+ bond_edit_mode: BondEditMode
86
+ bond_edit_order: BondOrder
87
+ }
88
+ type SceneProps = ComponentProps<typeof StructureScene> & typeof DEFAULTS.structure
79
89
 
80
90
  // Local reactive state for scene and lattice props. Deeply reactive so nested mutations propagate.
81
91
  // Deep-clone to prevent mutations from leaking to global defaults across component instances.
82
- let scene_props = $state(
83
- structuredClone(DEFAULTS.structure) as typeof DEFAULTS.structure & {
84
- active_sites?: number[]
85
- camera_target?: Vec3
86
- },
87
- )
92
+ let scene_props = $state(structuredClone(DEFAULTS.structure) as SceneProps)
88
93
  let lattice_props = $state({
89
94
  cell_edge_opacity: DEFAULTS.structure.cell_edge_opacity,
90
95
  cell_surface_opacity: DEFAULTS.structure.cell_surface_opacity,
@@ -103,6 +108,8 @@
103
108
  info_pane_open = $bindable(false),
104
109
  enable_measure_mode = $bindable(true),
105
110
  measure_mode = $bindable<MeasureMode>(`distance`),
111
+ bond_edit_mode = $bindable<BondEditMode>(`add`),
112
+ bond_edit_order = $bindable<BondOrder>(1),
106
113
  background_color = $bindable(),
107
114
  background_opacity = $bindable(0.1),
108
115
  show_controls,
@@ -206,6 +213,8 @@
206
213
  enable_info_pane?: boolean
207
214
  enable_measure_mode?: boolean
208
215
  measure_mode?: MeasureMode
216
+ bond_edit_mode?: BondEditMode
217
+ bond_edit_order?: BondOrder
209
218
  info_pane_open?: boolean
210
219
  fullscreen_toggle?: Snippet<[{ fullscreen: boolean }]> | boolean
211
220
  bottom_left?: Snippet<[{ structure?: AnyStructure }]>
@@ -450,11 +459,16 @@
450
459
 
451
460
  let measure_menu_open = $state(false)
452
461
  let export_pane_open = $state(false)
462
+ let focused = $state(false)
453
463
 
454
464
  // Bond customization state
455
465
  let added_bonds = $state<StructureBond[]>([])
456
466
  let removed_bonds = $state<StructureBond[]>([])
457
467
  let bond_order_overrides = $state<StructureBond[]>([])
468
+ let bond_undo_stack = $state<BondEditHistorySnapshot[]>([])
469
+ let bond_redo_stack = $state<BondEditHistorySnapshot[]>([])
470
+ let bond_history_context = $state<BondEditContext>()
471
+ let last_bond_structure_identity = $state(structure)
458
472
  let last_emitted_bond_signature = $state<string>()
459
473
  let bond_edit_snapshot = $state<BondEditSnapshot>()
460
474
  let has_bond_edits = $derived(
@@ -462,25 +476,93 @@
462
476
  bond_order_overrides.length > 0,
463
477
  )
464
478
 
479
+ const clone_bonds = (edit_bonds: StructureBond[]): StructureBond[] =>
480
+ edit_bonds.map((bond) => ({
481
+ ...bond,
482
+ cell_shift: bond.cell_shift && ([...bond.cell_shift] as Vec3),
483
+ }))
484
+
485
+ const snapshot_bond_edits = (): BondEditHistorySnapshot => ({
486
+ added_bonds: clone_bonds(added_bonds),
487
+ removed_bonds: clone_bonds(removed_bonds),
488
+ bond_order_overrides: clone_bonds(bond_order_overrides),
489
+ bond_edit_mode,
490
+ bond_edit_order,
491
+ })
492
+
493
+ function restore_bond_edit_snapshot(snapshot: BondEditHistorySnapshot) {
494
+ added_bonds = clone_bonds(snapshot.added_bonds)
495
+ removed_bonds = clone_bonds(snapshot.removed_bonds)
496
+ bond_order_overrides = clone_bonds(snapshot.bond_order_overrides)
497
+ bond_edit_mode = snapshot.bond_edit_mode
498
+ bond_edit_order = snapshot.bond_edit_order
499
+ clear_selection()
500
+ }
501
+
502
+ function clear_bond_history() {
503
+ bond_undo_stack = []
504
+ bond_redo_stack = []
505
+ bond_history_context = undefined
506
+ }
507
+
508
+ function push_bond_undo() {
509
+ if (bond_undo_stack.length >= MAX_HISTORY) {
510
+ bond_undo_stack.splice(0, bond_undo_stack.length - MAX_HISTORY + 1)
511
+ }
512
+ bond_history_context ??= current_bond_edit_context()
513
+ bond_undo_stack.push(snapshot_bond_edits())
514
+ bond_redo_stack = []
515
+ }
516
+
517
+ function undo_bond_edit() {
518
+ if (bond_undo_stack.length === 0) return
519
+ const restored = bond_undo_stack.pop()
520
+ if (!restored) return
521
+ bond_redo_stack.push(snapshot_bond_edits())
522
+ restore_bond_edit_snapshot(restored)
523
+ }
524
+
525
+ function redo_bond_edit() {
526
+ if (bond_redo_stack.length === 0) return
527
+ const restored = bond_redo_stack.pop()
528
+ if (!restored) return
529
+ bond_undo_stack.push(snapshot_bond_edits())
530
+ restore_bond_edit_snapshot(restored)
531
+ }
532
+
465
533
  function clear_bond_edits() {
466
534
  added_bonds = []
467
535
  removed_bonds = []
468
536
  bond_order_overrides = []
537
+ clear_bond_history()
469
538
  }
470
539
 
471
540
  function emit_bonds(next_bonds: StructureBond[] | undefined) {
472
- const signature = next_bonds === undefined ? `undefined` : JSON.stringify(next_bonds)
541
+ const signature = bond_signature(next_bonds)
473
542
  if (signature === last_emitted_bond_signature) return
474
543
  last_emitted_bond_signature = signature
475
544
  bonds = next_bonds
476
545
  on_bonds_change?.(next_bonds)
477
546
  }
478
547
 
548
+ const bond_signature = (edit_bonds: StructureBond[] | undefined): string =>
549
+ edit_bonds === undefined ? `undefined` : JSON.stringify(edit_bonds)
550
+
479
551
  const current_source_bonds = (): StructureBond[] | undefined =>
480
552
  bonds ?? structure?.properties?.bonds
481
553
 
554
+ const current_source_bond_signature = (): string => {
555
+ const raw_signature = bond_signature(current_source_bonds())
556
+ if (raw_signature !== last_emitted_bond_signature) return raw_signature
557
+ return bond_history_context?.source_bond_signature ??
558
+ (bond_edit_snapshot
559
+ ? bond_signature(bond_edit_snapshot.bonds)
560
+ : raw_signature)
561
+ }
562
+
482
563
  const current_bond_edit_context = (): BondEditContext => ({
483
- structure,
564
+ structure_identity: structure,
565
+ source_bond_signature: current_source_bond_signature(),
484
566
  cell_type,
485
567
  supercell_scaling,
486
568
  show_image_atoms,
@@ -490,7 +572,8 @@
490
572
  previous: BondEditContext,
491
573
  current: BondEditContext,
492
574
  ): boolean =>
493
- previous.structure !== current.structure ||
575
+ previous.structure_identity !== current.structure_identity ||
576
+ previous.source_bond_signature !== current.source_bond_signature ||
494
577
  previous.cell_type !== current.cell_type ||
495
578
  previous.supercell_scaling !== current.supercell_scaling ||
496
579
  previous.show_image_atoms !== current.show_image_atoms
@@ -498,7 +581,30 @@
498
581
  const resolve_bond_edit_reset_bonds = (
499
582
  snapshot: BondEditSnapshot,
500
583
  ): StructureBond[] | undefined =>
501
- snapshot.context.structure === structure ? snapshot.bonds : structure?.properties?.bonds
584
+ snapshot.context.structure_identity === structure
585
+ ? snapshot.bonds
586
+ : structure?.properties?.bonds
587
+
588
+ $effect(() => {
589
+ const next_structure_identity = structure
590
+ untrack(() => {
591
+ if (
592
+ last_bond_structure_identity !== next_structure_identity &&
593
+ bond_signature(bonds) === last_emitted_bond_signature
594
+ ) {
595
+ emit_bonds(structure?.properties?.bonds)
596
+ }
597
+ last_bond_structure_identity = next_structure_identity
598
+ })
599
+ })
600
+
601
+ $effect(() => {
602
+ const history_context = bond_history_context
603
+ if (history_context === undefined) return
604
+ if (bond_edit_context_changed(history_context, current_bond_edit_context())) {
605
+ untrack(clear_bond_history)
606
+ }
607
+ })
502
608
 
503
609
  $effect(() => {
504
610
  const snapshot = bond_edit_snapshot
@@ -544,6 +650,20 @@
544
650
  let add_atom_mode = $state(false)
545
651
  let add_element = $state<ElementSymbol>(`C` as ElementSymbol)
546
652
  let canvas_cursor = $state(`default`)
653
+ let is_measure_selection_mode = $derived(
654
+ measure_mode === `distance` || measure_mode === `angle`,
655
+ )
656
+ let show_measure_selection_limit = $derived(
657
+ is_measure_selection_mode && measured_sites.length >= MAX_SELECTED_SITES,
658
+ )
659
+ let show_selection_reset = $derived(
660
+ has_bond_edits ||
661
+ (is_measure_selection_mode && measured_sites.length > 0) ||
662
+ (measure_mode === `edit-atoms` && selected_sites.length > 0),
663
+ )
664
+ let atom_legend_selected_sites = $derived(
665
+ measure_mode === `edit-atoms` ? selected_sites : [],
666
+ )
547
667
  let change_element_mode = $state(false)
548
668
  let change_element_value = $state(``)
549
669
  // Ephemeral toast message for edit operations
@@ -607,6 +727,7 @@
607
727
  undo_stack = []
608
728
  redo_stack = []
609
729
  }
730
+ if (highlighted_sites.length > 0) highlighted_sites = []
610
731
  if (measure_mode === `edit-atoms`) {
611
732
  if (selected_sites.length > 0 || measured_sites.length > 0) clear_selection()
612
733
  if (site_radius_overrides?.size > 0) site_radius_overrides.clear()
@@ -624,6 +745,16 @@
624
745
  }
625
746
  untrack(() => {
626
747
  if (selected_sites.length > 0 || measured_sites.length > 0) clear_selection()
748
+ if (measure_mode === `edit-bonds`) bond_edit_mode = `add`
749
+ })
750
+ })
751
+
752
+ $effect(() => {
753
+ void bond_edit_mode
754
+ untrack(() => {
755
+ if (measure_mode === `edit-bonds` && (selected_sites.length > 0 || measured_sites.length > 0)) {
756
+ clear_selection()
757
+ }
627
758
  })
628
759
  })
629
760
 
@@ -633,6 +764,7 @@
633
764
  untrack(() => {
634
765
  // Clear bond edits from edit-bonds mode to avoid stale state
635
766
  if (has_bond_edits) clear_bond_edits()
767
+ else clear_bond_history()
636
768
  if (cell_type !== `original` && cell_transformed_structure && structure) {
637
769
  // Bake the transformed cell: push original to undo, replace structure
638
770
  is_internal_edit = true
@@ -644,6 +776,8 @@
644
776
  })
645
777
 
646
778
  let controls_config = $derived(normalize_show_controls(show_controls))
779
+ let viewer_active = $derived(hovered || focused)
780
+ let scene_gizmo = $derived(viewer_active && (scene_props.gizmo ?? scene_props.show_gizmo))
647
781
  let active_scene_sites = $derived([
648
782
  ...new SvelteSet([...(scene_props.active_sites ?? []), ...highlighted_sites]),
649
783
  ])
@@ -1007,9 +1141,13 @@
1007
1141
 
1008
1142
  function handle_keydown(event: KeyboardEvent) {
1009
1143
  // Don't handle shortcuts if user is typing in an input field
1010
- const target = event.target as HTMLElement
1011
- const is_input_focused = target.tagName === `INPUT` ||
1012
- target.tagName === `TEXTAREA`
1144
+ const target = event.target
1145
+ const is_input_focused =
1146
+ target instanceof HTMLElement &&
1147
+ (target.tagName === `INPUT` ||
1148
+ target.tagName === `TEXTAREA` ||
1149
+ target.tagName === `SELECT` ||
1150
+ target.isContentEditable)
1013
1151
 
1014
1152
  // Allow Escape to cancel add-atom mode even when the element input is focused
1015
1153
  if (event.key === `Escape` && measure_mode === `edit-atoms` && add_atom_mode) {
@@ -1020,17 +1158,54 @@
1020
1158
 
1021
1159
  if (is_input_focused) return
1022
1160
 
1161
+ if (measure_mode === `edit-bonds`) {
1162
+ const key = event.key.toLowerCase()
1163
+ const plain = !event.ctrlKey && !event.metaKey && !event.altKey
1164
+ if (event.ctrlKey || event.metaKey) {
1165
+ if (key === `z` && !event.shiftKey) {
1166
+ if (bond_undo_stack.length === 0) return
1167
+ event.preventDefault()
1168
+ undo_bond_edit()
1169
+ show_toast(`Undo bond edit (${bond_undo_stack.length} left)`)
1170
+ return
1171
+ } else if (key === `y` || (key === `z` && event.shiftKey)) {
1172
+ if (bond_redo_stack.length === 0) return
1173
+ event.preventDefault()
1174
+ redo_bond_edit()
1175
+ show_toast(`Redo bond edit (${bond_redo_stack.length} left)`)
1176
+ return
1177
+ }
1178
+ }
1179
+ if (key === `a` && plain) {
1180
+ event.preventDefault()
1181
+ bond_edit_mode = `add`
1182
+ return
1183
+ }
1184
+ if (key === `d` && plain) {
1185
+ event.preventDefault()
1186
+ bond_edit_mode = `delete`
1187
+ return
1188
+ }
1189
+ if (event.key === `Escape` && selected_sites.length > 0) {
1190
+ event.preventDefault()
1191
+ clear_selection()
1192
+ return
1193
+ }
1194
+ }
1195
+
1023
1196
  // Edit-atoms mode shortcuts (including undo/redo)
1024
1197
  if (measure_mode === `edit-atoms`) {
1025
1198
  // Undo/redo shortcuts (Ctrl/Cmd + Z/Y) — only active in edit-atoms mode
1026
1199
  if (event.ctrlKey || event.metaKey) {
1027
1200
  const key = event.key.toLowerCase()
1028
1201
  if (key === `z` && !event.shiftKey) {
1202
+ if (undo_stack.length === 0) return
1029
1203
  event.preventDefault()
1030
1204
  undo()
1031
1205
  show_toast(`Undo (${undo_stack.length} left)`)
1032
1206
  return
1033
1207
  } else if (key === `y` || (key === `z` && event.shiftKey)) {
1208
+ if (redo_stack.length === 0) return
1034
1209
  event.preventDefault()
1035
1210
  redo()
1036
1211
  show_toast(`Redo (${redo_stack.length} left)`)
@@ -1263,7 +1438,7 @@
1263
1438
  properties: {},
1264
1439
  }],
1265
1440
  }
1266
- show_toast(`Added ${elem} at (${xyz.map((c) => c.toFixed(2)).join(`, `)})`)
1441
+ show_toast(`Added ${elem} at (${xyz.map((coord) => coord.toFixed(2)).join(`, `)})`)
1267
1442
  }
1268
1443
 
1269
1444
  // Only set background override when background_color is explicitly provided
@@ -1306,6 +1481,7 @@
1306
1481
  <div
1307
1482
  class:dragover
1308
1483
  class:active={info_pane_open || controls_open || export_pane_open}
1484
+ class:gizmo-visible={Boolean(scene_gizmo)}
1309
1485
  role="application"
1310
1486
  tabindex="0"
1311
1487
  style:--canvas-cursor={canvas_cursor}
@@ -1315,8 +1491,15 @@
1315
1491
  bind:clientHeight={height}
1316
1492
  onmouseenter={() => (hovered = true)}
1317
1493
  onmouseleave={() => (hovered = false)}
1494
+ onfocusin={() => (focused = true)}
1495
+ onfocusout={(event) => {
1496
+ if (!(event.relatedTarget instanceof Node) || !wrapper?.contains(event.relatedTarget)) {
1497
+ focused = false
1498
+ }
1499
+ }}
1318
1500
  ondblclick={(event) => {
1319
- const target = event.target as HTMLElement
1501
+ const target = event.target
1502
+ if (!(target instanceof HTMLElement)) return
1320
1503
  // Don't reset if double-click was on UI controls/panes/legend
1321
1504
  if (
1322
1505
  target.closest(`.control-buttons`) ||
@@ -1398,7 +1581,7 @@
1398
1581
  aria-expanded={measure_menu_open}
1399
1582
  style="transform: scale(1.2)"
1400
1583
  >
1401
- {#if (measured_sites?.length ?? 0) >= MAX_SELECTED_SITES}
1584
+ {#if show_measure_selection_limit}
1402
1585
  <span class="selection-limit-text">
1403
1586
  {measured_sites.length}/{MAX_SELECTED_SITES}
1404
1587
  </span>
@@ -1417,7 +1600,7 @@
1417
1600
  style="margin-left: -2px"
1418
1601
  />
1419
1602
  </button>
1420
- {#if (measured_sites?.length ?? 0) > 0 || has_bond_edits}
1603
+ {#if show_selection_reset}
1421
1604
  <button
1422
1605
  type="button"
1423
1606
  aria-label="Reset selection and bond edits"
@@ -1463,7 +1646,7 @@
1463
1646
  }}
1464
1647
  >
1465
1648
  <Icon {icon} style="transform: scale({scale})" />
1466
- <span>{@html sanitize_html(label)}</span>
1649
+ <span>{label}</span>
1467
1650
  </button>
1468
1651
  {/each}
1469
1652
  </div>
@@ -1475,10 +1658,10 @@
1475
1658
  <div class="undo-redo-container">
1476
1659
  <button
1477
1660
  type="button"
1478
- aria-label="Undo (Ctrl+Z)"
1661
+ aria-label="Undo (Cmd/Ctrl+Z)"
1479
1662
  disabled={undo_stack.length === 0}
1480
1663
  onclick={undo}
1481
- title="Undo (Ctrl+Z)"
1664
+ title="Undo (Cmd/Ctrl+Z)"
1482
1665
  class="undo-redo-btn"
1483
1666
  >
1484
1667
  <Icon icon="Undo" />
@@ -1488,10 +1671,10 @@
1488
1671
  </button>
1489
1672
  <button
1490
1673
  type="button"
1491
- aria-label="Redo (Ctrl+Y)"
1674
+ aria-label="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1492
1675
  disabled={redo_stack.length === 0}
1493
1676
  onclick={redo}
1494
- title="Redo (Ctrl+Y)"
1677
+ title="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1495
1678
  class="undo-redo-btn"
1496
1679
  >
1497
1680
  <Icon icon="Redo" />
@@ -1502,6 +1685,65 @@
1502
1685
  </div>
1503
1686
  {/if}
1504
1687
 
1688
+ {#if measure_mode === `edit-bonds`}
1689
+ <div class="bond-edit-toolbar" aria-label="Bond editing controls">
1690
+ {#if bond_edit_mode === `add`}
1691
+ <label>
1692
+ <span>Bond order</span>
1693
+ <select bind:value={bond_edit_order}>
1694
+ {#each BOND_ORDER_OPTIONS as { order, label } (label)}
1695
+ <option value={order}>{label}</option>
1696
+ {/each}
1697
+ </select>
1698
+ </label>
1699
+ {/if}
1700
+ <div class="bond-edit-mode-toggle">
1701
+ {#each [
1702
+ { mode: `add`, label: `Add`, title: `Add: click two atoms` },
1703
+ { mode: `delete`, label: `Delete`, title: `Delete: click a bond` },
1704
+ ] as const as { mode, label, title } (mode)}
1705
+ <button
1706
+ type="button"
1707
+ class:selected={bond_edit_mode === mode}
1708
+ aria-pressed={bond_edit_mode === mode}
1709
+ title="{title} ({label[0]})"
1710
+ onclick={() => (bond_edit_mode = mode)}
1711
+ >
1712
+ {label}
1713
+ </button>
1714
+ {/each}
1715
+ </div>
1716
+ </div>
1717
+ <div class="undo-redo-container">
1718
+ <button
1719
+ type="button"
1720
+ aria-label="Undo bond edit (Cmd/Ctrl+Z)"
1721
+ disabled={bond_undo_stack.length === 0}
1722
+ onclick={undo_bond_edit}
1723
+ title="Undo bond edit (Cmd/Ctrl+Z)"
1724
+ class="undo-redo-btn"
1725
+ >
1726
+ <Icon icon="Undo" />
1727
+ {#if bond_undo_stack.length > 0}
1728
+ <span class="history-count">{bond_undo_stack.length}</span>
1729
+ {/if}
1730
+ </button>
1731
+ <button
1732
+ type="button"
1733
+ aria-label="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1734
+ disabled={bond_redo_stack.length === 0}
1735
+ onclick={redo_bond_edit}
1736
+ title="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1737
+ class="undo-redo-btn"
1738
+ >
1739
+ <Icon icon="Redo" />
1740
+ {#if bond_redo_stack.length > 0}
1741
+ <span class="history-count">{bond_redo_stack.length}</span>
1742
+ {/if}
1743
+ </button>
1744
+ </div>
1745
+ {/if}
1746
+
1505
1747
  <!-- Add-atom element input (shown when add_atom_mode is active) -->
1506
1748
  {#if measure_mode === `edit-atoms` && add_atom_mode}
1507
1749
  <div class="add-atom-input">
@@ -1608,8 +1850,9 @@
1608
1850
  bind:element_mapping
1609
1851
  bind:element_radius_overrides
1610
1852
  bind:site_radius_overrides
1611
- {selected_sites}
1853
+ selected_sites={atom_legend_selected_sites}
1612
1854
  structure={displayed_structure}
1855
+ show_mode_toggle={viewer_active}
1613
1856
  {sym_data}
1614
1857
  >
1615
1858
  {#if structure && `lattice` in structure}
@@ -1632,6 +1875,7 @@
1632
1875
  structure={displayed_structure}
1633
1876
  base_structure={cell_transformed_structure}
1634
1877
  {...scene_props}
1878
+ gizmo={scene_gizmo}
1635
1879
  {lattice_props}
1636
1880
  volumetric_data={supercell_volume}
1637
1881
  {isosurface_settings}
@@ -1653,6 +1897,8 @@
1653
1897
  bind:removed_bonds
1654
1898
  bind:bond_order_overrides
1655
1899
  {bond_edits_enabled}
1900
+ bind:bond_edit_mode
1901
+ {bond_edit_order}
1656
1902
  {measure_mode}
1657
1903
  {width}
1658
1904
  {height}
@@ -1660,6 +1906,7 @@
1660
1906
  {sym_data}
1661
1907
  on_sites_moved={handle_sites_moved}
1662
1908
  on_operation_start={push_undo}
1909
+ on_bond_edit_start={push_bond_undo}
1663
1910
  on_add_atom={handle_add_atom}
1664
1911
  bind:add_atom_mode
1665
1912
  bind:add_element
@@ -1678,20 +1925,6 @@
1678
1925
  <div class="edit-toast">{toast_msg}</div>
1679
1926
  {/if}
1680
1927
 
1681
- {#if measure_mode === `edit-bonds` && has_bond_edits}
1682
- <div class="bond-edit-status">
1683
- {#if added_bonds.length > 0}
1684
- <span class="added">+{added_bonds.length} added</span>
1685
- {/if}
1686
- {#if removed_bonds.length > 0}
1687
- <span class="removed">-{removed_bonds.length} removed</span>
1688
- {/if}
1689
- {#if bond_order_overrides.length > 0}
1690
- <span class="changed">{bond_order_overrides.length} ordered</span>
1691
- {/if}
1692
- </div>
1693
- {/if}
1694
-
1695
1928
  {#if symmetry_error}
1696
1929
  <div class="symmetry-error">
1697
1930
  <span>{symmetry_error}</span>
@@ -1740,6 +1973,11 @@
1740
1973
  background: transparent;
1741
1974
  cursor: var(--canvas-cursor, default);
1742
1975
  }
1976
+ .structure:not(.gizmo-visible) :global(.responsive-gizmo) {
1977
+ opacity: 0;
1978
+ pointer-events: none;
1979
+ visibility: hidden;
1980
+ }
1743
1981
  /* Avoid accidental text selection while interacting with the viewer */
1744
1982
  .structure :global(canvas),
1745
1983
  .structure section.control-buttons,
@@ -1906,34 +2144,6 @@
1906
2144
  opacity: 0;
1907
2145
  }
1908
2146
  }
1909
- .bond-edit-status {
1910
- position: absolute;
1911
- bottom: 1rem;
1912
- left: 50%;
1913
- transform: translateX(-50%);
1914
- background: color-mix(in srgb, var(--page-bg, Canvas) 85%, currentColor);
1915
- color: var(--text-color, currentColor);
1916
- padding: 0.5rem 1rem;
1917
- border-radius: var(--border-radius, 3pt);
1918
- font-size: 0.85rem;
1919
- display: flex;
1920
- gap: 0.75rem;
1921
- z-index: 100;
1922
- pointer-events: none;
1923
- box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
1924
- }
1925
- .bond-edit-status .added {
1926
- color: #4caf50;
1927
- font-weight: bold;
1928
- }
1929
- .bond-edit-status .removed {
1930
- color: #f44336;
1931
- font-weight: bold;
1932
- }
1933
- .bond-edit-status .changed {
1934
- color: var(--accent-color, #007acc);
1935
- font-weight: bold;
1936
- }
1937
2147
  /* CellSelect: position at left of legend, show on hover */
1938
2148
  .structure :global(.cell-select) {
1939
2149
  order: -1; /* Move to left side of AtomLegend flex container */
@@ -1954,6 +2164,45 @@
1954
2164
  align-items: center;
1955
2165
  justify-content: center;
1956
2166
  }
2167
+ .bond-edit-toolbar {
2168
+ --bond-edit-control-height: 1.8em;
2169
+ display: flex;
2170
+ align-items: center;
2171
+ gap: 0.4em;
2172
+ font-size: 0.8em;
2173
+ }
2174
+ .bond-edit-mode-toggle,
2175
+ .bond-edit-toolbar label {
2176
+ display: flex;
2177
+ align-items: center;
2178
+ }
2179
+ .bond-edit-mode-toggle {
2180
+ gap: 0.35em;
2181
+ }
2182
+ .bond-edit-mode-toggle button,
2183
+ .bond-edit-toolbar label,
2184
+ .bond-edit-toolbar select {
2185
+ height: var(--bond-edit-control-height);
2186
+ line-height: 1;
2187
+ }
2188
+ .bond-edit-mode-toggle button {
2189
+ min-width: 3.5em;
2190
+ font: inherit;
2191
+ }
2192
+ .bond-edit-mode-toggle button.selected {
2193
+ background: var(--accent-color, #007acc);
2194
+ color: white;
2195
+ }
2196
+ .bond-edit-mode-toggle button.selected:hover {
2197
+ background-color: color-mix(in srgb, var(--accent-color, #007acc) 70%, black);
2198
+ }
2199
+ .bond-edit-toolbar label {
2200
+ gap: 0.25em;
2201
+ }
2202
+ .bond-edit-toolbar select {
2203
+ max-width: 8em;
2204
+ font: inherit;
2205
+ }
1957
2206
  .history-count {
1958
2207
  position: absolute;
1959
2208
  bottom: -2px;
@@ -2,7 +2,7 @@ import type { ShowControlsProp } from '../controls';
2
2
  import type { ElementSymbol } from '../element';
3
3
  import Spinner from '../feedback/Spinner.svelte';
4
4
  import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types';
5
- import type { AnyStructure, MeasureMode, StructureBond } from './';
5
+ import type { AnyStructure, BondEditMode, BondOrder, MeasureMode, StructureBond } from './';
6
6
  import type { CellType, SymmetrySettings } from '../symmetry';
7
7
  import type { MoyoDataset } from '@spglib/moyo-wasm';
8
8
  import type { ComponentProps, Snippet } from 'svelte';
@@ -39,6 +39,8 @@ type $$ComponentProps = {
39
39
  enable_info_pane?: boolean;
40
40
  enable_measure_mode?: boolean;
41
41
  measure_mode?: MeasureMode;
42
+ bond_edit_mode?: BondEditMode;
43
+ bond_edit_order?: BondOrder;
42
44
  info_pane_open?: boolean;
43
45
  fullscreen_toggle?: Snippet<[{
44
46
  fullscreen: boolean;
@@ -82,6 +84,6 @@ type $$ComponentProps = {
82
84
  on_camera_reset?: EventHandler;
83
85
  on_bonds_change?: (bonds: StructureBond[] | undefined) => void;
84
86
  } & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`> & Omit<HTMLAttributes<HTMLDivElement>, `children`>;
85
- declare const Structure: import("svelte").Component<$$ComponentProps, {}, "structure" | "height" | "width" | "dragover" | "fullscreen" | "hovered" | "controls_open" | "loading" | "color_scheme" | "background_color" | "background_opacity" | "show_image_atoms" | "info_pane_open" | "wrapper" | "sym_data" | "png_dpi" | "error_msg" | "site_radius_overrides" | "atom_color_config" | "lattice_props" | "measure_mode" | "selected_sites" | "measured_sites" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "volumetric_data" | "isosurface_settings" | "element_mapping" | "bonds" | "supercell_scaling" | "cell_type" | "active_volume_idx" | "scene_props" | "highlighted_sites" | "hovered_site_idx" | "enable_measure_mode" | "performance_mode" | "displayed_structure" | "symmetry_settings">;
87
+ declare const Structure: import("svelte").Component<$$ComponentProps, {}, "structure" | "height" | "width" | "dragover" | "loading" | "fullscreen" | "hovered" | "controls_open" | "color_scheme" | "background_color" | "background_opacity" | "show_image_atoms" | "info_pane_open" | "wrapper" | "sym_data" | "png_dpi" | "error_msg" | "site_radius_overrides" | "atom_color_config" | "lattice_props" | "measure_mode" | "selected_sites" | "measured_sites" | "bond_edit_mode" | "bond_edit_order" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "volumetric_data" | "isosurface_settings" | "element_mapping" | "bonds" | "supercell_scaling" | "cell_type" | "active_volume_idx" | "scene_props" | "highlighted_sites" | "hovered_site_idx" | "enable_measure_mode" | "performance_mode" | "displayed_structure" | "symmetry_settings">;
86
88
  type Structure = ReturnType<typeof Structure>;
87
89
  export default Structure;