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.
- package/dist/FilePicker.svelte +1 -1
- package/dist/app.css +7 -0
- package/dist/brillouin/BrillouinZone.svelte +5 -2
- package/dist/brillouin/compute.js +8 -4
- package/dist/chempot-diagram/ChemPotDiagram3D.svelte +6 -6
- package/dist/chempot-diagram/async-compute.svelte.js +5 -4
- package/dist/chempot-diagram/chempot-worker.js +2 -2
- package/dist/chempot-diagram/compute.js +16 -16
- package/dist/composition/FormulaFilter.svelte +3 -3
- package/dist/constants.js +2 -8
- package/dist/convex-hull/ConvexHull.svelte +2 -2
- package/dist/convex-hull/ConvexHull2D.svelte +11 -10
- package/dist/convex-hull/ConvexHull3D.svelte +16 -14
- package/dist/convex-hull/ConvexHull4D.svelte +26 -14
- package/dist/convex-hull/ConvexHullControls.svelte +1 -1
- package/dist/convex-hull/ConvexHullInfoPane.svelte +68 -61
- package/dist/convex-hull/ConvexHullStats.svelte +23 -6
- package/dist/convex-hull/GasPressureControls.svelte +3 -3
- package/dist/convex-hull/TemperatureSlider.svelte +1 -1
- package/dist/convex-hull/barycentric-coords.js +2 -2
- package/dist/convex-hull/helpers.js +45 -27
- package/dist/convex-hull/thermodynamics.js +2 -2
- package/dist/element/BohrAtom.svelte +25 -27
- package/dist/element/BohrAtom.svelte.d.ts +2 -2
- package/dist/element/data.d.ts +2 -3
- package/dist/fermi-surface/FermiSurface.svelte +5 -2
- package/dist/fermi-surface/compute.js +3 -3
- package/dist/fermi-surface/parse.js +2 -2
- package/dist/fermi-surface/symmetry.js +1 -1
- package/dist/heatmap-matrix/HeatmapMatrix.svelte +8 -8
- package/dist/icons.d.ts +6 -6
- package/dist/icons.js +6 -6
- package/dist/io/decompress.js +12 -7
- package/dist/io/export.js +20 -16
- package/dist/io/is-binary.js +19 -4
- package/dist/isosurface/parse.js +8 -8
- package/dist/isosurface/types.js +9 -9
- package/dist/layout/InfoTag.svelte +1 -1
- package/dist/layout/json-tree/JsonNode.svelte +1 -0
- package/dist/layout/json-tree/utils.js +2 -1
- package/dist/marching-cubes.js +1 -1
- package/dist/math.js +1 -1
- package/dist/overlays/CopyButton.svelte +45 -0
- package/dist/overlays/CopyButton.svelte.d.ts +8 -0
- package/dist/overlays/InfoPaneCards.svelte +149 -0
- package/dist/overlays/InfoPaneCards.svelte.d.ts +22 -0
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +33 -35
- package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +2 -2
- package/dist/phase-diagram/PhaseDiagramControls.svelte +27 -29
- package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +2 -2
- package/dist/phase-diagram/parse.js +3 -3
- package/dist/phase-diagram/svg-to-diagram.js +10 -12
- package/dist/plot/BarPlot.svelte +24 -15
- package/dist/plot/BarPlot.svelte.d.ts +3 -2
- package/dist/plot/FillArea.svelte +2 -3
- package/dist/plot/FillArea.svelte.d.ts +3 -2
- package/dist/plot/Histogram.svelte +37 -19
- package/dist/plot/Line.svelte +2 -3
- package/dist/plot/Line.svelte.d.ts +2 -2
- package/dist/plot/PlotLegend.svelte +79 -8
- package/dist/plot/PlotLegend.svelte.d.ts +4 -0
- package/dist/plot/PortalSelect.svelte +5 -5
- package/dist/plot/ScatterPlot.svelte +47 -33
- package/dist/plot/ScatterPlot.svelte.d.ts +5 -4
- package/dist/plot/ScatterPlot3D.svelte +6 -3
- package/dist/plot/ScatterPoint.svelte +10 -4
- package/dist/plot/ScatterPoint.svelte.d.ts +4 -2
- package/dist/plot/SpacegroupBarPlot.svelte +5 -4
- package/dist/plot/data-cleaning.js +9 -9
- package/dist/plot/index.d.ts +0 -6
- package/dist/plot/scales.d.ts +3 -3
- package/dist/plot/scales.js +29 -29
- package/dist/plot/types.d.ts +5 -9
- package/dist/rdf/calc-rdf.js +1 -1
- package/dist/sanitize.js +22 -15
- package/dist/settings.d.ts +2 -0
- package/dist/settings.js +12 -3
- package/dist/spectral/Bands.svelte +6 -6
- package/dist/spectral/BandsAndDos.svelte +4 -4
- package/dist/spectral/BrillouinBandsDos.svelte +3 -3
- package/dist/spectral/Dos.svelte +2 -2
- package/dist/spectral/helpers.js +1 -1
- package/dist/structure/AtomLegend.svelte +4 -4
- package/dist/structure/AtomLegend.svelte.d.ts +1 -1
- package/dist/structure/Cylinder.svelte +7 -7
- package/dist/structure/Structure.svelte +169 -27
- package/dist/structure/Structure.svelte.d.ts +6 -2
- package/dist/structure/StructureControls.svelte +130 -16
- package/dist/structure/StructureControls.svelte.d.ts +1 -1
- package/dist/structure/StructureInfoPane.svelte +519 -218
- package/dist/structure/StructureInfoPane.svelte.d.ts +2 -1
- package/dist/structure/StructureScene.svelte +399 -68
- package/dist/structure/StructureScene.svelte.d.ts +8 -4
- package/dist/structure/atom-properties.js +3 -1
- package/dist/structure/bond-order-perception.d.ts +13 -0
- package/dist/structure/bond-order-perception.js +367 -0
- package/dist/structure/bonding.d.ts +10 -1
- package/dist/structure/bonding.js +232 -11
- package/dist/structure/export.js +6 -4
- package/dist/structure/index.d.ts +19 -4
- package/dist/structure/index.js +3 -0
- package/dist/structure/label-placement.d.ts +14 -0
- package/dist/structure/label-placement.js +72 -0
- package/dist/structure/parse.d.ts +2 -1
- package/dist/structure/parse.js +25 -36
- package/dist/structure/supercell.js +35 -2
- package/dist/symmetry/SymmetryStats.svelte +1 -1
- package/dist/symmetry/cell-transform.js +15 -1
- package/dist/symmetry/index.js +3 -3
- package/dist/table/HeatmapTable.svelte +3 -3
- package/dist/table/ToggleMenu.svelte +1 -1
- package/dist/trajectory/Trajectory.svelte +2 -2
- package/dist/trajectory/TrajectoryInfoPane.svelte +14 -88
- package/dist/trajectory/extract.js +4 -4
- package/dist/trajectory/frame-reader.js +2 -2
- package/dist/trajectory/parse/ase.js +2 -6
- package/dist/trajectory/parse/hdf5.js +1 -3
- package/dist/trajectory/plotting.js +1 -1
- package/dist/utils.js +1 -1
- package/dist/xrd/calc-xrd.js +1 -1
- package/package.json +22 -37
- package/dist/structure/ferrox-wasm-types.d.ts +0 -46
- package/dist/structure/ferrox-wasm-types.js +0 -18
- package/dist/structure/ferrox-wasm.d.ts +0 -94
- package/dist/structure/ferrox-wasm.js +0 -249
|
@@ -10,9 +10,10 @@ type $$ComponentProps = Omit<HTMLAttributes<HTMLDivElement>, `onclose`> & {
|
|
|
10
10
|
toggle_props?: ComponentProps<typeof DraggablePane>[`toggle_props`];
|
|
11
11
|
pane_props?: ComponentProps<typeof DraggablePane>[`pane_props`];
|
|
12
12
|
highlighted_sites?: number[];
|
|
13
|
+
hovered_site_idx?: number | null;
|
|
13
14
|
selected_sites?: number[];
|
|
14
15
|
sym_data?: MoyoDataset | null;
|
|
15
16
|
};
|
|
16
|
-
declare const StructureInfoPane: import("svelte").Component<$$ComponentProps, {}, "selected_sites" | "pane_open" | "highlighted_sites">;
|
|
17
|
+
declare const StructureInfoPane: import("svelte").Component<$$ComponentProps, {}, "selected_sites" | "pane_open" | "highlighted_sites" | "hovered_site_idx">;
|
|
17
18
|
type StructureInfoPane = ReturnType<typeof StructureInfoPane>;
|
|
18
19
|
export default StructureInfoPane;
|
|
@@ -18,7 +18,14 @@
|
|
|
18
18
|
import { DEFAULTS } from '../settings'
|
|
19
19
|
import { sanitize_html } from '../sanitize'
|
|
20
20
|
import { colors } from '../state.svelte'
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
AnyStructure,
|
|
23
|
+
BondOrder,
|
|
24
|
+
BondPair,
|
|
25
|
+
MeasureMode,
|
|
26
|
+
Site,
|
|
27
|
+
StructureBond,
|
|
28
|
+
} from './'
|
|
22
29
|
import {
|
|
23
30
|
Arrow,
|
|
24
31
|
atomic_radii,
|
|
@@ -48,8 +55,24 @@
|
|
|
48
55
|
import { type Camera, Color, type Mesh, type Scene } from 'three'
|
|
49
56
|
import Bond from './Bond.svelte'
|
|
50
57
|
import type { BondingStrategy } from './bonding'
|
|
51
|
-
import {
|
|
52
|
-
|
|
58
|
+
import {
|
|
59
|
+
BONDING_STRATEGIES,
|
|
60
|
+
get_bond_key,
|
|
61
|
+
get_bond_render_matrices,
|
|
62
|
+
get_explicit_bond_metadata,
|
|
63
|
+
normalize_structure_bond,
|
|
64
|
+
structure_bond_to_bond_pair,
|
|
65
|
+
} from './bonding'
|
|
66
|
+
import {
|
|
67
|
+
CanvasTooltip,
|
|
68
|
+
compose_perceived_bonds,
|
|
69
|
+
perceive_bond_orders,
|
|
70
|
+
} from './index'
|
|
71
|
+
import {
|
|
72
|
+
choose_site_label_offset,
|
|
73
|
+
LABEL_OFFSET_EPS,
|
|
74
|
+
make_label_position_calculator,
|
|
75
|
+
} from './label-placement'
|
|
53
76
|
|
|
54
77
|
type InstancedAtomGroup = {
|
|
55
78
|
element: string
|
|
@@ -59,6 +82,14 @@
|
|
|
59
82
|
atoms: (typeof atom_data)[number][]
|
|
60
83
|
}
|
|
61
84
|
|
|
85
|
+
type BondContextMenu = {
|
|
86
|
+
site_idx_1: number
|
|
87
|
+
site_idx_2: number
|
|
88
|
+
cell_shift?: Vec3
|
|
89
|
+
position: Vec3
|
|
90
|
+
}
|
|
91
|
+
type BondKeyTarget = Pick<StructureBond, `site_idx_1` | `site_idx_2` | `cell_shift`>
|
|
92
|
+
|
|
62
93
|
let pulse_time = $state(0)
|
|
63
94
|
let pulse_opacity = $derived(0.15 + 0.25 * Math.sin(pulse_time * 5))
|
|
64
95
|
$effect(() => {
|
|
@@ -96,9 +127,9 @@
|
|
|
96
127
|
show_site_indices = DEFAULTS.structure.show_site_indices,
|
|
97
128
|
site_label_size = DEFAULTS.structure.site_label_size,
|
|
98
129
|
site_label_offset = $bindable(DEFAULTS.structure.site_label_offset),
|
|
99
|
-
site_label_bg_color =
|
|
100
|
-
site_label_color =
|
|
101
|
-
site_label_padding =
|
|
130
|
+
site_label_bg_color = DEFAULTS.structure.site_label_bg_color,
|
|
131
|
+
site_label_color = DEFAULTS.structure.site_label_color,
|
|
132
|
+
site_label_padding = DEFAULTS.structure.site_label_padding,
|
|
102
133
|
vector_configs = $bindable<Record<string, VectorLayerConfig>>({}),
|
|
103
134
|
vector_scale = DEFAULTS.structure.vector_scale,
|
|
104
135
|
vector_color = DEFAULTS.structure.vector_color,
|
|
@@ -118,6 +149,8 @@
|
|
|
118
149
|
bond_thickness = DEFAULTS.structure.bond_thickness,
|
|
119
150
|
bond_color = DEFAULTS.structure.bond_color,
|
|
120
151
|
bonding_strategy = DEFAULTS.structure.bonding_strategy,
|
|
152
|
+
auto_bond_order = DEFAULTS.structure.auto_bond_order,
|
|
153
|
+
aromatic_display = DEFAULTS.structure.aromatic_display,
|
|
121
154
|
bonding_options = {},
|
|
122
155
|
fov = DEFAULTS.structure.fov,
|
|
123
156
|
initial_zoom = DEFAULTS.structure.initial_zoom,
|
|
@@ -134,6 +167,8 @@
|
|
|
134
167
|
measured_sites = $bindable([]),
|
|
135
168
|
added_bonds = $bindable([]),
|
|
136
169
|
removed_bonds = $bindable([]),
|
|
170
|
+
bond_order_overrides = $bindable([]),
|
|
171
|
+
bond_edits_enabled = true,
|
|
137
172
|
selection_highlight_color = `#6cf0ff`,
|
|
138
173
|
// Active highlight group with different color
|
|
139
174
|
active_sites = $bindable([]),
|
|
@@ -205,6 +240,8 @@
|
|
|
205
240
|
bond_thickness?: number
|
|
206
241
|
bond_color?: string
|
|
207
242
|
bonding_strategy?: BondingStrategy
|
|
243
|
+
auto_bond_order?: boolean
|
|
244
|
+
aromatic_display?: `aromatic` | `kekule`
|
|
208
245
|
bonding_options?: Record<string, unknown>
|
|
209
246
|
fov?: number
|
|
210
247
|
ambient_light?: number
|
|
@@ -224,8 +261,10 @@
|
|
|
224
261
|
measure_mode?: MeasureMode
|
|
225
262
|
selected_sites?: number[]
|
|
226
263
|
measured_sites?: number[]
|
|
227
|
-
added_bonds?: [
|
|
228
|
-
removed_bonds?: [
|
|
264
|
+
added_bonds?: StructureBond[]
|
|
265
|
+
removed_bonds?: StructureBond[]
|
|
266
|
+
bond_order_overrides?: StructureBond[]
|
|
267
|
+
bond_edits_enabled?: boolean
|
|
229
268
|
selection_highlight_color?: string
|
|
230
269
|
// Support for active highlight group with different color
|
|
231
270
|
active_sites?: number[]
|
|
@@ -286,6 +325,11 @@
|
|
|
286
325
|
let canvas_cursor = $derived.by(() => {
|
|
287
326
|
if (measure_mode === `edit-atoms` && add_atom_mode) return `crosshair`
|
|
288
327
|
if (hovered_idx != null) {
|
|
328
|
+
if (measure_mode === `edit-bonds`) {
|
|
329
|
+
return bond_edits_enabled && is_editable_bond_site(hovered_idx)
|
|
330
|
+
? `pointer`
|
|
331
|
+
: `not-allowed`
|
|
332
|
+
}
|
|
289
333
|
if (measure_mode === `edit-atoms`) {
|
|
290
334
|
const site = structure?.sites?.[hovered_idx]
|
|
291
335
|
if (site?.properties?.orig_site_idx != null) return `not-allowed`
|
|
@@ -312,36 +356,154 @@
|
|
|
312
356
|
// snaps to the new wrapped centroid.
|
|
313
357
|
let frozen_centroid = $state<Vec3 | null>(null)
|
|
314
358
|
|
|
315
|
-
const
|
|
316
|
-
|
|
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
|
+
let bond_context_menu = $state<BondContextMenu | null>(null)
|
|
366
|
+
// Threlte/HTML pointer events can close the visible menu before a button
|
|
367
|
+
// handler runs, so keep the target bond separately for menu actions.
|
|
368
|
+
let bond_context_target: BondContextMenu | null = null
|
|
369
|
+
|
|
370
|
+
function close_bond_context_menu() {
|
|
371
|
+
bond_context_menu = null
|
|
372
|
+
bond_context_target = null
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const make_bond_record = (
|
|
376
|
+
site_idx_1: number,
|
|
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)
|
|
381
|
+
|
|
382
|
+
const bond_key_for = (bond: BondKeyTarget): string =>
|
|
383
|
+
get_bond_key(bond.site_idx_1, bond.site_idx_2, bond.cell_shift)
|
|
384
|
+
|
|
385
|
+
const matches_bond_key = (bond: BondKeyTarget, key: string): boolean =>
|
|
386
|
+
bond_key_for(bond) === key
|
|
387
|
+
|
|
388
|
+
function is_editable_bond_site(site_idx: number): boolean {
|
|
389
|
+
return structure?.sites?.[site_idx]?.properties?.orig_site_idx == null
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const can_edit_bond = (bond: BondKeyTarget): boolean =>
|
|
393
|
+
bond_edits_enabled &&
|
|
394
|
+
is_editable_bond_site(bond.site_idx_1) &&
|
|
395
|
+
is_editable_bond_site(bond.site_idx_2)
|
|
396
|
+
|
|
397
|
+
const format_bond_order = (order: BondOrder | undefined): string =>
|
|
398
|
+
order === undefined ? `1` : `${order}`
|
|
399
|
+
|
|
400
|
+
function get_current_bond_order(
|
|
401
|
+
site_idx_1: number,
|
|
402
|
+
site_idx_2: number,
|
|
403
|
+
cell_shift?: Vec3,
|
|
404
|
+
): BondOrder | undefined {
|
|
405
|
+
const key = get_bond_key(site_idx_1, site_idx_2, cell_shift)
|
|
406
|
+
return bond_order_overrides.find((bond) => matches_bond_key(bond, key))?.order ??
|
|
407
|
+
added_bonds.find((bond) => matches_bond_key(bond, key))?.order ??
|
|
408
|
+
filtered_bond_pairs.find((bond) => matches_bond_key(bond, key))?.bond_order
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const midpoint = (pos_1: Vec3, pos_2: Vec3): Vec3 => [
|
|
412
|
+
(pos_1[0] + pos_2[0]) / 2,
|
|
413
|
+
(pos_1[1] + pos_2[1]) / 2,
|
|
414
|
+
(pos_1[2] + pos_2[2]) / 2,
|
|
415
|
+
]
|
|
416
|
+
|
|
417
|
+
let label_screen_margin = $derived(site_label_size * 10 + site_label_padding)
|
|
418
|
+
|
|
419
|
+
function open_bond_context_menu(bond: BondPair) {
|
|
420
|
+
if (!can_edit_bond(bond)) return
|
|
421
|
+
bond_context_target = {
|
|
422
|
+
site_idx_1: bond.site_idx_1,
|
|
423
|
+
site_idx_2: bond.site_idx_2,
|
|
424
|
+
cell_shift: bond.cell_shift,
|
|
425
|
+
position: midpoint(bond.pos_1, bond.pos_2),
|
|
426
|
+
}
|
|
427
|
+
bond_context_menu = bond_context_target
|
|
428
|
+
}
|
|
317
429
|
|
|
318
430
|
// Toggle a bond between two atoms: cycles through add → remove → restore states
|
|
319
|
-
function toggle_bond(site_1: number, site_2: number) {
|
|
320
|
-
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
const added_idx = added_bonds.findIndex(match)
|
|
326
|
-
if (added_idx >= 0) {
|
|
327
|
-
added_bonds = added_bonds.toSpliced(added_idx, 1)
|
|
431
|
+
function toggle_bond(site_1: number, site_2: number, cell_shift?: Vec3) {
|
|
432
|
+
if (!can_edit_bond({ site_idx_1: site_1, site_idx_2: site_2, cell_shift })) return
|
|
433
|
+
const record = make_bond_record(site_1, site_2, 1, cell_shift)
|
|
434
|
+
const key = bond_key_for(record)
|
|
435
|
+
if (added_bonds.some((bond) => matches_bond_key(bond, key))) {
|
|
436
|
+
added_bonds = added_bonds.filter((bond) => !matches_bond_key(bond, key))
|
|
328
437
|
return
|
|
329
438
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (removed_idx >= 0) {
|
|
333
|
-
removed_bonds = removed_bonds.toSpliced(removed_idx, 1)
|
|
439
|
+
if (removed_bonds.some((bond) => matches_bond_key(bond, key))) {
|
|
440
|
+
removed_bonds = removed_bonds.filter((bond) => !matches_bond_key(bond, key))
|
|
334
441
|
return
|
|
335
442
|
}
|
|
443
|
+
const has_calculated_bond = bond_pairs.some((bond) => matches_bond_key(bond, key))
|
|
444
|
+
if (bond_order_overrides.some((bond) => matches_bond_key(bond, key))) {
|
|
445
|
+
bond_order_overrides = bond_order_overrides.filter((bond) =>
|
|
446
|
+
!matches_bond_key(bond, key)
|
|
447
|
+
)
|
|
448
|
+
if (has_calculated_bond) removed_bonds = [...removed_bonds, record]
|
|
449
|
+
return
|
|
450
|
+
}
|
|
451
|
+
if (has_calculated_bond) removed_bonds = [...removed_bonds, record]
|
|
452
|
+
else added_bonds = [...added_bonds, record]
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function set_bond_order(
|
|
456
|
+
site_idx_1: number,
|
|
457
|
+
site_idx_2: number,
|
|
458
|
+
order: BondOrder,
|
|
459
|
+
cell_shift?: Vec3,
|
|
460
|
+
) {
|
|
461
|
+
if (!can_edit_bond({ site_idx_1, site_idx_2, cell_shift })) return
|
|
462
|
+
const key = get_bond_key(site_idx_1, site_idx_2, cell_shift)
|
|
463
|
+
const new_record = make_bond_record(site_idx_1, site_idx_2, order, cell_shift)
|
|
464
|
+
added_bonds = added_bonds.filter((bond) => !matches_bond_key(bond, key))
|
|
465
|
+
const has_calculated_bond = bond_pairs.some((bond) => matches_bond_key(bond, key))
|
|
466
|
+
if (has_calculated_bond) {
|
|
467
|
+
bond_order_overrides = [
|
|
468
|
+
...bond_order_overrides.filter((bond) => !matches_bond_key(bond, key)),
|
|
469
|
+
new_record,
|
|
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()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function set_context_bond_order(order: BondOrder) {
|
|
479
|
+
const menu = bond_context_target ?? bond_context_menu
|
|
480
|
+
if (!menu) return
|
|
481
|
+
set_bond_order(menu.site_idx_1, menu.site_idx_2, order, menu.cell_shift)
|
|
482
|
+
}
|
|
336
483
|
|
|
337
|
-
|
|
338
|
-
|
|
484
|
+
function remove_bond(site_idx_1: number, site_idx_2: number, cell_shift?: Vec3) {
|
|
485
|
+
if (!can_edit_bond({ site_idx_1, site_idx_2, cell_shift })) return
|
|
486
|
+
const key = get_bond_key(site_idx_1, site_idx_2, cell_shift)
|
|
487
|
+
added_bonds = added_bonds.filter((bond) => !matches_bond_key(bond, key))
|
|
488
|
+
bond_order_overrides = bond_order_overrides.filter((bond) =>
|
|
489
|
+
!matches_bond_key(bond, key)
|
|
490
|
+
)
|
|
339
491
|
if (
|
|
340
|
-
bond_pairs.some((bond) =>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
+
}
|
|
502
|
+
|
|
503
|
+
function remove_context_bond() {
|
|
504
|
+
const menu = bond_context_target ?? bond_context_menu
|
|
505
|
+
if (!menu) return
|
|
506
|
+
remove_bond(menu.site_idx_1, menu.site_idx_2, menu.cell_shift)
|
|
345
507
|
}
|
|
346
508
|
|
|
347
509
|
// Deduplicate clicks: when a highlight sphere and the underlying atom both
|
|
@@ -359,6 +521,7 @@
|
|
|
359
521
|
}
|
|
360
522
|
|
|
361
523
|
if (measure_mode === `edit-bonds`) {
|
|
524
|
+
if (!bond_edits_enabled || !is_editable_bond_site(site_index)) return
|
|
362
525
|
// In edit-bonds mode, select atoms to add/remove bonds between them
|
|
363
526
|
const new_sites = measured_sites.includes(site_index)
|
|
364
527
|
? measured_sites.filter((idx) => idx !== site_index)
|
|
@@ -417,6 +580,17 @@
|
|
|
417
580
|
? selected_sites.filter((idx) => idx !== site_index)
|
|
418
581
|
: [...selected_sites, site_index]
|
|
419
582
|
}
|
|
583
|
+
|
|
584
|
+
$effect(() => {
|
|
585
|
+
void structure
|
|
586
|
+
void measure_mode
|
|
587
|
+
void bond_edits_enabled
|
|
588
|
+
untrack(() => {
|
|
589
|
+
close_bond_context_menu()
|
|
590
|
+
hovered_bond_key = null
|
|
591
|
+
})
|
|
592
|
+
})
|
|
593
|
+
|
|
420
594
|
$effect(() => {
|
|
421
595
|
const count = structure?.sites?.length ?? 0
|
|
422
596
|
if (count <= 0) {
|
|
@@ -604,45 +778,89 @@
|
|
|
604
778
|
return has_visible_element && prop_visible
|
|
605
779
|
}
|
|
606
780
|
|
|
781
|
+
// Perception layer: bond_pairs with optional bond-order perception applied.
|
|
782
|
+
// Off by default (pass-through). Manual overrides are applied downstream in
|
|
783
|
+
// filtered_bond_pairs, so they still win over perceived orders.
|
|
784
|
+
let perceived_bond_pairs: BondPair[] = $derived.by(() => {
|
|
785
|
+
if (!auto_bond_order || !structure?.sites || bond_pairs.length === 0) {
|
|
786
|
+
return bond_pairs
|
|
787
|
+
}
|
|
788
|
+
const total_charge = (`charge` in structure ? structure.charge : 0) ?? 0
|
|
789
|
+
const perceived = perceive_bond_orders(structure.sites, bond_pairs, {
|
|
790
|
+
total_charge,
|
|
791
|
+
})
|
|
792
|
+
// Explicit structure.properties.bonds are user-authoritative and must
|
|
793
|
+
// never be clobbered by perception. Composition + precedence is a pure,
|
|
794
|
+
// unit-tested helper (see compose_perceived_bonds).
|
|
795
|
+
return compose_perceived_bonds(
|
|
796
|
+
perceived,
|
|
797
|
+
get_explicit_bond_metadata(structure),
|
|
798
|
+
aromatic_display,
|
|
799
|
+
)
|
|
800
|
+
})
|
|
801
|
+
|
|
607
802
|
let filtered_bond_pairs = $derived.by(() => {
|
|
608
|
-
if (!structure?.sites) return
|
|
803
|
+
if (!structure?.sites) return perceived_bond_pairs
|
|
609
804
|
|
|
610
805
|
// Build set of removed bond keys for efficient lookup
|
|
611
806
|
const removed_keys = new Set(
|
|
612
|
-
removed_bonds.map(
|
|
807
|
+
removed_bonds.map(bond_key_for),
|
|
808
|
+
)
|
|
809
|
+
const order_overrides = new Map(
|
|
810
|
+
bond_order_overrides.map((bond) => [bond_key_for(bond), bond.order]),
|
|
613
811
|
)
|
|
614
812
|
|
|
615
813
|
// Filter calculated bonds: exclude removed and hidden
|
|
616
|
-
const calculated =
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
814
|
+
const calculated = perceived_bond_pairs
|
|
815
|
+
.filter((bond) => {
|
|
816
|
+
if (removed_keys.has(bond_key_for(bond))) return false
|
|
817
|
+
return is_site_visible(bond.site_idx_1) && is_site_visible(bond.site_idx_2)
|
|
818
|
+
})
|
|
819
|
+
.map((bond) => {
|
|
820
|
+
const override = order_overrides.get(bond_key_for(bond))
|
|
821
|
+
return override === undefined ? bond : { ...bond, bond_order: override }
|
|
822
|
+
})
|
|
620
823
|
|
|
621
824
|
// Create BondPair objects for manually added bonds
|
|
622
|
-
const added: BondPair[] =
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
825
|
+
const added: BondPair[] = []
|
|
826
|
+
for (const added_bond of added_bonds) {
|
|
827
|
+
const { site_idx_1: idx_i, site_idx_2: idx_j } = added_bond
|
|
828
|
+
if (!is_site_visible(idx_i) || !is_site_visible(idx_j)) continue
|
|
829
|
+
added.push(structure_bond_to_bond_pair(structure, added_bond))
|
|
830
|
+
}
|
|
628
831
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
const dist = math.euclidean_dist(pos_1, pos_2)
|
|
832
|
+
return [...calculated, ...added]
|
|
833
|
+
})
|
|
632
834
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
site_idx_1: idx_i,
|
|
637
|
-
site_idx_2: idx_j,
|
|
638
|
-
bond_length: dist,
|
|
639
|
-
strength: 1.0,
|
|
640
|
-
transform_matrix: compute_bond_transform(pos_1, pos_2),
|
|
641
|
-
}
|
|
642
|
-
})
|
|
643
|
-
.filter((bond): bond is BondPair => bond !== null)
|
|
835
|
+
let editable_bond_pairs = $derived(
|
|
836
|
+
bond_edits_enabled ? filtered_bond_pairs.filter(can_edit_bond) : [],
|
|
837
|
+
)
|
|
644
838
|
|
|
645
|
-
|
|
839
|
+
let smart_site_label_offsets = $derived.by(() => {
|
|
840
|
+
const offsets = new SvelteMap<number, Vec3>()
|
|
841
|
+
if (filtered_bond_pairs.length === 0) return offsets
|
|
842
|
+
|
|
843
|
+
const bond_directions_by_site = new SvelteMap<number, Vec3[]>()
|
|
844
|
+
const add_bond_direction = (site_idx: number, pos_1: Vec3, pos_2: Vec3) => {
|
|
845
|
+
const direction = math.normalize_vec3(
|
|
846
|
+
math.subtract(pos_2, pos_1),
|
|
847
|
+
[0, 0, 0],
|
|
848
|
+
)
|
|
849
|
+
if (Math.hypot(...direction) < LABEL_OFFSET_EPS) return
|
|
850
|
+
bond_directions_by_site.set(site_idx, [
|
|
851
|
+
...(bond_directions_by_site.get(site_idx) ?? []),
|
|
852
|
+
direction,
|
|
853
|
+
])
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
for (const { site_idx_1, site_idx_2, pos_1, pos_2 } of filtered_bond_pairs) {
|
|
857
|
+
add_bond_direction(site_idx_1, pos_1, pos_2)
|
|
858
|
+
add_bond_direction(site_idx_2, pos_2, pos_1)
|
|
859
|
+
}
|
|
860
|
+
for (const [site_idx, bond_directions] of bond_directions_by_site) {
|
|
861
|
+
offsets.set(site_idx, choose_site_label_offset(bond_directions, site_label_offset))
|
|
862
|
+
}
|
|
863
|
+
return offsets
|
|
646
864
|
})
|
|
647
865
|
|
|
648
866
|
let instanced_bond_groups = $derived.by(() => {
|
|
@@ -673,8 +891,9 @@
|
|
|
673
891
|
|
|
674
892
|
const color_start = get_majority_color(site_a)
|
|
675
893
|
const color_end = get_majority_color(site_b)
|
|
676
|
-
const
|
|
677
|
-
|
|
894
|
+
for (const matrix of get_bond_render_matrices(bond_data, bond_thickness)) {
|
|
895
|
+
group.instances.push({ matrix, color_start, color_end })
|
|
896
|
+
}
|
|
678
897
|
}
|
|
679
898
|
|
|
680
899
|
return group.instances.length > 0 ? [group] : []
|
|
@@ -920,6 +1139,7 @@
|
|
|
920
1139
|
onstart: () => {
|
|
921
1140
|
camera_is_moving = true
|
|
922
1141
|
hovered_idx = null
|
|
1142
|
+
bond_context_menu = null
|
|
923
1143
|
},
|
|
924
1144
|
onend: () => {
|
|
925
1145
|
camera_is_moving = false
|
|
@@ -944,8 +1164,17 @@
|
|
|
944
1164
|
|
|
945
1165
|
{#snippet site_label_snippet(position: Vec3, site_idx: number)}
|
|
946
1166
|
{@const site = structure!.sites[site_idx]}
|
|
947
|
-
{@const
|
|
948
|
-
<extras.HTML
|
|
1167
|
+
{@const visual_radius = (radius_by_site_idx.get(site_idx) ?? 1) * 0.5}
|
|
1168
|
+
<extras.HTML
|
|
1169
|
+
center
|
|
1170
|
+
position={position}
|
|
1171
|
+
calculatePosition={make_label_position_calculator(
|
|
1172
|
+
position,
|
|
1173
|
+
() => smart_site_label_offsets.get(site_idx) ?? site_label_offset,
|
|
1174
|
+
visual_radius,
|
|
1175
|
+
label_screen_margin,
|
|
1176
|
+
)}
|
|
1177
|
+
>
|
|
949
1178
|
{#if atom_label}
|
|
950
1179
|
{@render atom_label({ site, site_idx })}
|
|
951
1180
|
{:else}
|
|
@@ -1168,12 +1397,12 @@
|
|
|
1168
1397
|
{/if}
|
|
1169
1398
|
|
|
1170
1399
|
<!-- Clickable bond hit-test cylinders in edit-bonds mode -->
|
|
1171
|
-
{#if measure_mode === `edit-bonds` &&
|
|
1172
|
-
{#each
|
|
1400
|
+
{#if measure_mode === `edit-bonds` && editable_bond_pairs.length > 0}
|
|
1401
|
+
{#each editable_bond_pairs as
|
|
1173
1402
|
bond
|
|
1174
|
-
(`bond-hit-${bond
|
|
1403
|
+
(`bond-hit-${bond_key_for(bond)}`)
|
|
1175
1404
|
}
|
|
1176
|
-
{@const bond_key =
|
|
1405
|
+
{@const bond_key = bond_key_for(bond)}
|
|
1177
1406
|
{@const is_hovered = hovered_bond_key === bond_key}
|
|
1178
1407
|
<T.Mesh
|
|
1179
1408
|
matrixAutoUpdate={false}
|
|
@@ -1181,13 +1410,19 @@
|
|
|
1181
1410
|
ref.matrix.fromArray(bond.transform_matrix)
|
|
1182
1411
|
ref.matrixWorldNeedsUpdate = true
|
|
1183
1412
|
}}
|
|
1184
|
-
|
|
1413
|
+
onpointerdown={(event: PointerEvent & { nativeEvent?: PointerEvent }) => {
|
|
1414
|
+
if (event.nativeEvent?.button === 2) return
|
|
1185
1415
|
event.stopPropagation()
|
|
1186
|
-
toggle_bond(bond.site_idx_1, bond.site_idx_2)
|
|
1416
|
+
toggle_bond(bond.site_idx_1, bond.site_idx_2, bond.cell_shift)
|
|
1187
1417
|
measured_sites = []
|
|
1188
1418
|
selected_sites = []
|
|
1189
1419
|
hovered_bond_key = null
|
|
1190
1420
|
}}
|
|
1421
|
+
oncontextmenu={(event: MouseEvent & { nativeEvent?: MouseEvent }) => {
|
|
1422
|
+
event.nativeEvent?.preventDefault()
|
|
1423
|
+
event.stopPropagation?.()
|
|
1424
|
+
open_bond_context_menu(bond)
|
|
1425
|
+
}}
|
|
1191
1426
|
onpointerenter={() => (hovered_bond_key = bond_key)}
|
|
1192
1427
|
onpointerleave={() => (hovered_bond_key = null)}
|
|
1193
1428
|
>
|
|
@@ -1202,6 +1437,70 @@
|
|
|
1202
1437
|
{/each}
|
|
1203
1438
|
{/if}
|
|
1204
1439
|
|
|
1440
|
+
{#if measure_mode === `edit-bonds` && bond_context_menu}
|
|
1441
|
+
{@const current_order = get_current_bond_order(
|
|
1442
|
+
bond_context_menu.site_idx_1,
|
|
1443
|
+
bond_context_menu.site_idx_2,
|
|
1444
|
+
bond_context_menu.cell_shift,
|
|
1445
|
+
)}
|
|
1446
|
+
<extras.HTML center position={bond_context_menu.position}>
|
|
1447
|
+
<div class="bond-context-menu">
|
|
1448
|
+
<strong>Bond Order ({format_bond_order(current_order)})</strong>
|
|
1449
|
+
{#each BOND_ORDER_OPTIONS as { order, label } (label)}
|
|
1450
|
+
<button
|
|
1451
|
+
type="button"
|
|
1452
|
+
onpointerdown={(event: PointerEvent) => {
|
|
1453
|
+
event.preventDefault()
|
|
1454
|
+
event.stopPropagation()
|
|
1455
|
+
set_context_bond_order(order)
|
|
1456
|
+
}}
|
|
1457
|
+
onkeydown={(event: KeyboardEvent) => {
|
|
1458
|
+
if (event.key !== `Enter` && event.key !== ` `) return
|
|
1459
|
+
event.preventDefault()
|
|
1460
|
+
event.stopPropagation()
|
|
1461
|
+
set_context_bond_order(order)
|
|
1462
|
+
}}
|
|
1463
|
+
>
|
|
1464
|
+
{label}
|
|
1465
|
+
</button>
|
|
1466
|
+
{/each}
|
|
1467
|
+
<button
|
|
1468
|
+
type="button"
|
|
1469
|
+
class="remove"
|
|
1470
|
+
onpointerdown={(event: PointerEvent) => {
|
|
1471
|
+
event.preventDefault()
|
|
1472
|
+
event.stopPropagation()
|
|
1473
|
+
remove_context_bond()
|
|
1474
|
+
}}
|
|
1475
|
+
onkeydown={(event: KeyboardEvent) => {
|
|
1476
|
+
if (event.key !== `Enter` && event.key !== ` `) return
|
|
1477
|
+
event.preventDefault()
|
|
1478
|
+
event.stopPropagation()
|
|
1479
|
+
remove_context_bond()
|
|
1480
|
+
}}
|
|
1481
|
+
>
|
|
1482
|
+
Remove
|
|
1483
|
+
</button>
|
|
1484
|
+
<button
|
|
1485
|
+
type="button"
|
|
1486
|
+
onpointerdown={(event: PointerEvent) => {
|
|
1487
|
+
event.preventDefault()
|
|
1488
|
+
event.stopPropagation()
|
|
1489
|
+
close_bond_context_menu()
|
|
1490
|
+
}}
|
|
1491
|
+
onkeydown={(event: KeyboardEvent) => {
|
|
1492
|
+
if (event.key !== `Enter` && event.key !== ` `) return
|
|
1493
|
+
event.preventDefault()
|
|
1494
|
+
event.stopPropagation()
|
|
1495
|
+
close_bond_context_menu()
|
|
1496
|
+
}}
|
|
1497
|
+
>
|
|
1498
|
+
Close
|
|
1499
|
+
</button>
|
|
1500
|
+
</div>
|
|
1501
|
+
</extras.HTML>
|
|
1502
|
+
{/if}
|
|
1503
|
+
|
|
1205
1504
|
<!-- highlight hovered, active and selected sites -->
|
|
1206
1505
|
{#each [
|
|
1207
1506
|
{
|
|
@@ -1275,7 +1574,8 @@
|
|
|
1275
1574
|
{/if}
|
|
1276
1575
|
|
|
1277
1576
|
<!-- hovered site tooltip -->
|
|
1278
|
-
{#if hovered_site && !camera_is_moving &&
|
|
1577
|
+
{#if hovered_site && !camera_is_moving &&
|
|
1578
|
+
(active_tooltip === `atom` || active_sites.includes(hovered_idx ?? -1))}
|
|
1279
1579
|
{@const abc = hovered_site.abc.map((val) => format_num(val, float_fmt)).join(
|
|
1280
1580
|
`, `,
|
|
1281
1581
|
)}
|
|
@@ -1557,6 +1857,37 @@
|
|
|
1557
1857
|
font-size: var(--canvas-tooltip-font-size, clamp(8pt, 2cqmin, 18pt));
|
|
1558
1858
|
box-shadow: var(--measure-label-shadow, 0 1px 6px rgba(0, 0, 0, 0.2));
|
|
1559
1859
|
}
|
|
1860
|
+
.bond-context-menu {
|
|
1861
|
+
display: grid;
|
|
1862
|
+
min-width: 8rem;
|
|
1863
|
+
gap: 2pt;
|
|
1864
|
+
padding: 5pt;
|
|
1865
|
+
border-radius: var(--border-radius, 3pt);
|
|
1866
|
+
background: var(--surface-bg, Canvas);
|
|
1867
|
+
color: var(--text-color, currentColor);
|
|
1868
|
+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25);
|
|
1869
|
+
pointer-events: auto;
|
|
1870
|
+
strong {
|
|
1871
|
+
font-size: 0.85em;
|
|
1872
|
+
padding: 0 2pt 2pt;
|
|
1873
|
+
white-space: nowrap;
|
|
1874
|
+
}
|
|
1875
|
+
button {
|
|
1876
|
+
border: none;
|
|
1877
|
+
border-radius: var(--border-radius, 3pt);
|
|
1878
|
+
background: transparent;
|
|
1879
|
+
color: inherit;
|
|
1880
|
+
cursor: pointer;
|
|
1881
|
+
padding: 2pt 5pt;
|
|
1882
|
+
text-align: left;
|
|
1883
|
+
}
|
|
1884
|
+
button:hover {
|
|
1885
|
+
background: color-mix(in srgb, currentColor 10%, transparent);
|
|
1886
|
+
}
|
|
1887
|
+
button.remove {
|
|
1888
|
+
color: var(--error-color, #f44336);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1560
1891
|
.selection-label {
|
|
1561
1892
|
display: inline-flex;
|
|
1562
1893
|
align-items: center;
|
|
@@ -3,7 +3,7 @@ import type { ElementSymbol } from '../element';
|
|
|
3
3
|
import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types';
|
|
4
4
|
import type { Vec3 } from '../math';
|
|
5
5
|
import type { CameraProjection, ShowBonds, VectorColorMode, VectorLayerConfig } from '../settings';
|
|
6
|
-
import type { AnyStructure, MeasureMode, Site } from './';
|
|
6
|
+
import type { AnyStructure, MeasureMode, Site, StructureBond } from './';
|
|
7
7
|
import { Lattice } from './';
|
|
8
8
|
import type { AtomColorConfig } from './atom-properties';
|
|
9
9
|
import type { MoyoDataset } from '@spglib/moyo-wasm';
|
|
@@ -51,6 +51,8 @@ type $$ComponentProps = {
|
|
|
51
51
|
bond_thickness?: number;
|
|
52
52
|
bond_color?: string;
|
|
53
53
|
bonding_strategy?: BondingStrategy;
|
|
54
|
+
auto_bond_order?: boolean;
|
|
55
|
+
aromatic_display?: `aromatic` | `kekule`;
|
|
54
56
|
bonding_options?: Record<string, unknown>;
|
|
55
57
|
fov?: number;
|
|
56
58
|
ambient_light?: number;
|
|
@@ -72,8 +74,10 @@ type $$ComponentProps = {
|
|
|
72
74
|
measure_mode?: MeasureMode;
|
|
73
75
|
selected_sites?: number[];
|
|
74
76
|
measured_sites?: number[];
|
|
75
|
-
added_bonds?: [
|
|
76
|
-
removed_bonds?: [
|
|
77
|
+
added_bonds?: StructureBond[];
|
|
78
|
+
removed_bonds?: StructureBond[];
|
|
79
|
+
bond_order_overrides?: StructureBond[];
|
|
80
|
+
bond_edits_enabled?: boolean;
|
|
77
81
|
selection_highlight_color?: string;
|
|
78
82
|
active_sites?: number[];
|
|
79
83
|
active_highlight_color?: string;
|
|
@@ -99,6 +103,6 @@ type $$ComponentProps = {
|
|
|
99
103
|
volumetric_data?: VolumetricData;
|
|
100
104
|
isosurface_settings?: IsosurfaceSettings;
|
|
101
105
|
};
|
|
102
|
-
declare const StructureScene: import("svelte").Component<$$ComponentProps, {}, "cursor" | "site_label_offset" | "vector_configs" | "camera" | "scene" | "orbit_controls" | "
|
|
106
|
+
declare const StructureScene: import("svelte").Component<$$ComponentProps, {}, "cursor" | "site_label_offset" | "vector_configs" | "camera" | "scene" | "orbit_controls" | "site_radius_overrides" | "hovered_idx" | "hovered_site" | "camera_is_moving" | "selected_sites" | "measured_sites" | "added_bonds" | "removed_bonds" | "bond_order_overrides" | "active_sites" | "rotation_target_ref" | "initial_computed_zoom" | "hidden_elements" | "hidden_prop_vals" | "element_radius_overrides" | "add_atom_mode" | "add_element" | "dragging_atoms">;
|
|
103
107
|
type StructureScene = ReturnType<typeof StructureScene>;
|
|
104
108
|
export default StructureScene;
|