matterviz 0.3.4 → 0.3.5

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 (125) hide show
  1. package/dist/FilePicker.svelte +1 -1
  2. package/dist/app.css +7 -0
  3. package/dist/brillouin/BrillouinZone.svelte +5 -2
  4. package/dist/brillouin/compute.js +8 -4
  5. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +6 -6
  6. package/dist/chempot-diagram/async-compute.svelte.js +5 -4
  7. package/dist/chempot-diagram/chempot-worker.js +2 -2
  8. package/dist/chempot-diagram/compute.js +16 -16
  9. package/dist/composition/FormulaFilter.svelte +3 -3
  10. package/dist/constants.js +2 -8
  11. package/dist/convex-hull/ConvexHull.svelte +2 -2
  12. package/dist/convex-hull/ConvexHull2D.svelte +11 -10
  13. package/dist/convex-hull/ConvexHull3D.svelte +16 -14
  14. package/dist/convex-hull/ConvexHull4D.svelte +26 -14
  15. package/dist/convex-hull/ConvexHullControls.svelte +1 -1
  16. package/dist/convex-hull/ConvexHullInfoPane.svelte +68 -61
  17. package/dist/convex-hull/ConvexHullStats.svelte +23 -6
  18. package/dist/convex-hull/GasPressureControls.svelte +3 -3
  19. package/dist/convex-hull/TemperatureSlider.svelte +1 -1
  20. package/dist/convex-hull/barycentric-coords.js +2 -2
  21. package/dist/convex-hull/helpers.js +45 -27
  22. package/dist/convex-hull/thermodynamics.js +2 -2
  23. package/dist/element/BohrAtom.svelte +25 -27
  24. package/dist/element/BohrAtom.svelte.d.ts +2 -2
  25. package/dist/element/data.d.ts +2 -3
  26. package/dist/fermi-surface/FermiSurface.svelte +5 -2
  27. package/dist/fermi-surface/compute.js +3 -3
  28. package/dist/fermi-surface/parse.js +2 -2
  29. package/dist/fermi-surface/symmetry.js +1 -1
  30. package/dist/heatmap-matrix/HeatmapMatrix.svelte +8 -8
  31. package/dist/icons.d.ts +6 -6
  32. package/dist/icons.js +6 -6
  33. package/dist/io/decompress.js +12 -7
  34. package/dist/io/export.js +20 -16
  35. package/dist/io/is-binary.js +19 -4
  36. package/dist/isosurface/parse.js +8 -8
  37. package/dist/isosurface/types.js +9 -9
  38. package/dist/layout/InfoTag.svelte +1 -1
  39. package/dist/layout/json-tree/JsonNode.svelte +1 -0
  40. package/dist/layout/json-tree/utils.js +2 -1
  41. package/dist/marching-cubes.js +1 -1
  42. package/dist/math.js +1 -1
  43. package/dist/overlays/CopyButton.svelte +45 -0
  44. package/dist/overlays/CopyButton.svelte.d.ts +8 -0
  45. package/dist/overlays/InfoPaneCards.svelte +149 -0
  46. package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
  47. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +33 -35
  48. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +2 -2
  49. package/dist/phase-diagram/PhaseDiagramControls.svelte +27 -29
  50. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +2 -2
  51. package/dist/phase-diagram/parse.js +3 -3
  52. package/dist/phase-diagram/svg-to-diagram.js +10 -12
  53. package/dist/plot/BarPlot.svelte +24 -15
  54. package/dist/plot/BarPlot.svelte.d.ts +3 -2
  55. package/dist/plot/FillArea.svelte +2 -3
  56. package/dist/plot/FillArea.svelte.d.ts +3 -2
  57. package/dist/plot/Histogram.svelte +37 -19
  58. package/dist/plot/Line.svelte +2 -3
  59. package/dist/plot/Line.svelte.d.ts +2 -2
  60. package/dist/plot/PlotLegend.svelte +79 -8
  61. package/dist/plot/PlotLegend.svelte.d.ts +4 -0
  62. package/dist/plot/PortalSelect.svelte +5 -5
  63. package/dist/plot/ScatterPlot.svelte +47 -33
  64. package/dist/plot/ScatterPlot.svelte.d.ts +5 -4
  65. package/dist/plot/ScatterPlot3D.svelte +6 -3
  66. package/dist/plot/ScatterPoint.svelte +10 -4
  67. package/dist/plot/ScatterPoint.svelte.d.ts +4 -2
  68. package/dist/plot/SpacegroupBarPlot.svelte +5 -4
  69. package/dist/plot/data-cleaning.js +9 -9
  70. package/dist/plot/index.d.ts +0 -6
  71. package/dist/plot/scales.d.ts +3 -3
  72. package/dist/plot/scales.js +29 -29
  73. package/dist/plot/types.d.ts +5 -9
  74. package/dist/rdf/calc-rdf.js +1 -1
  75. package/dist/sanitize.js +22 -15
  76. package/dist/settings.d.ts +2 -0
  77. package/dist/settings.js +12 -3
  78. package/dist/spectral/Bands.svelte +6 -6
  79. package/dist/spectral/BandsAndDos.svelte +4 -4
  80. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  81. package/dist/spectral/Dos.svelte +2 -2
  82. package/dist/spectral/helpers.js +1 -1
  83. package/dist/structure/AtomLegend.svelte +4 -4
  84. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  85. package/dist/structure/Cylinder.svelte +7 -7
  86. package/dist/structure/Structure.svelte +169 -27
  87. package/dist/structure/Structure.svelte.d.ts +6 -2
  88. package/dist/structure/StructureControls.svelte +130 -16
  89. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  90. package/dist/structure/StructureInfoPane.svelte +519 -218
  91. package/dist/structure/StructureInfoPane.svelte.d.ts +2 -1
  92. package/dist/structure/StructureScene.svelte +399 -68
  93. package/dist/structure/StructureScene.svelte.d.ts +8 -4
  94. package/dist/structure/atom-properties.js +3 -1
  95. package/dist/structure/bond-order-perception.d.ts +13 -0
  96. package/dist/structure/bond-order-perception.js +367 -0
  97. package/dist/structure/bonding.d.ts +10 -1
  98. package/dist/structure/bonding.js +232 -11
  99. package/dist/structure/export.js +6 -4
  100. package/dist/structure/index.d.ts +19 -4
  101. package/dist/structure/index.js +3 -0
  102. package/dist/structure/label-placement.d.ts +14 -0
  103. package/dist/structure/label-placement.js +72 -0
  104. package/dist/structure/parse.d.ts +2 -1
  105. package/dist/structure/parse.js +25 -36
  106. package/dist/structure/supercell.js +35 -2
  107. package/dist/symmetry/SymmetryStats.svelte +1 -1
  108. package/dist/symmetry/cell-transform.js +15 -1
  109. package/dist/symmetry/index.js +3 -3
  110. package/dist/table/HeatmapTable.svelte +3 -3
  111. package/dist/table/ToggleMenu.svelte +1 -1
  112. package/dist/trajectory/Trajectory.svelte +2 -2
  113. package/dist/trajectory/TrajectoryInfoPane.svelte +14 -88
  114. package/dist/trajectory/extract.js +4 -4
  115. package/dist/trajectory/frame-reader.js +2 -2
  116. package/dist/trajectory/parse/ase.js +2 -6
  117. package/dist/trajectory/parse/hdf5.js +1 -3
  118. package/dist/trajectory/plotting.js +1 -1
  119. package/dist/utils.js +1 -1
  120. package/dist/xrd/calc-xrd.js +1 -1
  121. package/package.json +22 -37
  122. package/dist/structure/ferrox-wasm-types.d.ts +0 -46
  123. package/dist/structure/ferrox-wasm-types.js +0 -18
  124. package/dist/structure/ferrox-wasm.d.ts +0 -94
  125. package/dist/structure/ferrox-wasm.js +0 -249
@@ -22,7 +22,12 @@
22
22
  import { DEFAULTS } from '../settings'
23
23
  import { sanitize_html } from '../sanitize'
24
24
  import { colors } from '../state.svelte'
25
- import type { AnyStructure, Crystal, MeasureMode } from './'
25
+ import type {
26
+ AnyStructure,
27
+ Crystal,
28
+ MeasureMode,
29
+ StructureBond,
30
+ } from './'
26
31
  import {
27
32
  default_vector_configs,
28
33
  get_element_counts,
@@ -42,7 +47,7 @@
42
47
  import { Canvas } from '@threlte/core'
43
48
  import type { ComponentProps, Snippet } from 'svelte'
44
49
  import { untrack } from 'svelte'
45
- import { click_outside, tooltip } from 'svelte-multiselect'
50
+ import { click_outside, tooltip } from 'svelte-multiselect/attachments'
46
51
  import type { HTMLAttributes } from 'svelte/elements'
47
52
  import { SvelteMap, SvelteSet } from 'svelte/reactivity'
48
53
  import type { Camera, OrthographicCamera, Scene } from 'three'
@@ -50,6 +55,7 @@
50
55
  import { get_property_colors } from './atom-properties'
51
56
  import AtomLegend from './AtomLegend.svelte'
52
57
  import CellSelect from './CellSelect.svelte'
58
+ import { merge_bond_edits } from './bonding'
53
59
  import type { StructureHandlerData } from './index'
54
60
  import { MAX_SELECTED_SITES } from './measure'
55
61
  import { normalize_fractional_coords, parse_any_structure } from './parse'
@@ -60,11 +66,22 @@
60
66
 
61
67
  // Type alias for event handlers to reduce verbosity
62
68
  type EventHandler = (data: StructureHandlerData) => void
69
+ type BondEditContext = {
70
+ structure?: AnyStructure
71
+ cell_type: CellType
72
+ supercell_scaling: string
73
+ show_image_atoms: boolean
74
+ }
75
+ type BondEditSnapshot = {
76
+ bonds: StructureBond[] | undefined
77
+ context: BondEditContext
78
+ }
63
79
 
64
80
  // Local reactive state for scene and lattice props. Deeply reactive so nested mutations propagate.
65
81
  // Deep-clone to prevent mutations from leaking to global defaults across component instances.
66
82
  let scene_props = $state(
67
83
  structuredClone(DEFAULTS.structure) as typeof DEFAULTS.structure & {
84
+ active_sites?: number[]
68
85
  camera_target?: Vec3
69
86
  },
70
87
  )
@@ -79,6 +96,7 @@
79
96
 
80
97
  let {
81
98
  structure = $bindable(),
99
+ bonds = $bindable(),
82
100
  scene_props: scene_props_in = $bindable(),
83
101
  lattice_props: lattice_props_in = $bindable(),
84
102
  controls_open = $bindable(false),
@@ -117,6 +135,8 @@
117
135
  performance_mode = $bindable(`quality`),
118
136
  // expose selected site indices for external control/highlighting
119
137
  selected_sites = $bindable([]),
138
+ highlighted_sites = $bindable([]),
139
+ hovered_site_idx = $bindable(null),
120
140
  // expose measured site indices for overlays/labels
121
141
  measured_sites = $bindable([]),
122
142
  // expose the displayed structure (with image atoms and supercell) for external use
@@ -153,10 +173,12 @@
153
173
  on_fullscreen_change,
154
174
  on_camera_move,
155
175
  on_camera_reset,
176
+ on_bonds_change,
156
177
  ...rest
157
178
  }:
158
179
  & {
159
180
  structure?: AnyStructure
181
+ bonds?: StructureBond[]
160
182
  scene_props?: ComponentProps<typeof StructureScene>
161
183
  /**
162
184
  * Controls visibility configuration.
@@ -199,6 +221,8 @@
199
221
  performance_mode?: `quality` | `speed`
200
222
  // allow parent components to control highlighted/selected site indices
201
223
  selected_sites?: number[]
224
+ highlighted_sites?: number[]
225
+ hovered_site_idx?: number | null
202
226
  // explicit measured sites for distance/angle overlays
203
227
  measured_sites?: number[]
204
228
  // expose the displayed structure (with image atoms and/or supercell) for external use
@@ -236,6 +260,7 @@
236
260
  on_fullscreen_change?: EventHandler
237
261
  on_camera_move?: EventHandler
238
262
  on_camera_reset?: EventHandler
263
+ on_bonds_change?: (bonds: StructureBond[] | undefined) => void
239
264
  }
240
265
  & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`>
241
266
  & Omit<HTMLAttributes<HTMLDivElement>, `children`> = $props()
@@ -427,8 +452,85 @@
427
452
  let export_pane_open = $state(false)
428
453
 
429
454
  // Bond customization state
430
- let added_bonds = $state<[number, number][]>([])
431
- let removed_bonds = $state<[number, number][]>([])
455
+ let added_bonds = $state<StructureBond[]>([])
456
+ let removed_bonds = $state<StructureBond[]>([])
457
+ let bond_order_overrides = $state<StructureBond[]>([])
458
+ let last_emitted_bond_signature = $state<string>()
459
+ let bond_edit_snapshot = $state<BondEditSnapshot>()
460
+ let has_bond_edits = $derived(
461
+ added_bonds.length > 0 || removed_bonds.length > 0 ||
462
+ bond_order_overrides.length > 0,
463
+ )
464
+
465
+ function clear_bond_edits() {
466
+ added_bonds = []
467
+ removed_bonds = []
468
+ bond_order_overrides = []
469
+ }
470
+
471
+ function emit_bonds(next_bonds: StructureBond[] | undefined) {
472
+ const signature = next_bonds === undefined ? `undefined` : JSON.stringify(next_bonds)
473
+ if (signature === last_emitted_bond_signature) return
474
+ last_emitted_bond_signature = signature
475
+ bonds = next_bonds
476
+ on_bonds_change?.(next_bonds)
477
+ }
478
+
479
+ const current_source_bonds = (): StructureBond[] | undefined =>
480
+ bonds ?? structure?.properties?.bonds
481
+
482
+ const current_bond_edit_context = (): BondEditContext => ({
483
+ structure,
484
+ cell_type,
485
+ supercell_scaling,
486
+ show_image_atoms,
487
+ })
488
+
489
+ const bond_edit_context_changed = (
490
+ previous: BondEditContext,
491
+ current: BondEditContext,
492
+ ): boolean =>
493
+ previous.structure !== current.structure ||
494
+ previous.cell_type !== current.cell_type ||
495
+ previous.supercell_scaling !== current.supercell_scaling ||
496
+ previous.show_image_atoms !== current.show_image_atoms
497
+
498
+ const resolve_bond_edit_reset_bonds = (
499
+ snapshot: BondEditSnapshot,
500
+ ): StructureBond[] | undefined =>
501
+ snapshot.context.structure === structure ? snapshot.bonds : structure?.properties?.bonds
502
+
503
+ $effect(() => {
504
+ const snapshot = bond_edit_snapshot
505
+ if (snapshot === undefined) return
506
+ const context = current_bond_edit_context()
507
+ if (!bond_edit_context_changed(snapshot.context, context)) return
508
+ untrack(() => {
509
+ emit_bonds(resolve_bond_edit_reset_bonds(snapshot))
510
+ clear_bond_edits()
511
+ bond_edit_snapshot = undefined
512
+ })
513
+ })
514
+
515
+ $effect(() => {
516
+ if (!has_bond_edits) {
517
+ if (bond_edit_snapshot === undefined) return
518
+ emit_bonds(resolve_bond_edit_reset_bonds(bond_edit_snapshot))
519
+ bond_edit_snapshot = undefined
520
+ return
521
+ }
522
+ bond_edit_snapshot ??= {
523
+ bonds: current_source_bonds(),
524
+ context: current_bond_edit_context(),
525
+ }
526
+ const edited_bonds = merge_bond_edits(
527
+ bond_edit_snapshot.bonds ?? [],
528
+ added_bonds,
529
+ removed_bonds,
530
+ bond_order_overrides,
531
+ )
532
+ emit_bonds(edited_bonds)
533
+ })
432
534
 
433
535
  // === Edit-atoms mode state ===
434
536
  let dragging_atoms = $state(false)
@@ -530,10 +632,7 @@
530
632
  if (measure_mode !== `edit-atoms`) return
531
633
  untrack(() => {
532
634
  // Clear bond edits from edit-bonds mode to avoid stale state
533
- if (added_bonds.length > 0 || removed_bonds.length > 0) {
534
- added_bonds = []
535
- removed_bonds = []
536
- }
635
+ if (has_bond_edits) clear_bond_edits()
537
636
  if (cell_type !== `original` && cell_transformed_structure && structure) {
538
637
  // Bake the transformed cell: push original to undo, replace structure
539
638
  is_internal_edit = true
@@ -545,6 +644,9 @@
545
644
  })
546
645
 
547
646
  let controls_config = $derived(normalize_show_controls(show_controls))
647
+ let active_scene_sites = $derived([
648
+ ...new SvelteSet([...(scene_props.active_sites ?? []), ...highlighted_sites]),
649
+ ])
548
650
 
549
651
  // Normalize structure coordinates: wrap fractional coords to [0,1) and recompute Cartesian
550
652
  // This ensures atoms are rendered inside the unit cell regardless of data source
@@ -553,24 +655,32 @@
553
655
  return normalize_fractional_coords(structure) as AnyStructure
554
656
  })
555
657
 
658
+ let structure_with_bonds = $derived.by(() => {
659
+ if (!normalized_structure || bonds === undefined) return normalized_structure
660
+ return {
661
+ ...normalized_structure,
662
+ properties: { ...normalized_structure.properties, bonds },
663
+ } as AnyStructure
664
+ })
665
+
556
666
  // Apply cell type transformation (original, conventional, or primitive)
557
667
  // This must happen BEFORE supercell transformation
558
668
  let cell_transformed_structure = $derived.by(() => {
559
669
  if (
560
- !normalized_structure || !(`lattice` in normalized_structure) ||
670
+ !structure_with_bonds || !(`lattice` in structure_with_bonds) ||
561
671
  cell_type === `original`
562
672
  ) {
563
- return normalized_structure
673
+ return structure_with_bonds
564
674
  }
565
675
  // Cell type transformation requires symmetry data
566
676
  if (!sym_data) {
567
- return normalized_structure
677
+ return structure_with_bonds
568
678
  }
569
679
  try {
570
- return transform_cell(normalized_structure as Crystal, cell_type, sym_data)
680
+ return transform_cell(structure_with_bonds as Crystal, cell_type, sym_data)
571
681
  } catch (error) {
572
682
  console.error(`Failed to transform cell to ${cell_type}:`, error)
573
- return normalized_structure
683
+ return structure_with_bonds
574
684
  }
575
685
  })
576
686
 
@@ -580,6 +690,19 @@
580
690
  let has_supercell = $derived(
581
691
  !!supercell_scaling && ![``, `1x1x1`, `1`].includes(supercell_scaling),
582
692
  )
693
+ let bond_edits_enabled = $derived(
694
+ cell_type === `original` && !has_supercell && !supercell_loading,
695
+ )
696
+
697
+ $effect(() => {
698
+ if (measure_mode !== `edit-bonds` || bond_edits_enabled) return
699
+ untrack(() => {
700
+ clear_selection()
701
+ clear_bond_edits()
702
+ measure_mode = `distance`
703
+ show_toast(`Bond editing is only available for the original 1x1x1 cell`)
704
+ })
705
+ })
583
706
 
584
707
  // Tile volumetric data to match supercell when active.
585
708
  // Gate on !supercell_loading so the tiled volume and supercell structure update
@@ -806,17 +929,17 @@
806
929
  }
807
930
 
808
931
  const emit_file_load_event = (
809
- structure: AnyStructure,
932
+ loaded_structure: AnyStructure,
810
933
  filename: string,
811
934
  content: string | ArrayBuffer,
812
935
  ) =>
813
936
  on_file_load?.({
814
- structure: structure,
937
+ structure: loaded_structure,
815
938
  filename,
816
939
  file_size: typeof content === `string`
817
940
  ? new Blob([content]).size
818
941
  : content.byteLength,
819
- total_atoms: structure.sites?.length || 0,
942
+ total_atoms: loaded_structure.sites?.length || 0,
820
943
  })
821
944
 
822
945
  // Try to parse content as a volumetric file, setting both structure and volumetric data.
@@ -930,8 +1053,7 @@
930
1053
  }
931
1054
  // Clear per-site overrides since indices shifted after deletion
932
1055
  if (site_radius_overrides?.size > 0) site_radius_overrides.clear()
933
- added_bonds = []
934
- removed_bonds = []
1056
+ clear_bond_edits()
935
1057
  show_toast(`Deleted ${n_deleted} site${n_deleted > 1 ? `s` : ``}`)
936
1058
  }
937
1059
  return
@@ -1295,15 +1417,13 @@
1295
1417
  style="margin-left: -2px"
1296
1418
  />
1297
1419
  </button>
1298
- {#if (measured_sites?.length ?? 0) > 0 || added_bonds.length > 0 ||
1299
- removed_bonds.length > 0}
1420
+ {#if (measured_sites?.length ?? 0) > 0 || has_bond_edits}
1300
1421
  <button
1301
1422
  type="button"
1302
1423
  aria-label="Reset selection and bond edits"
1303
1424
  onclick={() => {
1304
1425
  clear_selection()
1305
- added_bonds = []
1306
- removed_bonds = []
1426
+ clear_bond_edits()
1307
1427
  }}
1308
1428
  >
1309
1429
  <Icon icon="Reset" style="margin-left: -4px" />
@@ -1333,7 +1453,14 @@
1333
1453
  <button
1334
1454
  class="view-mode-option"
1335
1455
  class:selected={measure_mode === mode}
1336
- onclick={() => [measure_mode, measure_menu_open] = [mode, false]}
1456
+ disabled={mode === `edit-bonds` && !bond_edits_enabled}
1457
+ title={mode === `edit-bonds` && !bond_edits_enabled
1458
+ ? `Bond editing is only available for the original 1x1x1 cell`
1459
+ : label}
1460
+ onclick={() => {
1461
+ if (mode === `edit-bonds` && !bond_edits_enabled) return
1462
+ ;[measure_mode, measure_menu_open] = [mode, false]
1463
+ }}
1337
1464
  >
1338
1465
  <Icon {icon} style="transform: scale({scale})" />
1339
1466
  <span>{@html sanitize_html(label)}</span>
@@ -1427,7 +1554,9 @@
1427
1554
  <StructureInfoPane
1428
1555
  structure={normalized_structure}
1429
1556
  bind:pane_open={info_pane_open}
1430
- {selected_sites}
1557
+ bind:highlighted_sites
1558
+ bind:hovered_site_idx
1559
+ bind:selected_sites
1431
1560
  {sym_data}
1432
1561
  {@attach tooltip({ content: `Structure info pane` })}
1433
1562
  />
@@ -1508,6 +1637,8 @@
1508
1637
  {isosurface_settings}
1509
1638
  bind:camera_is_moving
1510
1639
  bind:selected_sites
1640
+ active_sites={active_scene_sites}
1641
+ bind:hovered_idx={hovered_site_idx}
1511
1642
  bind:measured_sites
1512
1643
  bind:scene
1513
1644
  bind:camera
@@ -1520,6 +1651,8 @@
1520
1651
  bind:site_radius_overrides
1521
1652
  bind:added_bonds
1522
1653
  bind:removed_bonds
1654
+ bind:bond_order_overrides
1655
+ {bond_edits_enabled}
1523
1656
  {measure_mode}
1524
1657
  {width}
1525
1658
  {height}
@@ -1545,8 +1678,7 @@
1545
1678
  <div class="edit-toast">{toast_msg}</div>
1546
1679
  {/if}
1547
1680
 
1548
- {#if measure_mode === `edit-bonds` &&
1549
- (added_bonds.length > 0 || removed_bonds.length > 0)}
1681
+ {#if measure_mode === `edit-bonds` && has_bond_edits}
1550
1682
  <div class="bond-edit-status">
1551
1683
  {#if added_bonds.length > 0}
1552
1684
  <span class="added">+{added_bonds.length} added</span>
@@ -1554,6 +1686,9 @@
1554
1686
  {#if removed_bonds.length > 0}
1555
1687
  <span class="removed">-{removed_bonds.length} removed</span>
1556
1688
  {/if}
1689
+ {#if bond_order_overrides.length > 0}
1690
+ <span class="changed">{bond_order_overrides.length} ordered</span>
1691
+ {/if}
1557
1692
  </div>
1558
1693
  {/if}
1559
1694
 
@@ -1626,7 +1761,10 @@
1626
1761
  gap: 4pt;
1627
1762
  /* buttons need higher z-index than AtomLegend to make info/controls panes occlude legend */
1628
1763
  /* we also need crazy high z-index to make info/control pane occlude threlte/extras' <HTML> elements for site labels */
1629
- z-index: var(--struct-buttons-z-index, 100000000);
1764
+ z-index: var(
1765
+ --struct-buttons-z-index,
1766
+ var(--z-index-overlay-controls, 100000000)
1767
+ );
1630
1768
  opacity: 0;
1631
1769
  pointer-events: none;
1632
1770
  transition: opacity 0.2s ease;
@@ -1792,6 +1930,10 @@
1792
1930
  color: #f44336;
1793
1931
  font-weight: bold;
1794
1932
  }
1933
+ .bond-edit-status .changed {
1934
+ color: var(--accent-color, #007acc);
1935
+ font-weight: bold;
1936
+ }
1795
1937
  /* CellSelect: position at left of legend, show on hover */
1796
1938
  .structure :global(.cell-select) {
1797
1939
  order: -1; /* Move to left side of AtomLegend flex container */
@@ -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 } from './';
5
+ import type { AnyStructure, 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';
@@ -15,6 +15,7 @@ import StructureScene from './StructureScene.svelte';
15
15
  type EventHandler = (data: StructureHandlerData) => void;
16
16
  type $$ComponentProps = {
17
17
  structure?: AnyStructure;
18
+ bonds?: StructureBond[];
18
19
  scene_props?: ComponentProps<typeof StructureScene>;
19
20
  /**
20
21
  * Controls visibility configuration.
@@ -53,6 +54,8 @@ type $$ComponentProps = {
53
54
  error_msg?: string;
54
55
  performance_mode?: `quality` | `speed`;
55
56
  selected_sites?: number[];
57
+ highlighted_sites?: number[];
58
+ hovered_site_idx?: number | null;
56
59
  measured_sites?: number[];
57
60
  displayed_structure?: AnyStructure;
58
61
  hidden_elements?: Set<ElementSymbol>;
@@ -77,7 +80,8 @@ type $$ComponentProps = {
77
80
  on_fullscreen_change?: EventHandler;
78
81
  on_camera_move?: EventHandler;
79
82
  on_camera_reset?: EventHandler;
83
+ on_bonds_change?: (bonds: StructureBond[] | undefined) => void;
80
84
  } & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`> & Omit<HTMLAttributes<HTMLDivElement>, `children`>;
81
- 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" | "atom_color_config" | "hidden_elements" | "hidden_prop_vals" | "element_mapping" | "element_radius_overrides" | "site_radius_overrides" | "selected_sites" | "supercell_scaling" | "cell_type" | "active_volume_idx" | "measured_sites" | "scene_props" | "lattice_props" | "volumetric_data" | "isosurface_settings" | "measure_mode" | "enable_measure_mode" | "performance_mode" | "displayed_structure" | "symmetry_settings">;
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">;
82
86
  type Structure = ReturnType<typeof Structure>;
83
87
  export default Structure;
@@ -112,21 +112,91 @@
112
112
  }
113
113
  })
114
114
 
115
+ const hex_color_pattern = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i
116
+ const color_mix_pattern =
117
+ /^color-mix\(in srgb,\s*(#[0-9a-f]{3}(?:[0-9a-f]{3})?)\s+(\d+(?:\.\d+)?)%,\s*transparent\)$/i
118
+
119
+ const as_hex_color = (color: string | undefined, fallback: string): string =>
120
+ color?.match(hex_color_pattern)?.[0] ?? fallback
121
+
122
+ const parse_label_bg_color = (
123
+ color: string | undefined,
124
+ fallback_hex_color: string,
125
+ fallback_opacity: number,
126
+ ): { hex_color: string; opacity: number } => {
127
+ if (color === `transparent`) {
128
+ return { hex_color: fallback_hex_color, opacity: 0 }
129
+ }
130
+ const color_mix = color?.match(color_mix_pattern)
131
+ if (color_mix) {
132
+ const percentage = Math.max(0, Math.min(100, Number(color_mix[2])))
133
+ return {
134
+ hex_color: color_mix[1],
135
+ opacity: percentage / 100,
136
+ }
137
+ }
138
+ const hex_color = color?.match(hex_color_pattern)?.[0]
139
+ return hex_color === undefined
140
+ ? { hex_color: fallback_hex_color, opacity: fallback_opacity }
141
+ : { hex_color, opacity: 1 }
142
+ }
143
+
144
+ const default_site_label_color = as_hex_color(
145
+ DEFAULTS.structure.site_label_color,
146
+ `#111111`,
147
+ )
148
+ const default_site_label_bg = parse_label_bg_color(
149
+ DEFAULTS.structure.site_label_bg_color,
150
+ `#000000`,
151
+ 0,
152
+ )
153
+ const initial_site_label_bg = parse_label_bg_color(
154
+ scene_props.site_label_bg_color,
155
+ default_site_label_bg.hex_color,
156
+ default_site_label_bg.opacity,
157
+ )
158
+
115
159
  // Atom label color management
116
160
  let site_label_hex_color = $state(
117
- scene_props.site_label_color || DEFAULTS.structure.site_label_color,
118
- )
119
- let site_label_bg_hex_color = $state(
120
- scene_props.site_label_bg_color || DEFAULTS.structure.site_label_bg_color,
161
+ as_hex_color(scene_props.site_label_color, default_site_label_color),
121
162
  )
122
- let site_label_background_opacity = $state(0)
163
+ let site_label_bg_hex_color = $state(initial_site_label_bg.hex_color)
164
+ let site_label_background_opacity = $state(initial_site_label_bg.opacity)
165
+ let last_synced_site_label_color = scene_props.site_label_color
166
+ let last_synced_site_label_bg_color = scene_props.site_label_bg_color
123
167
 
124
168
  $effect(() => {
125
- scene_props.site_label_color = site_label_hex_color
126
- scene_props.site_label_bg_color =
127
- `color-mix(in srgb, ${site_label_bg_hex_color} ${
128
- format_num(site_label_background_opacity, `.1~%`)
129
- }, transparent)`
169
+ const external_color_changed =
170
+ scene_props.site_label_color !== last_synced_site_label_color
171
+ const external_bg_changed =
172
+ scene_props.site_label_bg_color !== last_synced_site_label_bg_color
173
+
174
+ if (external_color_changed) {
175
+ site_label_hex_color = as_hex_color(
176
+ scene_props.site_label_color,
177
+ default_site_label_color,
178
+ )
179
+ }
180
+ if (external_bg_changed) {
181
+ const next_bg = parse_label_bg_color(
182
+ scene_props.site_label_bg_color,
183
+ default_site_label_bg.hex_color,
184
+ default_site_label_bg.opacity,
185
+ )
186
+ site_label_bg_hex_color = next_bg.hex_color
187
+ site_label_background_opacity = next_bg.opacity
188
+ }
189
+
190
+ if (!external_color_changed) scene_props.site_label_color = site_label_hex_color
191
+ if (!external_bg_changed) {
192
+ scene_props.site_label_bg_color =
193
+ `color-mix(in srgb, ${site_label_bg_hex_color} ${
194
+ format_num(site_label_background_opacity, `.1~%`)
195
+ }, transparent)`
196
+ }
197
+
198
+ last_synced_site_label_color = scene_props.site_label_color
199
+ last_synced_site_label_bg_color = scene_props.site_label_bg_color
130
200
  })
131
201
 
132
202
  // Ensure site_label_offset is always available
@@ -638,15 +708,19 @@
638
708
  scene_props.site_label_size = DEFAULTS.structure.site_label_size
639
709
  scene_props.site_label_padding = DEFAULTS.structure.site_label_padding
640
710
  scene_props.site_label_offset = [...DEFAULTS.structure.site_label_offset]
641
- site_label_hex_color = DEFAULTS.structure.site_label_color
642
- site_label_bg_hex_color = DEFAULTS.structure.site_label_bg_color
643
- site_label_background_opacity = 0
711
+ site_label_hex_color = default_site_label_color
712
+ site_label_bg_hex_color = default_site_label_bg.hex_color
713
+ site_label_background_opacity = default_site_label_bg.opacity
644
714
  }}
645
715
  >
646
716
  <div class="pane-row">
647
717
  <label>
648
718
  Color
649
- <input type="color" bind:value={site_label_hex_color} />
719
+ <input
720
+ type="color"
721
+ aria-label="Site label color"
722
+ bind:value={site_label_hex_color}
723
+ />
650
724
  </label>
651
725
  <label>
652
726
  Size
@@ -662,7 +736,11 @@
662
736
  <div class="pane-row">
663
737
  <label>
664
738
  Background
665
- <input type="color" bind:value={site_label_bg_hex_color} />
739
+ <input
740
+ type="color"
741
+ aria-label="Site label background color"
742
+ bind:value={site_label_bg_hex_color}
743
+ />
666
744
  </label>
667
745
  <label>
668
746
  Opacity
@@ -671,6 +749,7 @@
671
749
  min="0"
672
750
  max="1"
673
751
  step="0.01"
752
+ aria-label="Site label background opacity"
674
753
  bind:value={site_label_background_opacity}
675
754
  />
676
755
  <input
@@ -678,6 +757,7 @@
678
757
  min="0"
679
758
  max="1"
680
759
  step="0.01"
760
+ aria-label="Site label background opacity slider"
681
761
  bind:value={site_label_background_opacity}
682
762
  />
683
763
  </label>
@@ -938,7 +1018,10 @@
938
1018
  {/if}
939
1019
 
940
1020
  {#if !supercell_input_valid}
941
- <div style="color: red; font-size: 0.8em; margin-top: 4pt">
1021
+ <div
1022
+ data-testid="supercell-input-error"
1023
+ style="color: red; font-size: 0.8em; margin-top: 4pt"
1024
+ >
942
1025
  Invalid format. Use patterns like "2x2x2", "3x1x2", or "2".
943
1026
  </div>
944
1027
  {/if}
@@ -1083,11 +1166,15 @@
1083
1166
  title="Bonds"
1084
1167
  current_values={{
1085
1168
  bonding_strategy: scene_props.bonding_strategy,
1169
+ auto_bond_order: scene_props.auto_bond_order,
1170
+ aromatic_display: scene_props.aromatic_display,
1086
1171
  bond_color: scene_props.bond_color,
1087
1172
  bond_thickness: scene_props.bond_thickness,
1088
1173
  }}
1089
1174
  on_reset={() => {
1090
1175
  scene_props.bonding_strategy = DEFAULTS.structure.bonding_strategy
1176
+ scene_props.auto_bond_order = DEFAULTS.structure.auto_bond_order
1177
+ scene_props.aromatic_display = DEFAULTS.structure.aromatic_display
1091
1178
  scene_props.bond_color = DEFAULTS.structure.bond_color
1092
1179
  scene_props.bond_thickness = DEFAULTS.structure.bond_thickness
1093
1180
  }}
@@ -1104,6 +1191,33 @@
1104
1191
  {/each}
1105
1192
  </select>
1106
1193
  </label>
1194
+ <label
1195
+ style="gap: 6pt"
1196
+ {@attach tooltip({
1197
+ content: SETTINGS_CONFIG.structure.auto_bond_order.description,
1198
+ })}
1199
+ >
1200
+ <input type="checkbox" bind:checked={scene_props.auto_bond_order} />
1201
+ Auto bond order (perceive double/triple/aromatic)
1202
+ </label>
1203
+ {#if scene_props.auto_bond_order}
1204
+ <label
1205
+ {@attach tooltip({
1206
+ content: SETTINGS_CONFIG.structure.aromatic_display.description,
1207
+ })}
1208
+ >
1209
+ Aromatic display <select bind:value={scene_props.aromatic_display}>
1210
+ {#each Object.entries(
1211
+ SETTINGS_CONFIG.structure.aromatic_display.enum ?? {},
1212
+ ) as
1213
+ [value, label]
1214
+ (value)
1215
+ }
1216
+ <option {value}>{label}</option>
1217
+ {/each}
1218
+ </select>
1219
+ </label>
1220
+ {/if}
1107
1221
  <label>
1108
1222
  Color <input type="color" bind:value={scene_props.bond_color} />
1109
1223
  </label>
@@ -26,6 +26,6 @@ type $$ComponentProps = Omit<ComponentProps<typeof DraggablePane>, `children`> &
26
26
  pane_props?: ComponentProps<typeof DraggablePane>[`pane_props`];
27
27
  toggle_props?: ComponentProps<typeof DraggablePane>[`toggle_props`];
28
28
  };
29
- declare const StructureControls: import("svelte").Component<$$ComponentProps, {}, "controls_open" | "color_scheme" | "background_color" | "background_opacity" | "show_image_atoms" | "atom_color_config" | "supercell_scaling" | "cell_type" | "active_volume_idx" | "scene_props" | "lattice_props" | "supercell_loading" | "volumetric_data" | "isosurface_settings">;
29
+ declare const StructureControls: import("svelte").Component<$$ComponentProps, {}, "controls_open" | "color_scheme" | "background_color" | "background_opacity" | "show_image_atoms" | "atom_color_config" | "lattice_props" | "volumetric_data" | "isosurface_settings" | "supercell_scaling" | "cell_type" | "active_volume_idx" | "scene_props" | "supercell_loading">;
30
30
  type StructureControls = ReturnType<typeof StructureControls>;
31
31
  export default StructureControls;