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.
- package/dist/MillerIndexInput.svelte +5 -5
- package/dist/api/optimade.js +3 -3
- package/dist/brillouin/BrillouinZone.svelte +5 -2
- package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
- package/dist/brillouin/BrillouinZoneExportPane.svelte +1 -3
- package/dist/brillouin/BrillouinZoneInfoPane.svelte +1 -1
- package/dist/brillouin/BrillouinZoneScene.svelte +5 -5
- package/dist/brillouin/compute.js +21 -21
- package/dist/brillouin/index.d.ts +1 -1
- package/dist/brillouin/index.js +0 -1
- package/dist/brillouin/types.d.ts +8 -13
- package/dist/chempot-diagram/ChemPotDiagram.svelte +3 -3
- package/dist/chempot-diagram/ChemPotDiagram2D.svelte +3 -4
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +33 -34
- package/dist/chempot-diagram/compute.js +1 -7
- package/dist/chempot-diagram/temperature.d.ts +1 -1
- package/dist/chempot-diagram/temperature.js +1 -3
- package/dist/chempot-diagram/types.d.ts +4 -9
- package/dist/colors/index.js +5 -5
- package/dist/composition/Composition.svelte +2 -1
- package/dist/composition/Formula.svelte +7 -4
- package/dist/composition/FormulaFilter.svelte +1 -3
- package/dist/composition/format.js +4 -4
- package/dist/composition/parse.d.ts +2 -1
- package/dist/composition/parse.js +61 -46
- package/dist/convex-hull/ConvexHull2D.svelte +62 -51
- package/dist/convex-hull/ConvexHull3D.svelte +101 -90
- package/dist/convex-hull/ConvexHull4D.svelte +70 -58
- package/dist/convex-hull/ConvexHullControls.svelte +24 -35
- package/dist/convex-hull/ConvexHullInfoPane.svelte +8 -5
- package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +2 -0
- package/dist/convex-hull/ConvexHullStats.svelte +9 -2
- package/dist/convex-hull/ConvexHullStats.svelte.d.ts +2 -0
- package/dist/convex-hull/GasPressureControls.svelte +7 -7
- package/dist/convex-hull/StructurePopup.svelte +65 -30
- package/dist/convex-hull/StructurePopup.svelte.d.ts +6 -6
- package/dist/convex-hull/TemperatureSlider.svelte +8 -5
- package/dist/convex-hull/barycentric-coords.d.ts +2 -2
- package/dist/convex-hull/barycentric-coords.js +2 -2
- package/dist/convex-hull/gas-thermodynamics.js +2 -4
- package/dist/convex-hull/helpers.d.ts +13 -2
- package/dist/convex-hull/helpers.js +37 -16
- package/dist/convex-hull/index.d.ts +1 -0
- package/dist/convex-hull/index.js +1 -0
- package/dist/convex-hull/thermodynamics.d.ts +2 -1
- package/dist/convex-hull/thermodynamics.js +7 -7
- package/dist/convex-hull/types.d.ts +15 -15
- package/dist/effects.svelte.d.ts +12 -0
- package/dist/effects.svelte.js +37 -0
- package/dist/element/BohrAtom.svelte +4 -4
- package/dist/element/data.json.gz.d.ts +3 -1
- package/dist/element/index.d.ts +1 -1
- package/dist/element/index.js +0 -1
- package/dist/fermi-surface/FermiSurface.svelte +4 -4
- package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceControls.svelte +15 -19
- package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
- package/dist/fermi-surface/FermiSurfaceScene.svelte +8 -6
- package/dist/fermi-surface/compute.js +2 -2
- package/dist/fermi-surface/export.js +13 -26
- package/dist/fermi-surface/parse.js +8 -12
- package/dist/fermi-surface/types.d.ts +2 -5
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +21 -3
- package/dist/heatmap-matrix/index.js +6 -6
- package/dist/io/decompress.d.ts +2 -1
- package/dist/io/decompress.js +1 -1
- package/dist/io/export.js +1 -1
- package/dist/io/index.d.ts +1 -1
- package/dist/io/index.js +0 -1
- package/dist/io/url-drop.js +7 -1
- package/dist/isosurface/IsosurfaceControls.svelte +11 -25
- package/dist/isosurface/slice.js +1 -1
- package/dist/isosurface/types.js +12 -12
- package/dist/labels.d.ts +1 -1
- package/dist/labels.js +14 -11
- package/dist/layout/InfoTag.svelte +6 -4
- package/dist/layout/PropertyFilter.svelte +4 -2
- package/dist/layout/json-tree/JsonTree.svelte +22 -14
- package/dist/layout/json-tree/JsonValue.svelte +2 -2
- package/dist/layout/json-tree/types.d.ts +3 -2
- package/dist/layout/json-tree/types.js +0 -1
- package/dist/layout/json-tree/utils.d.ts +4 -4
- package/dist/layout/json-tree/utils.js +12 -20
- package/dist/marching-cubes.js +13 -15
- package/dist/math.d.ts +11 -1
- package/dist/math.js +15 -6
- package/dist/overlays/DragControlTab.svelte +98 -0
- package/dist/overlays/DragControlTab.svelte.d.ts +8 -0
- package/dist/overlays/DraggablePane.svelte +7 -84
- package/dist/overlays/index.d.ts +1 -0
- package/dist/overlays/index.js +1 -0
- package/dist/periodic-table/PeriodicTable.svelte +11 -11
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +4 -2
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramControls.svelte +4 -9
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
- package/dist/phase-diagram/PhaseDiagramExportPane.svelte +2 -10
- package/dist/phase-diagram/PhaseDiagramTooltip.svelte +2 -3
- package/dist/phase-diagram/TdbInfoPanel.svelte +3 -3
- package/dist/phase-diagram/build-diagram.js +11 -18
- package/dist/phase-diagram/diagram-input.d.ts +5 -9
- package/dist/phase-diagram/index.d.ts +2 -2
- package/dist/phase-diagram/index.js +0 -2
- package/dist/phase-diagram/parse.d.ts +2 -2
- package/dist/phase-diagram/parse.js +6 -10
- package/dist/phase-diagram/svg-to-diagram.js +15 -15
- package/dist/phase-diagram/types.d.ts +5 -11
- package/dist/phase-diagram/utils.d.ts +2 -2
- package/dist/phase-diagram/utils.js +9 -11
- package/dist/plot/BarPlot.svelte +162 -314
- package/dist/plot/BarPlot.svelte.d.ts +5 -4
- package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
- package/dist/plot/BinnedScatterPlot.svelte +1114 -0
- package/dist/plot/BinnedScatterPlot.svelte.d.ts +66 -0
- package/dist/plot/ColorBar.svelte +19 -17
- package/dist/plot/ColorBar.svelte.d.ts +1 -1
- package/dist/plot/FillArea.svelte +2 -4
- package/dist/plot/FillArea.svelte.d.ts +1 -1
- package/dist/plot/Histogram.svelte +167 -281
- package/dist/plot/Histogram.svelte.d.ts +1 -1
- package/dist/plot/HistogramControls.svelte.d.ts +1 -1
- package/dist/plot/InteractiveAxisLabel.svelte +5 -3
- package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
- package/dist/plot/PlotAxis.svelte +169 -0
- package/dist/plot/PlotAxis.svelte.d.ts +24 -0
- package/dist/plot/PlotControls.svelte.d.ts +1 -1
- package/dist/plot/ReferenceLine3D.svelte +53 -51
- package/dist/plot/ReferencePlane.svelte +39 -42
- package/dist/plot/ScatterPlot.svelte +300 -367
- package/dist/plot/ScatterPlot.svelte.d.ts +8 -5
- package/dist/plot/ScatterPlot3D.svelte +33 -6
- package/dist/plot/ScatterPlot3D.svelte.d.ts +3 -2
- package/dist/plot/ScatterPlot3DControls.svelte +9 -9
- package/dist/plot/ScatterPlotControls.svelte +3 -4
- package/dist/plot/ScatterPoint.svelte +18 -27
- package/dist/plot/ScatterPoint.svelte.d.ts +4 -3
- package/dist/plot/Surface3D.svelte +4 -7
- package/dist/plot/ZeroLines.svelte +2 -1
- package/dist/plot/ZeroLines.svelte.d.ts +2 -1
- package/dist/plot/ZoomRect.svelte +2 -2
- package/dist/plot/ZoomRect.svelte.d.ts +3 -3
- package/dist/plot/adaptive-density.d.ts +69 -0
- package/dist/plot/adaptive-density.js +191 -0
- package/dist/plot/auto-place.d.ts +43 -0
- package/dist/plot/auto-place.js +122 -0
- package/dist/plot/axis-utils.js +3 -5
- package/dist/plot/binned-scatter-types.d.ts +59 -0
- package/dist/plot/binned-scatter-types.js +1 -0
- package/dist/plot/data-cleaning.js +1 -1
- package/dist/plot/data-transform.js +1 -1
- package/dist/plot/fill-utils.d.ts +4 -9
- package/dist/plot/fill-utils.js +29 -44
- package/dist/plot/index.d.ts +4 -0
- package/dist/plot/index.js +2 -0
- package/dist/plot/interactions.d.ts +4 -4
- package/dist/plot/interactions.js +4 -3
- package/dist/plot/layout.d.ts +20 -2
- package/dist/plot/layout.js +59 -16
- package/dist/plot/reference-line.d.ts +1 -1
- package/dist/plot/reference-line.js +9 -11
- package/dist/plot/scales.d.ts +1 -1
- package/dist/plot/scales.js +20 -23
- package/dist/plot/types.d.ts +30 -58
- package/dist/plot/types.js +2 -6
- package/dist/plot/utils/label-placement.d.ts +24 -3
- package/dist/plot/utils/label-placement.js +82 -12
- package/dist/plot/utils/series-visibility.d.ts +8 -2
- package/dist/plot/utils/series-visibility.js +23 -5
- package/dist/rdf/RdfPlot.svelte +5 -5
- package/dist/rdf/calc-rdf.js +3 -3
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +2 -0
- package/dist/spectral/Bands.svelte +1 -1
- package/dist/spectral/BandsAndDos.svelte +22 -16
- package/dist/spectral/BrillouinBandsDos.svelte +20 -16
- package/dist/spectral/Dos.svelte +1 -1
- package/dist/spectral/helpers.d.ts +4 -2
- package/dist/spectral/helpers.js +44 -35
- package/dist/spectral/index.d.ts +1 -1
- package/dist/spectral/index.js +0 -1
- package/dist/structure/AtomLegend.svelte +23 -6
- package/dist/structure/AtomLegend.svelte.d.ts +1 -0
- package/dist/structure/CanvasTooltip.svelte +9 -9
- package/dist/structure/CanvasTooltip.svelte.d.ts +1 -1
- package/dist/structure/CellSelect.svelte +14 -16
- package/dist/structure/Structure.svelte +317 -68
- package/dist/structure/Structure.svelte.d.ts +4 -2
- package/dist/structure/StructureControls.svelte +20 -45
- package/dist/structure/StructureExportPane.svelte +2 -1
- package/dist/structure/StructureInfoPane.svelte +10 -8
- package/dist/structure/StructureScene.svelte +527 -177
- package/dist/structure/StructureScene.svelte.d.ts +5 -2
- package/dist/structure/atom-properties.js +4 -4
- package/dist/structure/bond-order-perception.js +115 -98
- package/dist/structure/bonding.d.ts +27 -1
- package/dist/structure/bonding.js +187 -16
- package/dist/structure/export.js +1 -1
- package/dist/structure/index.d.ts +3 -2
- package/dist/structure/index.js +0 -2
- package/dist/structure/parse.js +88 -59
- package/dist/symmetry/WyckoffTable.svelte +7 -0
- package/dist/symmetry/index.js +13 -14
- package/dist/table/HeatmapTable.svelte +45 -66
- package/dist/table/HeatmapTable.svelte.d.ts +1 -1
- package/dist/table/ToggleMenu.svelte +19 -10
- package/dist/theme/themes.mjs +12 -0
- package/dist/tooltip/index.d.ts +1 -1
- package/dist/tooltip/index.js +0 -1
- package/dist/trajectory/Trajectory.svelte +43 -15
- package/dist/trajectory/TrajectoryInfoPane.svelte +2 -2
- package/dist/trajectory/extract.js +1 -1
- package/dist/trajectory/frame-reader.js +4 -4
- package/dist/trajectory/helpers.d.ts +5 -4
- package/dist/trajectory/helpers.js +9 -17
- package/dist/trajectory/index.d.ts +2 -2
- package/dist/trajectory/index.js +2 -2
- package/dist/trajectory/parse/ase.js +4 -4
- package/dist/trajectory/parse/hdf5.js +1 -1
- package/dist/trajectory/parse/index.js +2 -3
- package/dist/trajectory/parse/lammps.js +1 -1
- package/dist/trajectory/parse/vasp.js +1 -1
- package/dist/trajectory/plotting.d.ts +1 -1
- package/dist/trajectory/plotting.js +38 -38
- package/dist/trajectory/types.d.ts +1 -1
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +9 -0
- package/dist/xrd/calc-xrd.js +3 -4
- package/dist/xrd/parse.js +1 -1
- package/package.json +42 -22
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
VectorLayerConfig,
|
|
17
17
|
} from '../settings'
|
|
18
18
|
import { DEFAULTS } from '../settings'
|
|
19
|
-
import {
|
|
19
|
+
import { create_pulse_animation } from '../effects.svelte'
|
|
20
20
|
import { colors } from '../state.svelte'
|
|
21
21
|
import type {
|
|
22
22
|
AnyStructure,
|
|
23
|
+
BondEditMode,
|
|
23
24
|
BondOrder,
|
|
24
25
|
BondPair,
|
|
25
26
|
MeasureMode,
|
|
@@ -52,15 +53,19 @@
|
|
|
52
53
|
import * as extras from '@threlte/extras'
|
|
53
54
|
import { type ComponentProps, type Snippet, untrack } from 'svelte'
|
|
54
55
|
import { SvelteMap, SvelteSet } from 'svelte/reactivity'
|
|
55
|
-
import { type Camera, Color, type Mesh, type Scene } from 'three'
|
|
56
|
+
import { type Camera, Color, type Mesh, type Object3D, type Scene, Vector3 } from 'three'
|
|
56
57
|
import Bond from './Bond.svelte'
|
|
57
|
-
import type { BondingStrategy } from './bonding'
|
|
58
|
+
import type { BondEditResult, BondingStrategy, BondKeyTarget } from './bonding'
|
|
58
59
|
import {
|
|
60
|
+
add_or_restore_bond,
|
|
59
61
|
BONDING_STRATEGIES,
|
|
62
|
+
BOND_ORDER_OPTIONS,
|
|
63
|
+
canonicalize_bond_target,
|
|
64
|
+
delete_bond as apply_delete_bond,
|
|
60
65
|
get_bond_key,
|
|
61
66
|
get_bond_render_matrices,
|
|
62
67
|
get_explicit_bond_metadata,
|
|
63
|
-
|
|
68
|
+
set_bond_order as apply_set_bond_order,
|
|
64
69
|
structure_bond_to_bond_pair,
|
|
65
70
|
} from './bonding'
|
|
66
71
|
import {
|
|
@@ -82,29 +87,40 @@
|
|
|
82
87
|
atoms: (typeof atom_data)[number][]
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
function instanced_atom_group_key(
|
|
91
|
+
{ element, radius, color, is_image_atom, atoms }: InstancedAtomGroup,
|
|
92
|
+
measure_mode: MeasureMode,
|
|
93
|
+
): string {
|
|
94
|
+
const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom
|
|
95
|
+
return `${element}-${format_num(radius, `.3~`)}-${color}-${
|
|
96
|
+
is_image_atom ? `img` : `base`
|
|
97
|
+
}-${edit_mode_image}-${atoms.length}`
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
type EditableAtomHitTarget = {
|
|
101
|
+
site_idx: number
|
|
102
|
+
position: Vec3
|
|
103
|
+
radius: number
|
|
104
|
+
}
|
|
105
|
+
|
|
85
106
|
type BondContextMenu = {
|
|
86
107
|
site_idx_1: number
|
|
87
108
|
site_idx_2: number
|
|
88
109
|
cell_shift?: Vec3
|
|
89
110
|
position: Vec3
|
|
90
111
|
}
|
|
91
|
-
type BondKeyTarget = Pick<StructureBond, `site_idx_1` | `site_idx_2` | `cell_shift`>
|
|
92
112
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
frame_id = requestAnimationFrame(animate)
|
|
106
|
-
return () => cancelAnimationFrame(frame_id)
|
|
107
|
-
})
|
|
113
|
+
type BondPointerEvent = PointerEvent & {
|
|
114
|
+
nativeEvent?: PointerEvent
|
|
115
|
+
object?: Object3D
|
|
116
|
+
point?: Vector3
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
type BondContextMenuEvent = MouseEvent & {
|
|
120
|
+
nativeEvent?: MouseEvent
|
|
121
|
+
object?: Object3D
|
|
122
|
+
point?: Vector3
|
|
123
|
+
}
|
|
108
124
|
|
|
109
125
|
let {
|
|
110
126
|
structure = undefined,
|
|
@@ -169,6 +185,8 @@
|
|
|
169
185
|
removed_bonds = $bindable([]),
|
|
170
186
|
bond_order_overrides = $bindable([]),
|
|
171
187
|
bond_edits_enabled = true,
|
|
188
|
+
bond_edit_mode = $bindable<BondEditMode>(`add`),
|
|
189
|
+
bond_edit_order = 1,
|
|
172
190
|
selection_highlight_color = `#6cf0ff`,
|
|
173
191
|
// Active highlight group with different color
|
|
174
192
|
active_sites = $bindable([]),
|
|
@@ -192,6 +210,7 @@
|
|
|
192
210
|
// Edit-atoms mode callbacks
|
|
193
211
|
on_sites_moved,
|
|
194
212
|
on_operation_start,
|
|
213
|
+
on_bond_edit_start,
|
|
195
214
|
on_add_atom,
|
|
196
215
|
add_atom_mode = $bindable(false),
|
|
197
216
|
add_element = $bindable(`C`),
|
|
@@ -265,6 +284,8 @@
|
|
|
265
284
|
removed_bonds?: StructureBond[]
|
|
266
285
|
bond_order_overrides?: StructureBond[]
|
|
267
286
|
bond_edits_enabled?: boolean
|
|
287
|
+
bond_edit_mode?: BondEditMode
|
|
288
|
+
bond_edit_order?: BondOrder
|
|
268
289
|
selection_highlight_color?: string
|
|
269
290
|
// Support for active highlight group with different color
|
|
270
291
|
active_sites?: number[]
|
|
@@ -285,6 +306,7 @@
|
|
|
285
306
|
// Edit-atoms mode callbacks and state
|
|
286
307
|
on_sites_moved?: (scene_indices: number[], delta: Vec3) => void
|
|
287
308
|
on_operation_start?: () => void
|
|
309
|
+
on_bond_edit_start?: () => void
|
|
288
310
|
on_add_atom?: (xyz: Vec3, element: ElementSymbol) => void
|
|
289
311
|
add_atom_mode?: boolean // whether user is in click-to-place add-atom sub-mode
|
|
290
312
|
add_element?: ElementSymbol // element to add when clicking in add-atom mode
|
|
@@ -294,6 +316,12 @@
|
|
|
294
316
|
isosurface_settings?: IsosurfaceSettings // Isosurface rendering settings
|
|
295
317
|
} = $props()
|
|
296
318
|
|
|
319
|
+
const pulse = create_pulse_animation(
|
|
320
|
+
() => selected_sites.length > 0 || active_sites.length > 0,
|
|
321
|
+
{ step: 0.015, frequency: 5 },
|
|
322
|
+
)
|
|
323
|
+
let pulse_opacity = $derived(0.15 + 0.25 * pulse.unit)
|
|
324
|
+
|
|
297
325
|
const threlte = useThrelte()
|
|
298
326
|
$effect(() => {
|
|
299
327
|
scene = threlte.scene
|
|
@@ -318,15 +346,54 @@
|
|
|
318
346
|
})
|
|
319
347
|
|
|
320
348
|
let bond_pairs: BondPair[] = $state([])
|
|
321
|
-
let
|
|
349
|
+
let atom_tooltip_active = $state(false)
|
|
322
350
|
let hovered_bond_key = $state<string | null>(null)
|
|
351
|
+
const ATOM_HOVER_CLEAR_DELAY_MS = 200
|
|
352
|
+
let clear_atom_hover_timeout: ReturnType<typeof setTimeout> | null = null
|
|
353
|
+
|
|
354
|
+
function cancel_atom_hover_clear(): void {
|
|
355
|
+
if (clear_atom_hover_timeout == null) return
|
|
356
|
+
clearTimeout(clear_atom_hover_timeout)
|
|
357
|
+
clear_atom_hover_timeout = null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function set_atom_hover(site_idx: number): void {
|
|
361
|
+
cancel_atom_hover_clear()
|
|
362
|
+
if (hovered_idx !== site_idx) hovered_idx = site_idx
|
|
363
|
+
if (!atom_tooltip_active) atom_tooltip_active = true
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function schedule_atom_hover_clear(site_idx: number): void {
|
|
367
|
+
cancel_atom_hover_clear()
|
|
368
|
+
clear_atom_hover_timeout = setTimeout(() => {
|
|
369
|
+
clear_atom_hover_timeout = null
|
|
370
|
+
if (hovered_idx !== site_idx) return
|
|
371
|
+
hovered_idx = null
|
|
372
|
+
atom_tooltip_active = false
|
|
373
|
+
}, ATOM_HOVER_CLEAR_DELAY_MS)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const atom_hover_props = (site_idx: number | null, disabled = false) => ({
|
|
377
|
+
onpointerenter: () => {
|
|
378
|
+
if (!disabled && site_idx != null) set_atom_hover(site_idx)
|
|
379
|
+
},
|
|
380
|
+
onpointermove: () => {
|
|
381
|
+
if (!disabled && site_idx != null) set_atom_hover(site_idx)
|
|
382
|
+
},
|
|
383
|
+
onpointerleave: () => {
|
|
384
|
+
if (!disabled && site_idx != null) schedule_atom_hover_clear(site_idx)
|
|
385
|
+
},
|
|
386
|
+
})
|
|
323
387
|
|
|
324
388
|
// Cursor style for the canvas, derived from mode and hover state
|
|
325
389
|
let canvas_cursor = $derived.by(() => {
|
|
326
390
|
if (measure_mode === `edit-atoms` && add_atom_mode) return `crosshair`
|
|
391
|
+
if (measure_mode === `edit-bonds` && hovered_bond_key != null) {
|
|
392
|
+
return bond_edits_enabled ? `pointer` : `not-allowed`
|
|
393
|
+
}
|
|
327
394
|
if (hovered_idx != null) {
|
|
328
395
|
if (measure_mode === `edit-bonds`) {
|
|
329
|
-
return
|
|
396
|
+
return bond_edit_mode === `add` && can_select_bond_site(hovered_idx)
|
|
330
397
|
? `pointer`
|
|
331
398
|
: `not-allowed`
|
|
332
399
|
}
|
|
@@ -356,12 +423,6 @@
|
|
|
356
423
|
// snaps to the new wrapped centroid.
|
|
357
424
|
let frozen_centroid = $state<Vec3 | null>(null)
|
|
358
425
|
|
|
359
|
-
const BOND_ORDER_OPTIONS: { order: BondOrder; label: string }[] = [
|
|
360
|
-
{ order: 1, label: `Single` },
|
|
361
|
-
{ order: 2, label: `Double` },
|
|
362
|
-
{ order: 3, label: `Triple` },
|
|
363
|
-
{ order: `aromatic`, label: `Aromatic` },
|
|
364
|
-
]
|
|
365
426
|
let bond_context_menu = $state<BondContextMenu | null>(null)
|
|
366
427
|
// Threlte/HTML pointer events can close the visible menu before a button
|
|
367
428
|
// handler runs, so keep the target bond separately for menu actions.
|
|
@@ -372,27 +433,45 @@
|
|
|
372
433
|
bond_context_target = null
|
|
373
434
|
}
|
|
374
435
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
site_idx_2: number,
|
|
378
|
-
order: BondOrder,
|
|
379
|
-
cell_shift?: Vec3,
|
|
380
|
-
): StructureBond => normalize_structure_bond(site_idx_1, site_idx_2, order, cell_shift)
|
|
436
|
+
const canonical_bond_target = (bond: BondKeyTarget): BondKeyTarget =>
|
|
437
|
+
canonicalize_bond_target(bond, structure?.sites)
|
|
381
438
|
|
|
382
|
-
const bond_key_for = (bond: BondKeyTarget): string =>
|
|
439
|
+
const bond_key_for = (bond: BondKeyTarget): string => {
|
|
440
|
+
const target = canonical_bond_target(bond)
|
|
441
|
+
return get_bond_key(target.site_idx_1, target.site_idx_2, target.cell_shift)
|
|
442
|
+
}
|
|
443
|
+
const rendered_bond_key_for = (bond: BondKeyTarget): string =>
|
|
383
444
|
get_bond_key(bond.site_idx_1, bond.site_idx_2, bond.cell_shift)
|
|
384
445
|
|
|
385
446
|
const matches_bond_key = (bond: BondKeyTarget, key: string): boolean =>
|
|
386
447
|
bond_key_for(bond) === key
|
|
387
448
|
|
|
388
|
-
|
|
389
|
-
|
|
449
|
+
const find_added_bond_by_rendered_key = (key: string): StructureBond | undefined =>
|
|
450
|
+
added_bonds.find((bond) => rendered_bond_key_for(bond) === key)
|
|
451
|
+
|
|
452
|
+
function resolve_bond_edit_target(
|
|
453
|
+
site_idx_1: number,
|
|
454
|
+
site_idx_2: number,
|
|
455
|
+
cell_shift?: Vec3,
|
|
456
|
+
): BondKeyTarget {
|
|
457
|
+
const rendered_target = { site_idx_1, site_idx_2, cell_shift }
|
|
458
|
+
const rendered_key = rendered_bond_key_for(rendered_target)
|
|
459
|
+
return find_added_bond_by_rendered_key(rendered_key) ??
|
|
460
|
+
canonical_bond_target(rendered_target)
|
|
390
461
|
}
|
|
391
462
|
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
463
|
+
const is_image_bond_site = (site_idx: number): boolean =>
|
|
464
|
+
structure?.sites?.[site_idx]?.properties?.orig_site_idx != null
|
|
465
|
+
|
|
466
|
+
const can_select_bond_site = (site_idx: number): boolean =>
|
|
467
|
+
bond_edits_enabled && structure?.sites?.[site_idx] != null
|
|
468
|
+
|
|
469
|
+
const can_edit_bond = (bond: BondKeyTarget): boolean => {
|
|
470
|
+
const target = canonical_bond_target(bond)
|
|
471
|
+
return bond_edits_enabled &&
|
|
472
|
+
!is_image_bond_site(target.site_idx_1) &&
|
|
473
|
+
!is_image_bond_site(target.site_idx_2)
|
|
474
|
+
}
|
|
396
475
|
|
|
397
476
|
const format_bond_order = (order: BondOrder | undefined): string =>
|
|
398
477
|
order === undefined ? `1` : `${order}`
|
|
@@ -403,7 +482,8 @@
|
|
|
403
482
|
cell_shift?: Vec3,
|
|
404
483
|
): BondOrder | undefined {
|
|
405
484
|
const key = get_bond_key(site_idx_1, site_idx_2, cell_shift)
|
|
406
|
-
return
|
|
485
|
+
return find_added_bond_by_rendered_key(key)?.order ??
|
|
486
|
+
bond_order_overrides.find((bond) => matches_bond_key(bond, key))?.order ??
|
|
407
487
|
added_bonds.find((bond) => matches_bond_key(bond, key))?.order ??
|
|
408
488
|
filtered_bond_pairs.find((bond) => matches_bond_key(bond, key))?.bond_order
|
|
409
489
|
}
|
|
@@ -414,42 +494,168 @@
|
|
|
414
494
|
(pos_1[2] + pos_2[2]) / 2,
|
|
415
495
|
]
|
|
416
496
|
|
|
497
|
+
const BOND_ENDPOINT_HIT_FRACTION = 0.3
|
|
498
|
+
const BOND_ENDPOINT_SITE_MATCH_TOLERANCE = 1e-6
|
|
499
|
+
const EDITABLE_ATOM_HIT_RADIUS_SCALE = 1.15
|
|
500
|
+
const skip_raycast = (): void => undefined
|
|
501
|
+
|
|
502
|
+
function apply_bond_transform(mesh: Mesh, bond: BondPair): void {
|
|
503
|
+
mesh.matrix.fromArray(bond.transform_matrix)
|
|
504
|
+
mesh.matrixWorldNeedsUpdate = true
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function apply_non_raycastable_bond_hit_transform(mesh: Mesh, bond: BondPair): void {
|
|
508
|
+
apply_bond_transform(mesh, bond)
|
|
509
|
+
disable_raycast(mesh)
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function disable_raycast(mesh: Mesh): void {
|
|
513
|
+
mesh.raycast = skip_raycast
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function site_world_position(parent: Object3D, site: Site): Vector3 {
|
|
517
|
+
const position = new Vector3(...site.xyz)
|
|
518
|
+
return parent.localToWorld(position)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function get_bond_endpoint_site_idx(
|
|
522
|
+
site_idx: number,
|
|
523
|
+
world_position: Vector3,
|
|
524
|
+
parent: Object3D,
|
|
525
|
+
): number {
|
|
526
|
+
if (!structure?.sites) return site_idx
|
|
527
|
+
const site = structure.sites[site_idx]
|
|
528
|
+
if (!site) return site_idx
|
|
529
|
+
|
|
530
|
+
const matches_world_position = (candidate_site: Site): boolean =>
|
|
531
|
+
site_world_position(parent, candidate_site).distanceTo(world_position) <=
|
|
532
|
+
BOND_ENDPOINT_SITE_MATCH_TOLERANCE
|
|
533
|
+
|
|
534
|
+
if (matches_world_position(site)) {
|
|
535
|
+
return site_idx
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const image_site_idx = structure.sites.findIndex((candidate_site) =>
|
|
539
|
+
candidate_site.properties?.orig_site_idx === site_idx &&
|
|
540
|
+
matches_world_position(candidate_site)
|
|
541
|
+
)
|
|
542
|
+
return image_site_idx === -1 ? site_idx : image_site_idx
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function get_bond_endpoint_hit_site_idx(
|
|
546
|
+
bond: BondPair,
|
|
547
|
+
event: BondPointerEvent,
|
|
548
|
+
): number | null {
|
|
549
|
+
if (!event.point) return null
|
|
550
|
+
const parent = event.object?.parent
|
|
551
|
+
if (!parent) return null
|
|
552
|
+
|
|
553
|
+
const world_pos_1 = new Vector3(...bond.pos_1)
|
|
554
|
+
const world_pos_2 = new Vector3(...bond.pos_2)
|
|
555
|
+
parent.localToWorld(world_pos_1)
|
|
556
|
+
parent.localToWorld(world_pos_2)
|
|
557
|
+
|
|
558
|
+
const bond_vec = world_pos_2.clone().sub(world_pos_1)
|
|
559
|
+
const length_sq = bond_vec.lengthSq()
|
|
560
|
+
if (length_sq <= math.EPS) return null
|
|
561
|
+
|
|
562
|
+
const hit_vec = event.point.clone().sub(world_pos_1)
|
|
563
|
+
const bond_fraction = hit_vec.dot(bond_vec) / length_sq
|
|
564
|
+
if (bond_fraction <= BOND_ENDPOINT_HIT_FRACTION) {
|
|
565
|
+
return get_bond_endpoint_site_idx(bond.site_idx_1, world_pos_1, parent)
|
|
566
|
+
}
|
|
567
|
+
if (bond_fraction >= 1 - BOND_ENDPOINT_HIT_FRACTION) {
|
|
568
|
+
return get_bond_endpoint_site_idx(bond.site_idx_2, world_pos_2, parent)
|
|
569
|
+
}
|
|
570
|
+
return null
|
|
571
|
+
}
|
|
572
|
+
|
|
417
573
|
let label_screen_margin = $derived(site_label_size * 10 + site_label_padding)
|
|
418
574
|
|
|
419
|
-
function
|
|
575
|
+
function get_bond_context_menu_position(
|
|
576
|
+
bond: BondPair,
|
|
577
|
+
event?: BondContextMenuEvent,
|
|
578
|
+
): Vec3 {
|
|
579
|
+
const parent = event?.object?.parent
|
|
580
|
+
if (!event?.point || !parent) return midpoint(bond.pos_1, bond.pos_2)
|
|
581
|
+
|
|
582
|
+
const local_point = event.point.clone()
|
|
583
|
+
parent.worldToLocal(local_point)
|
|
584
|
+
return [local_point.x, local_point.y, local_point.z]
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function open_bond_context_menu(bond: BondPair, event?: BondContextMenuEvent) {
|
|
420
588
|
if (!can_edit_bond(bond)) return
|
|
421
589
|
bond_context_target = {
|
|
422
590
|
site_idx_1: bond.site_idx_1,
|
|
423
591
|
site_idx_2: bond.site_idx_2,
|
|
424
592
|
cell_shift: bond.cell_shift,
|
|
425
|
-
position:
|
|
593
|
+
position: get_bond_context_menu_position(bond, event),
|
|
426
594
|
}
|
|
427
595
|
bond_context_menu = bond_context_target
|
|
428
596
|
}
|
|
429
597
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
598
|
+
const current_bond_edit_state = () => ({
|
|
599
|
+
added_bonds,
|
|
600
|
+
removed_bonds,
|
|
601
|
+
bond_order_overrides,
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
function apply_bond_edit_result(result: BondEditResult, close_menu = true) {
|
|
605
|
+
if (!result.changed) return
|
|
606
|
+
on_bond_edit_start?.()
|
|
607
|
+
added_bonds = result.state.added_bonds
|
|
608
|
+
removed_bonds = result.state.removed_bonds
|
|
609
|
+
bond_order_overrides = result.state.bond_order_overrides
|
|
610
|
+
if (close_menu) close_bond_context_menu()
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const find_visible_bond = (
|
|
614
|
+
target: BondKeyTarget,
|
|
615
|
+
canonical_target: BondKeyTarget = target,
|
|
616
|
+
): BondPair | undefined => {
|
|
617
|
+
const rendered_key = rendered_bond_key_for(target)
|
|
618
|
+
const canonical_key = bond_key_for(canonical_target)
|
|
619
|
+
return filtered_bond_pairs.find((bond) => rendered_bond_key_for(bond) === rendered_key) ??
|
|
620
|
+
filtered_bond_pairs.find((bond) => bond_key_for(bond) === canonical_key)
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function open_bond_order_menu_for_target(
|
|
624
|
+
target: BondKeyTarget,
|
|
625
|
+
canonical_target: BondKeyTarget = target,
|
|
626
|
+
) {
|
|
627
|
+
const bond = find_visible_bond(target, canonical_target)
|
|
628
|
+
if (bond) open_bond_context_menu(bond)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function add_or_restore_pair(site_idx_1: number, site_idx_2: number) {
|
|
632
|
+
const rendered_target = { site_idx_1, site_idx_2 }
|
|
633
|
+
if (!can_edit_bond(rendered_target)) return
|
|
634
|
+
const edit_state = current_bond_edit_state()
|
|
635
|
+
const canonical_target = canonical_bond_target(rendered_target)
|
|
636
|
+
const canonical_result = add_or_restore_bond(
|
|
637
|
+
edit_state,
|
|
638
|
+
canonical_target,
|
|
639
|
+
editable_perceived_bond_pairs,
|
|
640
|
+
bond_edit_order,
|
|
641
|
+
)
|
|
642
|
+
const use_rendered_target =
|
|
643
|
+
canonical_result.action === `added` &&
|
|
644
|
+
rendered_bond_key_for(canonical_target) !== rendered_bond_key_for(rendered_target)
|
|
645
|
+
const target = use_rendered_target ? rendered_target : canonical_target
|
|
646
|
+
const result = use_rendered_target
|
|
647
|
+
? add_or_restore_bond(
|
|
648
|
+
edit_state,
|
|
649
|
+
rendered_target,
|
|
650
|
+
editable_perceived_bond_pairs,
|
|
651
|
+
bond_edit_order,
|
|
652
|
+
)
|
|
653
|
+
: canonical_result
|
|
654
|
+
if (result.action === `already-visible`) {
|
|
655
|
+
open_bond_order_menu_for_target(rendered_target, target)
|
|
449
656
|
return
|
|
450
657
|
}
|
|
451
|
-
|
|
452
|
-
else added_bonds = [...added_bonds, record]
|
|
658
|
+
apply_bond_edit_result(result, false)
|
|
453
659
|
}
|
|
454
660
|
|
|
455
661
|
function set_bond_order(
|
|
@@ -458,21 +664,16 @@
|
|
|
458
664
|
order: BondOrder,
|
|
459
665
|
cell_shift?: Vec3,
|
|
460
666
|
) {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
removed_bonds = removed_bonds.filter((bond) => !matches_bond_key(bond, key))
|
|
472
|
-
} else {
|
|
473
|
-
added_bonds = [...added_bonds, new_record]
|
|
474
|
-
}
|
|
475
|
-
close_bond_context_menu()
|
|
667
|
+
const target = resolve_bond_edit_target(site_idx_1, site_idx_2, cell_shift)
|
|
668
|
+
if (!can_edit_bond(target)) return
|
|
669
|
+
apply_bond_edit_result(
|
|
670
|
+
apply_set_bond_order(
|
|
671
|
+
current_bond_edit_state(),
|
|
672
|
+
target,
|
|
673
|
+
editable_perceived_bond_pairs,
|
|
674
|
+
order,
|
|
675
|
+
),
|
|
676
|
+
)
|
|
476
677
|
}
|
|
477
678
|
|
|
478
679
|
function set_context_bond_order(order: BondOrder) {
|
|
@@ -482,22 +683,15 @@
|
|
|
482
683
|
}
|
|
483
684
|
|
|
484
685
|
function remove_bond(site_idx_1: number, site_idx_2: number, cell_shift?: Vec3) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
686
|
+
const target = resolve_bond_edit_target(site_idx_1, site_idx_2, cell_shift)
|
|
687
|
+
if (!can_edit_bond(target)) return
|
|
688
|
+
apply_bond_edit_result(
|
|
689
|
+
apply_delete_bond(
|
|
690
|
+
current_bond_edit_state(),
|
|
691
|
+
target,
|
|
692
|
+
editable_perceived_bond_pairs,
|
|
693
|
+
),
|
|
490
694
|
)
|
|
491
|
-
if (
|
|
492
|
-
bond_pairs.some((bond) => matches_bond_key(bond, key)) &&
|
|
493
|
-
!removed_bonds.some((bond) => matches_bond_key(bond, key))
|
|
494
|
-
) {
|
|
495
|
-
removed_bonds = [
|
|
496
|
-
...removed_bonds,
|
|
497
|
-
make_bond_record(site_idx_1, site_idx_2, 1, cell_shift),
|
|
498
|
-
]
|
|
499
|
-
}
|
|
500
|
-
close_bond_context_menu()
|
|
501
695
|
}
|
|
502
696
|
|
|
503
697
|
function remove_context_bond() {
|
|
@@ -510,19 +704,57 @@
|
|
|
510
704
|
// intercept the same native click, only the first intersection should fire.
|
|
511
705
|
// All threlte intersection events from one click share the same nativeEvent ref.
|
|
512
706
|
let last_native_event: Event | null = null
|
|
707
|
+
// extras.Instance does not always emit pointerdown, so edit-bonds also falls
|
|
708
|
+
// back to click. When pointerdown did fire, skip the matching click once.
|
|
709
|
+
let last_edit_bonds_pointerdown_site_idx: number | null = null
|
|
710
|
+
let clear_edit_bonds_pointerdown_site_timeout:
|
|
711
|
+
| ReturnType<typeof setTimeout>
|
|
712
|
+
| null = null
|
|
713
|
+
|
|
714
|
+
function remember_edit_bonds_pointerdown_site(site_idx: number) {
|
|
715
|
+
last_edit_bonds_pointerdown_site_idx = site_idx
|
|
716
|
+
if (clear_edit_bonds_pointerdown_site_timeout != null) {
|
|
717
|
+
clearTimeout(clear_edit_bonds_pointerdown_site_timeout)
|
|
718
|
+
}
|
|
719
|
+
clear_edit_bonds_pointerdown_site_timeout = setTimeout(() => {
|
|
720
|
+
last_edit_bonds_pointerdown_site_idx = null
|
|
721
|
+
clear_edit_bonds_pointerdown_site_timeout = null
|
|
722
|
+
}, 250)
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function select_edit_bonds_site(site_idx: number, event: Event): void {
|
|
726
|
+
toggle_selection(site_idx, event)
|
|
727
|
+
remember_edit_bonds_pointerdown_site(site_idx)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function skip_duplicate_edit_bonds_click(site_idx: number) {
|
|
731
|
+
if (last_edit_bonds_pointerdown_site_idx !== site_idx) return false
|
|
732
|
+
|
|
733
|
+
last_edit_bonds_pointerdown_site_idx = null
|
|
734
|
+
if (clear_edit_bonds_pointerdown_site_timeout != null) {
|
|
735
|
+
clearTimeout(clear_edit_bonds_pointerdown_site_timeout)
|
|
736
|
+
clear_edit_bonds_pointerdown_site_timeout = null
|
|
737
|
+
}
|
|
738
|
+
return true
|
|
739
|
+
}
|
|
513
740
|
|
|
514
741
|
function toggle_selection(site_index: number, evt?: Event) {
|
|
515
742
|
evt?.stopPropagation?.()
|
|
516
|
-
const
|
|
517
|
-
|
|
743
|
+
const event_with_native = evt as Event & { nativeEvent?: unknown } | undefined
|
|
744
|
+
const native_event = event_with_native?.nativeEvent ?? evt
|
|
518
745
|
if (native_event instanceof Event) {
|
|
519
746
|
if (native_event === last_native_event) return
|
|
520
747
|
last_native_event = native_event
|
|
521
748
|
}
|
|
522
749
|
|
|
523
750
|
if (measure_mode === `edit-bonds`) {
|
|
524
|
-
if (
|
|
525
|
-
|
|
751
|
+
if (bond_edit_mode === `delete`) {
|
|
752
|
+
measured_sites = []
|
|
753
|
+
selected_sites = []
|
|
754
|
+
return
|
|
755
|
+
}
|
|
756
|
+
if (!can_select_bond_site(site_index)) return
|
|
757
|
+
// In Add mode, select atom pairs without making existing bonds destructive.
|
|
526
758
|
const new_sites = measured_sites.includes(site_index)
|
|
527
759
|
? measured_sites.filter((idx) => idx !== site_index)
|
|
528
760
|
: [...measured_sites, site_index]
|
|
@@ -530,9 +762,9 @@
|
|
|
530
762
|
measured_sites = new_sites
|
|
531
763
|
selected_sites = new_sites
|
|
532
764
|
|
|
533
|
-
// When two atoms are selected,
|
|
534
|
-
if (
|
|
535
|
-
|
|
765
|
+
// When two atoms are selected, add/restore or open order editing.
|
|
766
|
+
if (new_sites.length === 2) {
|
|
767
|
+
add_or_restore_pair(new_sites[0], new_sites[1])
|
|
536
768
|
measured_sites = []
|
|
537
769
|
selected_sites = []
|
|
538
770
|
}
|
|
@@ -584,6 +816,7 @@
|
|
|
584
816
|
$effect(() => {
|
|
585
817
|
void structure
|
|
586
818
|
void measure_mode
|
|
819
|
+
void bond_edit_mode
|
|
587
820
|
void bond_edits_enabled
|
|
588
821
|
untrack(() => {
|
|
589
822
|
close_bond_context_menu()
|
|
@@ -626,9 +859,16 @@
|
|
|
626
859
|
: [0, 0, 0] as Vec3,
|
|
627
860
|
)
|
|
628
861
|
|
|
629
|
-
let structure_size = $derived(
|
|
630
|
-
lattice
|
|
631
|
-
|
|
862
|
+
let structure_size = $derived.by(() => {
|
|
863
|
+
if (lattice) return (lattice.a + lattice.b + lattice.c) / 2
|
|
864
|
+
if (!structure?.sites?.length) return 10
|
|
865
|
+
|
|
866
|
+
const ranges = [0, 1, 2].map((axis_idx) => {
|
|
867
|
+
const coords = structure.sites.map((site) => site.xyz[axis_idx])
|
|
868
|
+
return Math.max(...coords) - Math.min(...coords)
|
|
869
|
+
})
|
|
870
|
+
return Math.max(1, ...ranges)
|
|
871
|
+
})
|
|
632
872
|
|
|
633
873
|
// Characteristic inter-atomic spacing: cube root of volume per atom.
|
|
634
874
|
// Excludes PBC image atoms (orig_site_idx) so toggling image atoms doesn't affect arrow sizing.
|
|
@@ -799,6 +1039,10 @@
|
|
|
799
1039
|
)
|
|
800
1040
|
})
|
|
801
1041
|
|
|
1042
|
+
let editable_perceived_bond_pairs = $derived(
|
|
1043
|
+
perceived_bond_pairs.map((bond) => ({ ...bond, ...canonical_bond_target(bond) })),
|
|
1044
|
+
)
|
|
1045
|
+
|
|
802
1046
|
let filtered_bond_pairs = $derived.by(() => {
|
|
803
1047
|
if (!structure?.sites) return perceived_bond_pairs
|
|
804
1048
|
|
|
@@ -806,14 +1050,18 @@
|
|
|
806
1050
|
const removed_keys = new Set(
|
|
807
1051
|
removed_bonds.map(bond_key_for),
|
|
808
1052
|
)
|
|
1053
|
+
const added_keys = new Set(
|
|
1054
|
+
added_bonds.map(bond_key_for),
|
|
1055
|
+
)
|
|
809
1056
|
const order_overrides = new Map(
|
|
810
1057
|
bond_order_overrides.map((bond) => [bond_key_for(bond), bond.order]),
|
|
811
1058
|
)
|
|
812
1059
|
|
|
813
|
-
// Filter calculated bonds: exclude removed and hidden
|
|
1060
|
+
// Filter calculated bonds: exclude removed, replaced by manual additions, and hidden.
|
|
814
1061
|
const calculated = perceived_bond_pairs
|
|
815
1062
|
.filter((bond) => {
|
|
816
|
-
|
|
1063
|
+
const key = bond_key_for(bond)
|
|
1064
|
+
if (removed_keys.has(key) || added_keys.has(key)) return false
|
|
817
1065
|
return is_site_visible(bond.site_idx_1) && is_site_visible(bond.site_idx_2)
|
|
818
1066
|
})
|
|
819
1067
|
.map((bond) => {
|
|
@@ -907,6 +1155,28 @@
|
|
|
907
1155
|
return map
|
|
908
1156
|
})
|
|
909
1157
|
|
|
1158
|
+
let editable_atom_hit_targets = $derived.by(() => {
|
|
1159
|
+
if (
|
|
1160
|
+
measure_mode !== `edit-bonds` ||
|
|
1161
|
+
bond_edit_mode !== `add` ||
|
|
1162
|
+
!bond_edits_enabled
|
|
1163
|
+
) {
|
|
1164
|
+
return []
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const targets = new SvelteMap<number, EditableAtomHitTarget>()
|
|
1168
|
+
for (const atom of atom_data) {
|
|
1169
|
+
if (!can_select_bond_site(atom.site_idx)) continue
|
|
1170
|
+
if (targets.has(atom.site_idx)) continue
|
|
1171
|
+
targets.set(atom.site_idx, {
|
|
1172
|
+
site_idx: atom.site_idx,
|
|
1173
|
+
position: atom.position,
|
|
1174
|
+
radius: atom.radius,
|
|
1175
|
+
})
|
|
1176
|
+
}
|
|
1177
|
+
return [...targets.values()]
|
|
1178
|
+
})
|
|
1179
|
+
|
|
910
1180
|
// Get radius for a site (for highlight fallback when site is hidden/filtered)
|
|
911
1181
|
// Checks site_radius_overrides first for consistency with visible atoms
|
|
912
1182
|
const get_site_radius = (site: Site, site_idx: number | null): number => {
|
|
@@ -1138,6 +1408,7 @@
|
|
|
1138
1408
|
dampingFactor: rotation_damping,
|
|
1139
1409
|
onstart: () => {
|
|
1140
1410
|
camera_is_moving = true
|
|
1411
|
+
cancel_atom_hover_clear()
|
|
1141
1412
|
hovered_idx = null
|
|
1142
1413
|
bond_context_menu = null
|
|
1143
1414
|
},
|
|
@@ -1178,29 +1449,45 @@
|
|
|
1178
1449
|
{#if atom_label}
|
|
1179
1450
|
{@render atom_label({ site, site_idx })}
|
|
1180
1451
|
{:else}
|
|
1181
|
-
<
|
|
1452
|
+
<button
|
|
1453
|
+
type="button"
|
|
1182
1454
|
class="atom-label"
|
|
1183
1455
|
style:font-size="{site_label_size * 0.85}em"
|
|
1184
1456
|
style:background={site_label_bg_color}
|
|
1185
1457
|
style:padding="{site_label_padding}px"
|
|
1186
1458
|
style:color={site_label_color}
|
|
1459
|
+
onpointerdown={(event) => {
|
|
1460
|
+
event.preventDefault()
|
|
1461
|
+
event.stopImmediatePropagation()
|
|
1462
|
+
toggle_selection(site_idx, event)
|
|
1463
|
+
}}
|
|
1464
|
+
onclick={(event) => {
|
|
1465
|
+
event.preventDefault()
|
|
1466
|
+
event.stopImmediatePropagation()
|
|
1467
|
+
}}
|
|
1468
|
+
onkeydown={(event) => {
|
|
1469
|
+
if (event.key !== `Enter` && event.key !== ` `) return
|
|
1470
|
+
event.preventDefault()
|
|
1471
|
+
event.stopPropagation()
|
|
1472
|
+
toggle_selection(site_idx, event)
|
|
1473
|
+
}}
|
|
1187
1474
|
>
|
|
1188
1475
|
{#if show_site_labels}
|
|
1189
1476
|
{#if site.species.length === 1}
|
|
1190
1477
|
{site.species[0].element}{#if show_site_indices}-{site_idx + 1}{/if}
|
|
1191
1478
|
{:else}
|
|
1192
|
-
{
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1479
|
+
{#each site.species as
|
|
1480
|
+
{ element, occu, oxidation_state }
|
|
1481
|
+
(`${element}-${occu}-${oxidation_state}`)
|
|
1482
|
+
}
|
|
1483
|
+
{element}<sub>{format_num(occu, `.3~`).replace(`0.`, `.`)}</sub>
|
|
1484
|
+
{/each}
|
|
1485
|
+
{#if show_site_indices}-{site_idx + 1}{/if}
|
|
1199
1486
|
{/if}
|
|
1200
1487
|
{:else if show_site_indices}
|
|
1201
1488
|
{site_idx + 1}
|
|
1202
1489
|
{/if}
|
|
1203
|
-
</
|
|
1490
|
+
</button>
|
|
1204
1491
|
{/if}
|
|
1205
1492
|
</extras.HTML>
|
|
1206
1493
|
{/snippet}
|
|
@@ -1240,13 +1527,11 @@
|
|
|
1240
1527
|
<T.Group position={math.scale(rotation_target, -1)}>
|
|
1241
1528
|
{#if show_atoms}
|
|
1242
1529
|
<!-- Instanced rendering for full occupancy atoms -->
|
|
1243
|
-
{#each instanced_atom_groups as
|
|
1244
|
-
{ element, radius, color, is_image_atom, atoms }
|
|
1245
|
-
(`${element}-${radius}-${color}-${is_image_atom ? `img` : `base`}`)
|
|
1246
|
-
}
|
|
1530
|
+
{#each instanced_atom_groups as atom_group (instanced_atom_group_key(atom_group, measure_mode))}
|
|
1531
|
+
{@const { element, radius, color, is_image_atom, atoms } = atom_group}
|
|
1247
1532
|
{@const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom}
|
|
1248
1533
|
<extras.InstancedMesh
|
|
1249
|
-
key=
|
|
1534
|
+
key={instanced_atom_group_key(atom_group, measure_mode)}
|
|
1250
1535
|
limit={atoms.length}
|
|
1251
1536
|
range={atoms.length}
|
|
1252
1537
|
frustumCulled={false}
|
|
@@ -1261,18 +1546,26 @@
|
|
|
1261
1546
|
<extras.Instance
|
|
1262
1547
|
position={atom.position}
|
|
1263
1548
|
scale={atom.radius}
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1549
|
+
{...atom_hover_props(atom.site_idx, edit_mode_image)}
|
|
1550
|
+
onpointerdown={(event: PointerEvent) => {
|
|
1551
|
+
if (
|
|
1552
|
+
edit_mode_image ||
|
|
1553
|
+
measure_mode !== `edit-bonds` ||
|
|
1554
|
+
bond_edit_mode !== `add`
|
|
1555
|
+
) {
|
|
1556
|
+
return
|
|
1557
|
+
}
|
|
1558
|
+
select_edit_bonds_site(atom.site_idx, event)
|
|
1273
1559
|
}}
|
|
1274
1560
|
onclick={(event: MouseEvent) => {
|
|
1275
1561
|
if (edit_mode_image) return
|
|
1562
|
+
if (measure_mode === `edit-bonds`) {
|
|
1563
|
+
if (bond_edit_mode !== `add`) return
|
|
1564
|
+
if (skip_duplicate_edit_bonds_click(atom.site_idx)) {
|
|
1565
|
+
event.stopPropagation()
|
|
1566
|
+
return
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1276
1569
|
toggle_selection(atom.site_idx, event)
|
|
1277
1570
|
}}
|
|
1278
1571
|
/>
|
|
@@ -1290,18 +1583,26 @@
|
|
|
1290
1583
|
<T.Group
|
|
1291
1584
|
position={atom.position}
|
|
1292
1585
|
scale={atom.radius}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1586
|
+
{...atom_hover_props(atom.site_idx, partial_edit_image)}
|
|
1587
|
+
onpointerdown={(event: PointerEvent) => {
|
|
1588
|
+
if (
|
|
1589
|
+
partial_edit_image ||
|
|
1590
|
+
measure_mode !== `edit-bonds` ||
|
|
1591
|
+
bond_edit_mode !== `add`
|
|
1592
|
+
) {
|
|
1593
|
+
return
|
|
1594
|
+
}
|
|
1595
|
+
select_edit_bonds_site(atom.site_idx, event)
|
|
1302
1596
|
}}
|
|
1303
1597
|
onclick={(event: MouseEvent) => {
|
|
1304
1598
|
if (partial_edit_image) return
|
|
1599
|
+
if (measure_mode === `edit-bonds`) {
|
|
1600
|
+
if (bond_edit_mode !== `add`) return
|
|
1601
|
+
if (skip_duplicate_edit_bonds_click(atom.site_idx)) {
|
|
1602
|
+
event.stopPropagation()
|
|
1603
|
+
return
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1305
1606
|
toggle_selection(atom.site_idx, event)
|
|
1306
1607
|
}}
|
|
1307
1608
|
>
|
|
@@ -1400,37 +1701,86 @@
|
|
|
1400
1701
|
{#if measure_mode === `edit-bonds` && editable_bond_pairs.length > 0}
|
|
1401
1702
|
{#each editable_bond_pairs as
|
|
1402
1703
|
bond
|
|
1403
|
-
(`bond-hit-${
|
|
1704
|
+
(`bond-hit-${bond_edit_mode}-${rendered_bond_key_for(bond)}`)
|
|
1404
1705
|
}
|
|
1405
|
-
{@const bond_key =
|
|
1706
|
+
{@const bond_key = rendered_bond_key_for(bond)}
|
|
1406
1707
|
{@const is_hovered = hovered_bond_key === bond_key}
|
|
1708
|
+
{@const is_delete_mode = bond_edit_mode === `delete`}
|
|
1709
|
+
{@const bond_hit_radius =
|
|
1710
|
+
bond_thickness * (is_delete_mode ? 5 : 1.25)}
|
|
1711
|
+
{@const bond_hover_radius = bond_thickness * 1.1}
|
|
1407
1712
|
<T.Mesh
|
|
1408
1713
|
matrixAutoUpdate={false}
|
|
1409
|
-
oncreate={(ref) =>
|
|
1410
|
-
|
|
1411
|
-
ref.matrixWorldNeedsUpdate = true
|
|
1412
|
-
}}
|
|
1413
|
-
onpointerdown={(event: PointerEvent & { nativeEvent?: PointerEvent }) => {
|
|
1714
|
+
oncreate={(ref) => apply_bond_transform(ref, bond)}
|
|
1715
|
+
onpointerdown={(event: BondPointerEvent) => {
|
|
1414
1716
|
if (event.nativeEvent?.button === 2) return
|
|
1415
1717
|
event.stopPropagation()
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1718
|
+
if (is_delete_mode) {
|
|
1719
|
+
remove_bond(bond.site_idx_1, bond.site_idx_2, bond.cell_shift)
|
|
1720
|
+
measured_sites = []
|
|
1721
|
+
selected_sites = []
|
|
1722
|
+
hovered_bond_key = null
|
|
1723
|
+
} else {
|
|
1724
|
+
const endpoint_site_idx = get_bond_endpoint_hit_site_idx(bond, event)
|
|
1725
|
+
if (endpoint_site_idx != null) {
|
|
1726
|
+
select_edit_bonds_site(endpoint_site_idx, event)
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1420
1729
|
}}
|
|
1421
|
-
oncontextmenu={(event:
|
|
1730
|
+
oncontextmenu={(event: BondContextMenuEvent) => {
|
|
1422
1731
|
event.nativeEvent?.preventDefault()
|
|
1423
1732
|
event.stopPropagation?.()
|
|
1424
|
-
open_bond_context_menu(bond)
|
|
1733
|
+
open_bond_context_menu(bond, event)
|
|
1425
1734
|
}}
|
|
1426
1735
|
onpointerenter={() => (hovered_bond_key = bond_key)}
|
|
1736
|
+
onpointermove={() => (hovered_bond_key = bond_key)}
|
|
1427
1737
|
onpointerleave={() => (hovered_bond_key = null)}
|
|
1428
1738
|
>
|
|
1429
|
-
<T.CylinderGeometry
|
|
1739
|
+
<T.CylinderGeometry
|
|
1740
|
+
args={[
|
|
1741
|
+
bond_hit_radius,
|
|
1742
|
+
bond_hit_radius,
|
|
1743
|
+
1,
|
|
1744
|
+
6,
|
|
1745
|
+
]}
|
|
1746
|
+
/>
|
|
1430
1747
|
<T.MeshBasicMaterial
|
|
1431
1748
|
transparent
|
|
1432
|
-
opacity={
|
|
1433
|
-
|
|
1749
|
+
opacity={0}
|
|
1750
|
+
depthWrite={false}
|
|
1751
|
+
/>
|
|
1752
|
+
</T.Mesh>
|
|
1753
|
+
{#if is_hovered}
|
|
1754
|
+
<T.Mesh
|
|
1755
|
+
matrixAutoUpdate={false}
|
|
1756
|
+
oncreate={(ref) => apply_non_raycastable_bond_hit_transform(ref, bond)}
|
|
1757
|
+
>
|
|
1758
|
+
<T.CylinderGeometry args={[bond_hover_radius, bond_hover_radius, 1, 6]} />
|
|
1759
|
+
<T.MeshBasicMaterial
|
|
1760
|
+
transparent
|
|
1761
|
+
opacity={0.25}
|
|
1762
|
+
color={is_delete_mode ? `#ff4444` : `#6cf0ff`}
|
|
1763
|
+
depthWrite={false}
|
|
1764
|
+
/>
|
|
1765
|
+
</T.Mesh>
|
|
1766
|
+
{/if}
|
|
1767
|
+
{/each}
|
|
1768
|
+
{/if}
|
|
1769
|
+
|
|
1770
|
+
{#if editable_atom_hit_targets.length > 0}
|
|
1771
|
+
{#each editable_atom_hit_targets as atom_hit (atom_hit.site_idx)}
|
|
1772
|
+
<T.Mesh
|
|
1773
|
+
position={atom_hit.position}
|
|
1774
|
+
scale={atom_hit.radius * EDITABLE_ATOM_HIT_RADIUS_SCALE}
|
|
1775
|
+
{...atom_hover_props(atom_hit.site_idx)}
|
|
1776
|
+
onpointerdown={(event: PointerEvent) => {
|
|
1777
|
+
select_edit_bonds_site(atom_hit.site_idx, event)
|
|
1778
|
+
}}
|
|
1779
|
+
>
|
|
1780
|
+
<T.SphereGeometry args={[0.5, 12, 12]} />
|
|
1781
|
+
<T.MeshBasicMaterial
|
|
1782
|
+
transparent
|
|
1783
|
+
opacity={0}
|
|
1434
1784
|
depthWrite={false}
|
|
1435
1785
|
/>
|
|
1436
1786
|
</T.Mesh>
|
|
@@ -1443,7 +1793,7 @@
|
|
|
1443
1793
|
bond_context_menu.site_idx_2,
|
|
1444
1794
|
bond_context_menu.cell_shift,
|
|
1445
1795
|
)}
|
|
1446
|
-
<extras.HTML
|
|
1796
|
+
<extras.HTML autoRender={false} position={bond_context_menu.position}>
|
|
1447
1797
|
<div class="bond-context-menu">
|
|
1448
1798
|
<strong>Bond Order ({format_bond_order(current_order)})</strong>
|
|
1449
1799
|
{#each BOND_ORDER_OPTIONS as { order, label } (label)}
|
|
@@ -1537,11 +1887,7 @@
|
|
|
1537
1887
|
<T.Mesh
|
|
1538
1888
|
position={xyz}
|
|
1539
1889
|
scale={1.2 * highlight_radius}
|
|
1540
|
-
|
|
1541
|
-
if (site_idx !== null && Number.isInteger(site_idx)) {
|
|
1542
|
-
toggle_selection(site_idx, event)
|
|
1543
|
-
}
|
|
1544
|
-
}}
|
|
1890
|
+
oncreate={disable_raycast}
|
|
1545
1891
|
>
|
|
1546
1892
|
<T.SphereGeometry args={[0.5, 22, 22]} />
|
|
1547
1893
|
<T.MeshStandardMaterial
|
|
@@ -1557,9 +1903,10 @@
|
|
|
1557
1903
|
{/if}
|
|
1558
1904
|
{/each}
|
|
1559
1905
|
|
|
1560
|
-
<!-- selection order labels (1, 2, 3, ...) for
|
|
1906
|
+
<!-- selection order labels (1, 2, 3, ...) for measurements and bond editing -->
|
|
1561
1907
|
{#if structure?.sites && (measured_sites?.length ?? 0) > 0 &&
|
|
1562
|
-
|
|
1908
|
+
(measure_mode === `distance` || measure_mode === `angle` ||
|
|
1909
|
+
measure_mode === `edit-bonds`)}
|
|
1563
1910
|
{#each measured_sites as site_index, loop_idx (site_index)}
|
|
1564
1911
|
{@const site = structure.sites[site_index]}
|
|
1565
1912
|
{#if site}
|
|
@@ -1575,7 +1922,7 @@
|
|
|
1575
1922
|
|
|
1576
1923
|
<!-- hovered site tooltip -->
|
|
1577
1924
|
{#if hovered_site && !camera_is_moving &&
|
|
1578
|
-
(
|
|
1925
|
+
(atom_tooltip_active || active_sites.includes(hovered_idx ?? -1))}
|
|
1579
1926
|
{@const abc = hovered_site.abc.map((val) => format_num(val, float_fmt)).join(
|
|
1580
1927
|
`, `,
|
|
1581
1928
|
)}
|
|
@@ -1585,13 +1932,13 @@
|
|
|
1585
1932
|
{@const bond_neighbors = (() => {
|
|
1586
1933
|
if (hovered_idx == null || !structure?.sites) return []
|
|
1587
1934
|
return filtered_bond_pairs
|
|
1588
|
-
.filter((
|
|
1589
|
-
|
|
1935
|
+
.filter((bond) =>
|
|
1936
|
+
bond.site_idx_1 === hovered_idx || bond.site_idx_2 === hovered_idx
|
|
1590
1937
|
)
|
|
1591
|
-
.map((
|
|
1592
|
-
const neighbor_idx =
|
|
1593
|
-
?
|
|
1594
|
-
:
|
|
1938
|
+
.map((bond) => {
|
|
1939
|
+
const neighbor_idx = bond.site_idx_1 === hovered_idx
|
|
1940
|
+
? bond.site_idx_2
|
|
1941
|
+
: bond.site_idx_1
|
|
1595
1942
|
return structure.sites[neighbor_idx]?.species[0]?.element ?? `?`
|
|
1596
1943
|
})
|
|
1597
1944
|
})()}
|
|
@@ -1614,11 +1961,6 @@
|
|
|
1614
1961
|
idx
|
|
1615
1962
|
(`${element ?? ``}-${occu ?? ``}-${oxi_state ?? ``}-${idx}`)
|
|
1616
1963
|
}
|
|
1617
|
-
{@const oxi_str = (oxi_state != null && oxi_state !== 0)
|
|
1618
|
-
? `<sup>${Math.abs(oxi_state)}${
|
|
1619
|
-
oxi_state > 0 ? `+` : `−`
|
|
1620
|
-
}</sup>`
|
|
1621
|
-
: ``}
|
|
1622
1964
|
{@const element_name = element_data.find((elem) =>
|
|
1623
1965
|
elem.symbol === element
|
|
1624
1966
|
)?.name ??
|
|
@@ -1627,7 +1969,11 @@
|
|
|
1627
1969
|
{#if occu !== 1}<span class="occupancy">{
|
|
1628
1970
|
format_num(occu, `.3~f`)
|
|
1629
1971
|
}</span>{/if}
|
|
1630
|
-
<strong>
|
|
1972
|
+
<strong>
|
|
1973
|
+
{element}{#if oxi_state != null && oxi_state !== 0}<sup>{Math.abs(
|
|
1974
|
+
oxi_state,
|
|
1975
|
+
)}{oxi_state > 0 ? `+` : `−`}</sup>{/if}
|
|
1976
|
+
</strong>
|
|
1631
1977
|
{#if element_name}<span class="elem-name">{element_name}</span>{/if}
|
|
1632
1978
|
{/each}
|
|
1633
1979
|
</div>
|
|
@@ -1822,7 +2168,11 @@
|
|
|
1822
2168
|
}
|
|
1823
2169
|
.atom-label {
|
|
1824
2170
|
background: var(--struct-atom-label-bg, rgba(0, 0, 0, 0.1));
|
|
2171
|
+
border: 0;
|
|
1825
2172
|
border-radius: var(--struct-atom-label-border-radius, var(--border-radius, 3pt));
|
|
2173
|
+
color: inherit;
|
|
2174
|
+
cursor: pointer;
|
|
2175
|
+
font: inherit;
|
|
1826
2176
|
padding: var(--struct-atom-label-padding, 0 3px);
|
|
1827
2177
|
white-space: nowrap;
|
|
1828
2178
|
}
|
|
@@ -1861,7 +2211,7 @@
|
|
|
1861
2211
|
display: grid;
|
|
1862
2212
|
min-width: 8rem;
|
|
1863
2213
|
gap: 2pt;
|
|
1864
|
-
padding: 5pt;
|
|
2214
|
+
padding: 3pt 5pt;
|
|
1865
2215
|
border-radius: var(--border-radius, 3pt);
|
|
1866
2216
|
background: var(--surface-bg, Canvas);
|
|
1867
2217
|
color: var(--text-color, currentColor);
|