matterviz 0.1.12 → 0.1.13

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 (195) hide show
  1. package/dist/ContextMenu.svelte +1 -1
  2. package/dist/ContextMenu.svelte.d.ts +3 -3
  3. package/dist/DraggablePane.svelte +10 -5
  4. package/dist/DraggablePane.svelte.d.ts +3 -3
  5. package/dist/FilePicker.svelte.d.ts +3 -3
  6. package/dist/Icon.svelte.d.ts +3 -3
  7. package/dist/Nav.svelte +76 -31
  8. package/dist/SettingsSection.svelte.d.ts +3 -3
  9. package/dist/Spinner.svelte +18 -29
  10. package/dist/Spinner.svelte.d.ts +3 -3
  11. package/dist/app.css +4 -0
  12. package/dist/bands/Bands.svelte +216 -0
  13. package/dist/bands/Bands.svelte.d.ts +16 -0
  14. package/dist/bands/BandsAndDos.svelte +26 -0
  15. package/dist/bands/BandsAndDos.svelte.d.ts +16 -0
  16. package/dist/bands/Dos.svelte +124 -0
  17. package/dist/bands/Dos.svelte.d.ts +18 -0
  18. package/dist/bands/helpers.d.ts +11 -0
  19. package/dist/bands/helpers.js +195 -0
  20. package/dist/bands/index.d.ts +5 -0
  21. package/dist/bands/index.js +6 -0
  22. package/dist/bands/types.d.ts +54 -0
  23. package/dist/bands/types.js +2 -0
  24. package/dist/brillouin/BrillouinZone.svelte +373 -0
  25. package/dist/brillouin/BrillouinZone.svelte.d.ts +57 -0
  26. package/dist/brillouin/BrillouinZoneControls.svelte +88 -0
  27. package/dist/brillouin/BrillouinZoneControls.svelte.d.ts +14 -0
  28. package/dist/brillouin/BrillouinZoneExportPane.svelte +129 -0
  29. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +15 -0
  30. package/dist/brillouin/BrillouinZoneInfoPane.svelte +115 -0
  31. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +13 -0
  32. package/dist/brillouin/BrillouinZoneScene.svelte +180 -0
  33. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +34 -0
  34. package/dist/brillouin/compute.d.ts +7 -0
  35. package/dist/brillouin/compute.js +215 -0
  36. package/dist/brillouin/index.d.ts +7 -0
  37. package/dist/brillouin/index.js +7 -0
  38. package/dist/brillouin/types.d.ts +23 -0
  39. package/dist/brillouin/types.js +1 -0
  40. package/dist/composition/BarChart.svelte.d.ts +3 -3
  41. package/dist/composition/BubbleChart.svelte +7 -6
  42. package/dist/composition/BubbleChart.svelte.d.ts +3 -3
  43. package/dist/composition/Composition.svelte.d.ts +3 -3
  44. package/dist/composition/PieChart.svelte +1 -1
  45. package/dist/composition/PieChart.svelte.d.ts +3 -3
  46. package/dist/composition/parse.d.ts +3 -3
  47. package/dist/coordination/CoordinationBarPlot.svelte +241 -0
  48. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +29 -0
  49. package/dist/coordination/calc-coordination.d.ts +15 -0
  50. package/dist/coordination/calc-coordination.js +66 -0
  51. package/dist/coordination/index.d.ts +8 -0
  52. package/dist/coordination/index.js +7 -0
  53. package/dist/element/BohrAtom.svelte.d.ts +3 -3
  54. package/dist/element/ElementHeading.svelte.d.ts +3 -3
  55. package/dist/element/ElementPhoto.svelte.d.ts +3 -3
  56. package/dist/element/ElementStats.svelte.d.ts +3 -3
  57. package/dist/element/ElementTile.svelte +1 -1
  58. package/dist/element/ElementTile.svelte.d.ts +4 -4
  59. package/dist/element/Nucleus.svelte.d.ts +3 -3
  60. package/dist/icons.d.ts +4 -0
  61. package/dist/icons.js +4 -0
  62. package/dist/index.d.ts +5 -1
  63. package/dist/index.js +5 -1
  64. package/dist/io/export.d.ts +1 -6
  65. package/dist/io/export.js +12 -12
  66. package/dist/labels.d.ts +19 -0
  67. package/dist/labels.js +59 -0
  68. package/dist/math.d.ts +7 -2
  69. package/dist/math.js +24 -12
  70. package/dist/periodic-table/PeriodicTable.svelte.d.ts +3 -3
  71. package/dist/periodic-table/PeriodicTableControls.svelte.d.ts +3 -3
  72. package/dist/periodic-table/PropertySelect.svelte +1 -1
  73. package/dist/periodic-table/PropertySelect.svelte.d.ts +5 -4
  74. package/dist/periodic-table/TableInset.svelte.d.ts +3 -3
  75. package/dist/phase-diagram/PhaseDiagram2D.svelte +44 -107
  76. package/dist/phase-diagram/PhaseDiagram2D.svelte.d.ts +7 -27
  77. package/dist/phase-diagram/PhaseDiagram3D.svelte +67 -170
  78. package/dist/phase-diagram/PhaseDiagram3D.svelte.d.ts +7 -31
  79. package/dist/phase-diagram/PhaseDiagram4D.svelte +87 -145
  80. package/dist/phase-diagram/PhaseDiagram4D.svelte.d.ts +5 -28
  81. package/dist/phase-diagram/PhaseDiagramControls.svelte +9 -9
  82. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +6 -6
  83. package/dist/phase-diagram/PhaseDiagramInfoPane.svelte +4 -4
  84. package/dist/phase-diagram/PhaseDiagramInfoPane.svelte.d.ts +5 -5
  85. package/dist/phase-diagram/PhaseDiagramStats.svelte +9 -16
  86. package/dist/phase-diagram/PhaseDiagramStats.svelte.d.ts +3 -3
  87. package/dist/phase-diagram/StructurePopup.svelte.d.ts +3 -3
  88. package/dist/phase-diagram/barycentric-coords.js +13 -24
  89. package/dist/phase-diagram/helpers.d.ts +12 -14
  90. package/dist/phase-diagram/helpers.js +70 -23
  91. package/dist/phase-diagram/index.d.ts +58 -0
  92. package/dist/phase-diagram/index.js +44 -0
  93. package/dist/phase-diagram/thermodynamics.js +2 -3
  94. package/dist/phase-diagram/types.d.ts +3 -8
  95. package/dist/plot/BarPlot.svelte +474 -240
  96. package/dist/plot/BarPlot.svelte.d.ts +16 -37
  97. package/dist/plot/BarPlotControls.svelte +33 -201
  98. package/dist/plot/BarPlotControls.svelte.d.ts +11 -22
  99. package/dist/plot/ColorBar.svelte.d.ts +3 -3
  100. package/dist/plot/ColorScaleSelect.svelte +1 -1
  101. package/dist/plot/ColorScaleSelect.svelte.d.ts +3 -3
  102. package/dist/plot/ElementScatter.svelte +5 -5
  103. package/dist/plot/ElementScatter.svelte.d.ts +8 -8
  104. package/dist/plot/Histogram.svelte +318 -170
  105. package/dist/plot/Histogram.svelte.d.ts +21 -41
  106. package/dist/plot/HistogramControls.svelte +115 -347
  107. package/dist/plot/HistogramControls.svelte.d.ts +13 -28
  108. package/dist/plot/Line.svelte +1 -1
  109. package/dist/plot/Line.svelte.d.ts +4 -4
  110. package/dist/plot/PlotControls.svelte +280 -0
  111. package/dist/plot/PlotControls.svelte.d.ts +4 -0
  112. package/dist/plot/PlotLegend.svelte +9 -24
  113. package/dist/plot/PlotLegend.svelte.d.ts +3 -3
  114. package/dist/plot/ScatterPlot.svelte +536 -571
  115. package/dist/plot/ScatterPlot.svelte.d.ts +18 -90
  116. package/dist/plot/ScatterPlotControls.svelte +108 -316
  117. package/dist/plot/ScatterPlotControls.svelte.d.ts +11 -38
  118. package/dist/plot/ScatterPoint.svelte +1 -1
  119. package/dist/plot/ScatterPoint.svelte.d.ts +5 -5
  120. package/dist/plot/SpacegroupBarPlot.svelte +235 -0
  121. package/dist/plot/SpacegroupBarPlot.svelte.d.ts +9 -0
  122. package/dist/plot/data-transform.d.ts +2 -2
  123. package/dist/plot/data-transform.js +1 -5
  124. package/dist/plot/index.d.ts +12 -200
  125. package/dist/plot/index.js +14 -6
  126. package/dist/plot/layout.d.ts +10 -16
  127. package/dist/plot/layout.js +40 -25
  128. package/dist/plot/scales.d.ts +7 -8
  129. package/dist/plot/scales.js +22 -7
  130. package/dist/plot/types.d.ts +303 -0
  131. package/dist/plot/types.js +1 -0
  132. package/dist/rdf/RdfPlot.svelte +156 -0
  133. package/dist/rdf/RdfPlot.svelte.d.ts +20 -0
  134. package/dist/rdf/calc-rdf.d.ts +4 -0
  135. package/dist/rdf/calc-rdf.js +107 -0
  136. package/dist/rdf/index.d.ts +22 -0
  137. package/dist/rdf/index.js +2 -0
  138. package/dist/settings.d.ts +86 -75
  139. package/dist/settings.js +257 -155
  140. package/dist/structure/Bond.svelte +126 -110
  141. package/dist/structure/Bond.svelte.d.ts +7 -20
  142. package/dist/structure/CanvasTooltip.svelte.d.ts +4 -4
  143. package/dist/structure/Cylinder.svelte +29 -0
  144. package/dist/structure/Cylinder.svelte.d.ts +10 -0
  145. package/dist/structure/Lattice.svelte +1 -2
  146. package/dist/structure/Lattice.svelte.d.ts +3 -3
  147. package/dist/structure/Structure.svelte +23 -8
  148. package/dist/structure/Structure.svelte.d.ts +10 -7
  149. package/dist/structure/StructureControls.svelte +25 -134
  150. package/dist/structure/StructureControls.svelte.d.ts +4 -9
  151. package/dist/structure/StructureExportPane.svelte +188 -0
  152. package/dist/structure/StructureExportPane.svelte.d.ts +17 -0
  153. package/dist/structure/StructureInfoPane.svelte.d.ts +3 -3
  154. package/dist/structure/StructureLegend.svelte +91 -27
  155. package/dist/structure/StructureLegend.svelte.d.ts +5 -4
  156. package/dist/structure/StructureScene.svelte +93 -100
  157. package/dist/structure/StructureScene.svelte.d.ts +15 -12
  158. package/dist/structure/Vector.svelte +19 -25
  159. package/dist/structure/Vector.svelte.d.ts +3 -3
  160. package/dist/structure/bonding.d.ts +16 -14
  161. package/dist/structure/bonding.js +207 -216
  162. package/dist/structure/export.d.ts +3 -0
  163. package/dist/structure/export.js +120 -0
  164. package/dist/structure/index.d.ts +15 -1
  165. package/dist/structure/index.js +2 -0
  166. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -3
  167. package/dist/symmetry/index.d.ts +1 -0
  168. package/dist/symmetry/index.js +1 -0
  169. package/dist/symmetry/spacegroups.d.ts +9 -0
  170. package/dist/symmetry/spacegroups.js +397 -0
  171. package/dist/theme/ThemeControl.svelte.d.ts +3 -3
  172. package/dist/theme/themes.js +2 -16
  173. package/dist/trajectory/Trajectory.svelte +52 -78
  174. package/dist/trajectory/Trajectory.svelte.d.ts +5 -5
  175. package/dist/trajectory/TrajectoryError.svelte.d.ts +3 -3
  176. package/dist/trajectory/TrajectoryExportPane.svelte +1 -1
  177. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +5 -5
  178. package/dist/trajectory/TrajectoryInfoPane.svelte +1 -1
  179. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -3
  180. package/dist/trajectory/extract.js +2 -1
  181. package/dist/trajectory/index.d.ts +1 -1
  182. package/dist/trajectory/index.js +1 -1
  183. package/dist/trajectory/parse.js +21 -14
  184. package/dist/utils.d.ts +1 -0
  185. package/dist/utils.js +17 -0
  186. package/dist/xrd/XrdPlot.svelte +14 -11
  187. package/dist/xrd/XrdPlot.svelte.d.ts +6 -5
  188. package/package.json +20 -12
  189. package/readme.md +1 -1
  190. package/dist/material/MaterialCard.svelte +0 -123
  191. package/dist/material/MaterialCard.svelte.d.ts +0 -9
  192. package/dist/material/index.d.ts +0 -1
  193. package/dist/material/index.js +0 -1
  194. package/dist/plot/formatting.d.ts +0 -7
  195. package/dist/plot/formatting.js +0 -53
@@ -1,266 +1,257 @@
1
+ // Bonding algorithms for structure visualization
1
2
  import { element_data } from '../element';
2
- // Bonding strategy map
3
- export const BONDING_STRATEGIES = {
4
- max_dist,
5
- nearest_neighbor,
6
- electroneg_ratio,
7
- };
8
- // Performance-optimized element lookup map
9
3
  const element_lookup = new Map(element_data.map((el) => [el.symbol, el]));
10
- // Build covalent radii map from ground truth data
11
- const covalent_radii = new Map(element_data
12
- .filter((el) => el.covalent_radius !== null)
13
- .map((el) => [el.symbol, el.covalent_radius]));
14
- // Simple distance-based bonding with relative distance ratios
15
- export function max_dist(structure, { max_distance_ratio = 1.8, min_bond_dist = 0.4, same_species_penalty = 0.6 } = {}) {
16
- const bonds = [];
17
- const sites = structure.sites;
18
- if (sites.length < 2)
19
- return bonds;
20
- const min_dist_sq = min_bond_dist ** 2;
21
- // Pre-calculate closest bond distances for each atom for efficient saturation penalty
22
- const closest_bond_distances = new Map();
23
- for (let idx_a = 0; idx_a < sites.length - 1; idx_a++) {
24
- const [x1, y1, z1] = sites[idx_a].xyz;
25
- const element_a = sites[idx_a].species?.[0]?.element;
26
- const radius_a = covalent_radii.get(element_a);
27
- for (let idx_b = idx_a + 1; idx_b < sites.length; idx_b++) {
28
- const [x2, y2, z2] = sites[idx_b].xyz;
29
- const element_b = sites[idx_b].species?.[0]?.element;
30
- const radius_b = covalent_radii.get(element_b);
31
- const [dx, dy, dz] = [x2 - x1, y2 - y1, z2 - z1];
32
- const dist_sq = dx * dx + dy * dy + dz * dz;
33
- const distance = Math.sqrt(dist_sq);
34
- if (dist_sq < min_dist_sq)
35
- continue;
36
- if (!radius_a || !radius_b)
37
- continue;
38
- const expected_dist = radius_a + radius_b;
39
- const max_allowed_dist = expected_dist * max_distance_ratio;
40
- if (distance <= max_allowed_dist) {
41
- // Distance-dependent strength (closer to expected = stronger)
42
- const distance_ratio = distance / expected_dist;
43
- let strength = Math.exp(-((distance_ratio - 1) ** 2) / (2 * 0.3 ** 2));
44
- // Apply same-species penalty
45
- if (element_a === element_b) {
46
- strength *= same_species_penalty;
47
- }
48
- // Apply bond saturation penalty more efficiently
49
- const closest_a = closest_bond_distances.get(idx_a) ?? Infinity;
50
- const closest_b = closest_bond_distances.get(idx_b) ?? Infinity;
51
- if (distance > closest_a) {
52
- const distance_ratio_a = distance / closest_a;
53
- strength *= Math.exp(-(distance_ratio_a - 1) / 0.5);
54
- }
55
- if (distance > closest_b) {
56
- const distance_ratio_b = distance / closest_b;
57
- strength *= Math.exp(-(distance_ratio_b - 1) / 0.5);
58
- }
59
- bonds.push({
60
- pos_1: sites[idx_a].xyz,
61
- pos_2: sites[idx_b].xyz,
62
- site_idx_1: idx_a,
63
- site_idx_2: idx_b,
64
- bond_length: distance,
65
- strength,
66
- });
67
- // Update closest bond distances
68
- if (distance < closest_a)
69
- closest_bond_distances.set(idx_a, distance);
70
- if (distance < closest_b)
71
- closest_bond_distances.set(idx_b, distance);
4
+ const covalent_radii = new Map(element_data.filter((el) => el.covalent_radius !== null).map((el) => [el.symbol, el.covalent_radius]));
5
+ // Get the species with highest occupancy from a site.
6
+ function get_majority_species(site) {
7
+ return (site.species ?? []).reduce((max, spec) => (spec.occu > max.occu ? spec : max), site.species?.[0] ?? { element: ``, occu: -1 });
8
+ }
9
+ // Compute 4x4 transformation matrix for bond cylinder between two positions.
10
+ // Uses Y-up, right-handed coordinate system convention for Three.js compatibility.
11
+ function compute_bond_transform(pos_1, pos_2) {
12
+ const [dx, dy, dz] = [pos_2[0] - pos_1[0], pos_2[1] - pos_1[1], pos_2[2] - pos_1[2]];
13
+ const height = Math.sqrt(dx * dx + dy * dy + dz * dz);
14
+ if (height < 1e-10) {
15
+ return new Float32Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]);
16
+ }
17
+ const [dir_x, dir_y, dir_z] = [dx / height, dy / height, dz / height];
18
+ let [m00, m01, m02, m10, m11, m12, m20, m21, m22] = [0, 0, 0, 0, 0, 0, 0, 0, 0];
19
+ // Special case: bond pointing straight up (+Y)
20
+ if (Math.abs(dir_y - 1.0) < 1e-10) {
21
+ ;
22
+ [m00, m01, m02, m10, m11, m12, m20, m21, m22] = [1, 0, 0, 0, 1, 0, 0, 0, 1];
23
+ }
24
+ else if (Math.abs(dir_y + 1.0) < 1e-10) {
25
+ // Special case: bond pointing straight down (-Y)
26
+ ;
27
+ [m00, m01, m02, m10, m11, m12, m20, m21, m22] = [1, 0, 0, 0, -1, 0, 0, 0, 1];
28
+ }
29
+ else {
30
+ // General case: construct orthonormal basis (right, dir, up)
31
+ // Right vector: perpendicular to dir in XZ plane
32
+ const [rx, rz] = [-dir_z, dir_x];
33
+ const r_len = Math.sqrt(rx * rx + rz * rz);
34
+ const [right_x, right_z] = [rx / r_len, rz / r_len];
35
+ // Up vector: cross product of dir and right
36
+ const [up_x, up_y, up_z] = [
37
+ dir_y * right_z,
38
+ dir_z * right_x - dir_x * right_z,
39
+ -dir_y * right_x,
40
+ ];
41
+ [m00, m01, m02, m10, m11, m12, m20, m21, m22] = [
42
+ right_x,
43
+ dir_x,
44
+ up_x,
45
+ 0,
46
+ dir_y,
47
+ up_y,
48
+ right_z,
49
+ dir_z,
50
+ up_z,
51
+ ];
52
+ }
53
+ // Position at midpoint between the two atoms
54
+ const [px, py, pz] = [
55
+ (pos_1[0] + pos_2[0]) / 2,
56
+ (pos_1[1] + pos_2[1]) / 2,
57
+ (pos_1[2] + pos_2[2]) / 2,
58
+ ];
59
+ return new Float32Array([
60
+ ...[m00, m10, m20, 0],
61
+ ...[m01 * height, m11 * height, m21 * height, 0],
62
+ ...[m02, m12, m22, 0],
63
+ ...[px, py, pz, 1],
64
+ ]);
65
+ }
66
+ // Build spatial grid by dividing 3D space into cubic cells.
67
+ function build_spatial_grid(sites, cell_size) {
68
+ const grid = new Map();
69
+ for (let idx = 0; idx < sites.length; idx++) {
70
+ const [x, y, z] = sites[idx].xyz;
71
+ const key = `${Math.floor(x / cell_size)},${Math.floor(y / cell_size)},${Math.floor(z / cell_size)}`;
72
+ const cell = grid.get(key);
73
+ if (cell)
74
+ cell.push(idx);
75
+ else
76
+ grid.set(key, [idx]);
77
+ }
78
+ return grid;
79
+ }
80
+ // Get all site indices in 3x3x3 cube of cells around position.
81
+ function get_neighbors_from_grid(pos, grid, cell_size) {
82
+ const [cx, cy, cz] = [
83
+ Math.floor(pos[0] / cell_size),
84
+ Math.floor(pos[1] / cell_size),
85
+ Math.floor(pos[2] / cell_size),
86
+ ];
87
+ const neighbors = [];
88
+ for (let dx = -1; dx <= 1; dx++) {
89
+ for (let dy = -1; dy <= 1; dy++) {
90
+ for (let dz = -1; dz <= 1; dz++) {
91
+ const cell = grid.get(`${cx + dx},${cy + dy},${cz + dz}`);
92
+ if (cell)
93
+ neighbors.push(...cell);
72
94
  }
73
95
  }
74
96
  }
75
- return bonds;
97
+ return neighbors;
98
+ }
99
+ // Setup spatial decomposition for structures with >50 atoms.
100
+ function setup_spatial_grid(sites, cutoff) {
101
+ const use_grid = sites.length > 50;
102
+ return use_grid ? { grid: build_spatial_grid(sites, cutoff), cell_size: cutoff } : null;
103
+ }
104
+ // Get candidate neighbor indices using spatial grid or all sites.
105
+ function get_candidates(pos, sites, spatial) {
106
+ return spatial
107
+ ? get_neighbors_from_grid(pos, spatial.grid, spatial.cell_size)
108
+ : Array.from({ length: sites.length }, (_, idx) => idx);
76
109
  }
77
- // nearest neighbor bonding - only neighboring closest to the central atom are bonded
78
- export function nearest_neighbor(structure, { max_neighbors = 4, tolerance_factor = 1.1, min_bond_dist = 0.4, same_species_penalty = 0.6, } = {}) {
79
- const sites = structure.sites;
110
+ export const BONDING_STRATEGIES = { electroneg_ratio, solid_angle };
111
+ // Electronegativity-based bonding with chemical preferences.
112
+ // This algorithm considers electronegativity differences between atoms, metal/nonmetal
113
+ // properties, and distance to determine bond strength. Bonds are only created if the
114
+ // computed strength exceeds the strength_threshold parameter (default: 0.3).
115
+ export function electroneg_ratio(structure, { electronegativity_threshold = 1.7, // Max electronegativity difference for bonding
116
+ max_distance_ratio = 2.0, // Max distance as multiple of sum of covalent radii
117
+ min_bond_dist = 0.4, // Minimum bond distance in Angstroms
118
+ metal_metal_penalty = 0.7, // Strength penalty for metal-metal bonds
119
+ metal_nonmetal_bonus = 1.5, // Strength bonus for metal-nonmetal bonds
120
+ similar_electronegativity_bonus = 1.2, // Bonus for similar electronegativity
121
+ same_species_penalty = 0.5, // Penalty for bonds between same element
122
+ strength_threshold = 0.3, // Minimum bond strength to include in results
123
+ } = {}) {
124
+ const { sites } = structure;
80
125
  if (sites.length < 2)
81
126
  return [];
82
127
  const bonds = [];
83
128
  const min_dist_sq = min_bond_dist ** 2;
84
- // Pre-calculate closest bond distances for each atom for efficient saturation penalty
85
- const closest_bond_distances = new Map();
86
- // For each atom, find its true nearest neighbors
87
- for (let idx_a = 0; idx_a < sites.length; idx_a++) {
129
+ const closest = new Map();
130
+ const props = sites.map((site) => {
131
+ const majority = get_majority_species(site);
132
+ const elem = majority.element;
133
+ const data = element_lookup.get(elem);
134
+ return {
135
+ element: elem,
136
+ electroneg: data?.electronegativity ?? 2.0,
137
+ is_metal: data?.metal ?? false,
138
+ is_nonmetal: data?.nonmetal ?? false,
139
+ radius: elem ? covalent_radii.get(elem) : undefined,
140
+ };
141
+ });
142
+ let max_radius = 0;
143
+ for (const radius of covalent_radii.values()) {
144
+ if (radius > max_radius)
145
+ max_radius = radius;
146
+ }
147
+ const max_cutoff = max_radius * 2 * max_distance_ratio;
148
+ const spatial = setup_spatial_grid(sites, max_cutoff);
149
+ for (let idx_a = 0; idx_a < sites.length - 1; idx_a++) {
88
150
  const [x1, y1, z1] = sites[idx_a].xyz;
89
- const element_a = sites[idx_a].species?.[0]?.element;
90
- const radius_a = covalent_radii.get(element_a);
91
- // Collect all potential neighbors with their distances
92
- const neighbors = [];
93
- for (let idx_b = 0; idx_b < sites.length; idx_b++) {
94
- if (idx_a === idx_b)
151
+ const pa = props[idx_a];
152
+ for (const idx_b of get_candidates(sites[idx_a].xyz, sites, spatial)) {
153
+ if (idx_b <= idx_a)
95
154
  continue;
96
155
  const [x2, y2, z2] = sites[idx_b].xyz;
97
- const element_b = sites[idx_b].species?.[0]?.element;
98
- const radius_b = covalent_radii.get(element_b);
156
+ const pb = props[idx_b];
99
157
  const [dx, dy, dz] = [x2 - x1, y2 - y1, z2 - z1];
100
158
  const dist_sq = dx * dx + dy * dy + dz * dz;
101
- const distance = Math.sqrt(dist_sq);
102
- if (dist_sq >= min_dist_sq) {
103
- if (!radius_a || !radius_b)
104
- continue;
105
- const expected_dist = radius_a + radius_b;
106
- const normalized_distance = distance / expected_dist;
107
- neighbors.push({
108
- idx: idx_b,
109
- distance,
110
- normalized_distance,
111
- element: element_b,
112
- radius: radius_b,
113
- });
159
+ const dist = Math.sqrt(dist_sq);
160
+ if (dist_sq < min_dist_sq || !pa.radius || !pb.radius)
161
+ continue;
162
+ const expected = pa.radius + pb.radius;
163
+ if (dist > expected * max_distance_ratio)
164
+ continue;
165
+ const en_diff = Math.abs(pa.electroneg - pb.electroneg);
166
+ const en_ratio = en_diff / (pa.electroneg + pb.electroneg);
167
+ let bond_strength = 1.0;
168
+ if (pa.is_metal && pb.is_metal) {
169
+ bond_strength *= metal_metal_penalty;
114
170
  }
115
- }
116
- if (neighbors.length === 0)
117
- continue;
118
- // Sort by normalized distance to get true nearest neighbors
119
- neighbors.sort((a, b) => a.normalized_distance - b.normalized_distance);
120
- // Find the actual nearest neighbor distance
121
- const nearest_normalized_dist = neighbors[0].normalized_distance;
122
- const tolerance_threshold = nearest_normalized_dist * tolerance_factor;
123
- // Select neighbors that are both:
124
- // 1. Among the top max_neighbors closest
125
- // 2. Within tolerance_factor of the nearest neighbor distance
126
- const selected_neighbors = neighbors
127
- .slice(0, max_neighbors)
128
- .filter((neighbor, index) => index < max_neighbors && neighbor.normalized_distance <= tolerance_threshold);
129
- // Create bonds to selected nearest neighbors (avoid duplicates)
130
- for (const neighbor of selected_neighbors) {
131
- const idx_b = neighbor.idx;
132
- // Only create bond if idx_a < idx_b to avoid duplicates
133
- if (idx_a < idx_b) {
134
- // Calculate bond strength based on how close it is to the nearest neighbor
135
- const distance_ratio = neighbor.normalized_distance / nearest_normalized_dist;
136
- let strength = Math.exp(-((distance_ratio - 1) ** 2) / (2 * 0.15 ** 2));
137
- // Apply same-species penalty
138
- if (element_a === neighbor.element)
139
- strength *= same_species_penalty;
140
- // Apply bond saturation penalty more efficiently
141
- const closest_a = closest_bond_distances.get(idx_a) ?? Infinity;
142
- const closest_b = closest_bond_distances.get(idx_b) ?? Infinity;
143
- if (neighbor.distance > closest_a) {
144
- const distance_ratio_a = neighbor.distance / closest_a;
145
- strength *= Math.exp(-(distance_ratio_a - 1) / 0.5);
146
- }
147
- if (neighbor.distance > closest_b) {
148
- const distance_ratio_b = neighbor.distance / closest_b;
149
- strength *= Math.exp(-(distance_ratio_b - 1) / 0.5);
150
- }
171
+ else if ((pa.is_metal && pb.is_nonmetal) || (pa.is_nonmetal && pb.is_metal)) {
172
+ bond_strength *= metal_nonmetal_bonus;
173
+ if (en_diff > electronegativity_threshold)
174
+ bond_strength *= 1.3;
175
+ }
176
+ else if (en_diff < 0.5) {
177
+ bond_strength *= similar_electronegativity_bonus;
178
+ }
179
+ const dist_weight = Math.exp(-((dist / expected - 1) ** 2) / 0.18);
180
+ const en_weight = 1.0 - 0.3 * en_ratio;
181
+ let strength = bond_strength * dist_weight * en_weight;
182
+ if (pa.element === pb.element)
183
+ strength *= same_species_penalty;
184
+ const ca = closest.get(idx_a) ?? Infinity;
185
+ const cb = closest.get(idx_b) ?? Infinity;
186
+ if (dist > ca)
187
+ strength *= Math.exp(-(dist / ca - 1) / 0.5);
188
+ if (dist > cb)
189
+ strength *= Math.exp(-(dist / cb - 1) / 0.5);
190
+ if (strength > strength_threshold) {
151
191
  bonds.push({
152
192
  pos_1: sites[idx_a].xyz,
153
193
  pos_2: sites[idx_b].xyz,
154
194
  site_idx_1: idx_a,
155
195
  site_idx_2: idx_b,
156
- bond_length: neighbor.distance,
196
+ bond_length: dist,
157
197
  strength,
198
+ transform_matrix: compute_bond_transform(sites[idx_a].xyz, sites[idx_b].xyz),
158
199
  });
159
- // Update closest bond distances
160
- if (neighbor.distance < closest_a) {
161
- closest_bond_distances.set(idx_a, neighbor.distance);
162
- }
163
- if (neighbor.distance < closest_b) {
164
- closest_bond_distances.set(idx_b, neighbor.distance);
165
- }
200
+ if (dist < ca)
201
+ closest.set(idx_a, dist);
202
+ if (dist < cb)
203
+ closest.set(idx_b, dist);
166
204
  }
167
205
  }
168
206
  }
169
207
  return bonds;
170
208
  }
171
- // Electronegativity-based bonding with enhanced weighting
172
- export function electroneg_ratio(structure, { electronegativity_threshold = 1.7, max_distance_ratio = 2.0, min_bond_dist = 0.4, metal_metal_penalty = 0.7, metal_nonmetal_bonus = 1.5, similar_electronegativity_bonus = 1.2, same_species_penalty = 0.5, strength_threshold = 0.3, } = {}) {
173
- const sites = structure.sites;
209
+ // Solid angle-based bonding using geometric proximity heuristics.
210
+ // Inspired by Voronoi tessellation without having to actually compute Voronoi cells.
211
+ // This algorithm computes bond strength based on the solid angle subtended by atoms
212
+ // and their distance penalty. Bonds are only created if the computed strength exceeds
213
+ // the strength_threshold parameter.
214
+ export function solid_angle(structure, { min_solid_angle = 0.01, min_face_area = 0.05, max_distance = 5.0, min_bond_dist = 0.4, strength_threshold = 0.05, } = {}) {
215
+ const { sites } = structure;
174
216
  if (sites.length < 2)
175
217
  return [];
176
218
  const bonds = [];
177
219
  const min_dist_sq = min_bond_dist ** 2;
178
- // Pre-calculate closest bond distances for each atom for efficient saturation penalty
179
- const closest_bond_distances = new Map();
180
- // Pre-calculate element properties
181
- const site_properties = sites.map((site) => {
182
- const element = element_lookup.get(site.species?.[0]?.element);
183
- return {
184
- element: site.species?.[0]?.element,
185
- electronegativity: element?.electronegativity ?? 2.0,
186
- is_metal: element?.metal ?? false,
187
- is_nonmetal: element?.nonmetal ?? false,
188
- covalent_radius: covalent_radii.get(site.species?.[0]?.element),
189
- };
190
- });
191
- // Phase 1: Find all potential bonds with electronegativity weighting
220
+ const max_dist_sq = max_distance ** 2;
221
+ const spatial = setup_spatial_grid(sites, max_distance);
192
222
  for (let idx_a = 0; idx_a < sites.length - 1; idx_a++) {
193
223
  const [x1, y1, z1] = sites[idx_a].xyz;
194
- const props_a = site_properties[idx_a];
195
- for (let idx_b = idx_a + 1; idx_b < sites.length; idx_b++) {
224
+ const majority_a = get_majority_species(sites[idx_a]);
225
+ const ra = majority_a.element ? covalent_radii.get(majority_a.element) : undefined;
226
+ for (const idx_b of get_candidates(sites[idx_a].xyz, sites, spatial)) {
227
+ if (idx_b <= idx_a)
228
+ continue;
196
229
  const [x2, y2, z2] = sites[idx_b].xyz;
197
- const props_b = site_properties[idx_b];
230
+ const majority_b = get_majority_species(sites[idx_b]);
231
+ const rb = majority_b.element ? covalent_radii.get(majority_b.element) : undefined;
198
232
  const [dx, dy, dz] = [x2 - x1, y2 - y1, z2 - z1];
199
233
  const dist_sq = dx * dx + dy * dy + dz * dz;
200
- const distance = Math.sqrt(dist_sq);
201
- if (dist_sq < min_dist_sq)
202
- continue;
203
- if (!props_a.covalent_radius || !props_b.covalent_radius)
234
+ const dist = Math.sqrt(dist_sq);
235
+ if (dist_sq < min_dist_sq || dist_sq > max_dist_sq || !ra || !rb)
204
236
  continue;
205
- const expected_dist = props_a.covalent_radius + props_b.covalent_radius;
206
- const max_allowed_dist = expected_dist * max_distance_ratio;
207
- if (distance > max_allowed_dist)
237
+ const avg_r = (ra + rb) / 2.0;
238
+ const face_area = Math.PI * avg_r * avg_r;
239
+ const solid_angle = face_area / dist_sq;
240
+ if (solid_angle < min_solid_angle || face_area < min_face_area)
208
241
  continue;
209
- // Enhanced electronegativity weighting (CrystalNN-inspired)
210
- const electronegativity_diff = Math.abs(props_a.electronegativity - props_b.electronegativity);
211
- const electronegativity_ratio = electronegativity_diff /
212
- (props_a.electronegativity + props_b.electronegativity);
213
- let bond_strength = 1.0;
214
- // Chemical bonding preferences
215
- if (props_a.is_metal && props_b.is_metal) {
216
- bond_strength *= metal_metal_penalty;
217
- }
218
- else if ((props_a.is_metal && props_b.is_nonmetal) ||
219
- (props_a.is_nonmetal && props_b.is_metal)) {
220
- bond_strength *= metal_nonmetal_bonus;
221
- if (electronegativity_diff > electronegativity_threshold) {
222
- bond_strength *= 1.3; // Ionic character bonus
223
- }
224
- }
225
- else if (electronegativity_diff < 0.5) {
226
- bond_strength *= similar_electronegativity_bonus;
227
- }
228
- // Distance-dependent weighting (CrystalNN approach)
229
- const distance_ratio = distance / expected_dist;
230
- const distance_weight = Math.exp(-((distance_ratio - 1) ** 2) / (2 * 0.3 ** 2));
231
- // Electronegativity-based weighting
232
- const electro_weight = 1.0 - 0.3 * electronegativity_ratio;
233
- // Combined strength score
234
- let strength = bond_strength * distance_weight * electro_weight;
235
- // Apply same-species penalty
236
- if (props_a.element === props_b.element) {
237
- strength *= same_species_penalty;
238
- }
239
- // Apply bond saturation penalty more efficiently
240
- const closest_a = closest_bond_distances.get(idx_a) ?? Infinity;
241
- const closest_b = closest_bond_distances.get(idx_b) ?? Infinity;
242
- if (distance > closest_a) {
243
- const distance_ratio_a = distance / closest_a;
244
- strength *= Math.exp(-(distance_ratio_a - 1) / 0.5);
245
- }
246
- if (distance > closest_b) {
247
- const distance_ratio_b = distance / closest_b;
248
- strength *= Math.exp(-(distance_ratio_b - 1) / 0.5);
249
- }
242
+ const dist_penalty = Math.exp(-((dist / (ra + rb) - 1) ** 2) / 0.4);
243
+ const angle_strength = Math.min(solid_angle / (4.0 * Math.PI), 1.0);
244
+ const strength = angle_strength * dist_penalty;
250
245
  if (strength > strength_threshold) {
251
246
  bonds.push({
252
247
  pos_1: sites[idx_a].xyz,
253
248
  pos_2: sites[idx_b].xyz,
254
249
  site_idx_1: idx_a,
255
250
  site_idx_2: idx_b,
256
- bond_length: distance,
251
+ bond_length: dist,
257
252
  strength,
253
+ transform_matrix: compute_bond_transform(sites[idx_a].xyz, sites[idx_b].xyz),
258
254
  });
259
- // Update closest bond distances
260
- if (distance < closest_a)
261
- closest_bond_distances.set(idx_a, distance);
262
- if (distance < closest_b)
263
- closest_bond_distances.set(idx_b, distance);
264
255
  }
265
256
  }
266
257
  }
@@ -1,4 +1,5 @@
1
1
  import type { AnyStructure } from '..';
2
+ import { type Scene } from 'three';
2
3
  export declare function create_structure_filename(structure: AnyStructure | undefined, extension: string): string;
3
4
  export declare function structure_to_xyz_str(structure?: AnyStructure): string;
4
5
  export declare function structure_to_cif_str(structure?: AnyStructure): string;
@@ -8,3 +9,5 @@ export declare function export_structure_as_cif(structure?: AnyStructure): void;
8
9
  export declare function export_structure_as_poscar(structure?: AnyStructure): void;
9
10
  export declare function export_structure_as_xyz(structure?: AnyStructure): void;
10
11
  export declare function export_structure_as_json(structure?: AnyStructure): void;
12
+ export declare function export_structure_as_glb(scene: Scene | null, structure: AnyStructure | undefined): void;
13
+ export declare function export_structure_as_obj(scene: Scene | null, structure: AnyStructure | undefined): void;
@@ -1,6 +1,76 @@
1
1
  import { electro_neg_formula } from '..';
2
2
  import { download } from '../io/fetch';
3
3
  import * as math from '../math';
4
+ import { Group, Matrix4, Mesh, MeshStandardMaterial, } from 'three';
5
+ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
6
+ import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js';
7
+ // Helper function to convert InstancedMesh to regular Mesh objects for export
8
+ // This is necessary because GLB/OBJ exporters don't handle InstancedMesh properly
9
+ // Note: Threlte's InstancedMesh sets isInstancedMesh=true but type remains "Mesh"
10
+ function convert_instanced_meshes_to_regular(scene) {
11
+ const cloned_scene = scene.clone();
12
+ // Find all InstancedMesh objects in the cloned scene
13
+ const instanced_meshes = [];
14
+ cloned_scene.traverse((object) => {
15
+ // Check for isInstancedMesh property (Threlte) or type === InstancedMesh (vanilla Three.js)
16
+ // @ts-expect-error - checking for isInstancedMesh property
17
+ const is_instanced = object.isInstancedMesh === true ||
18
+ object.type === `InstancedMesh`;
19
+ if (is_instanced) {
20
+ instanced_meshes.push(object);
21
+ }
22
+ });
23
+ // Convert each InstancedMesh to individual Mesh objects
24
+ for (const instanced_mesh of instanced_meshes) {
25
+ const parent = instanced_mesh.parent;
26
+ if (!parent || !instanced_mesh.instanceMatrix)
27
+ continue;
28
+ // Create a group to hold all the individual meshes
29
+ const group = new Group();
30
+ group.name = instanced_mesh.name;
31
+ // Get the base transform from the InstancedMesh
32
+ const base_matrix = new Matrix4();
33
+ base_matrix.copy(instanced_mesh.matrix);
34
+ // Create individual meshes for each instance
35
+ const instance_matrix = new Matrix4();
36
+ for (let idx = 0; idx < instanced_mesh.count; idx++) {
37
+ instanced_mesh.getMatrixAt(idx, instance_matrix);
38
+ // Clone geometry for each instance (applyMatrix4 modifies geometry in place)
39
+ const mesh = new Mesh(instanced_mesh.geometry.clone(), instanced_mesh.material instanceof Array
40
+ ? instanced_mesh.material.map((mat) => mat.clone())
41
+ : instanced_mesh.material.clone());
42
+ // Combine base transform with instance transform
43
+ const combined_matrix = new Matrix4();
44
+ combined_matrix.multiplyMatrices(base_matrix, instance_matrix);
45
+ mesh.applyMatrix4(combined_matrix);
46
+ // Copy instance color if it exists
47
+ if (instanced_mesh.instanceColor) {
48
+ const color_r = instanced_mesh.instanceColor.getX(idx);
49
+ const color_g = instanced_mesh.instanceColor.getY(idx);
50
+ const color_b = instanced_mesh.instanceColor.getZ(idx);
51
+ if (mesh.material instanceof MeshStandardMaterial) {
52
+ mesh.material.color.setRGB(color_r, color_g, color_b);
53
+ }
54
+ else if (Array.isArray(mesh.material)) {
55
+ mesh.material.forEach((mat) => {
56
+ if (mat instanceof MeshStandardMaterial) {
57
+ mat.color.setRGB(color_r, color_g, color_b);
58
+ }
59
+ });
60
+ }
61
+ }
62
+ group.add(mesh);
63
+ }
64
+ // Replace the InstancedMesh with the Group in the parent
65
+ parent.remove(instanced_mesh);
66
+ parent.add(group);
67
+ // Update world matrices after scene graph modification
68
+ group.updateMatrixWorld(true);
69
+ }
70
+ // Update all world matrices in the modified scene
71
+ cloned_scene.updateMatrixWorld(true);
72
+ return cloned_scene;
73
+ }
4
74
  // Generate a filename for structure exports based on structure metadata
5
75
  export function create_structure_filename(structure, extension) {
6
76
  if (!structure)
@@ -337,3 +407,53 @@ export function export_structure_as_json(structure) {
337
407
  console.error(`Error exporting JSON:`, error);
338
408
  }
339
409
  }
410
+ // Export Three.js scene as GLB (binary GLTF) file
411
+ // GLB preserves materials and colors, making it ideal for element visualization
412
+ export function export_structure_as_glb(scene, structure) {
413
+ try {
414
+ if (!scene) {
415
+ console.warn(`No scene available for GLB export`);
416
+ return;
417
+ }
418
+ // Convert instanced meshes to regular meshes for export
419
+ const export_scene = convert_instanced_meshes_to_regular(scene);
420
+ const exporter = new GLTFExporter();
421
+ const filename = create_structure_filename(structure, `glb`);
422
+ // Export as binary GLB format
423
+ exporter.parse(export_scene, (result) => {
424
+ if (result instanceof ArrayBuffer) {
425
+ const blob = new Blob([result], { type: `model/gltf-binary` });
426
+ download(blob, filename, `model/gltf-binary`);
427
+ }
428
+ else {
429
+ console.error(`GLB export returned unexpected format`);
430
+ }
431
+ }, (error) => {
432
+ console.error(`GLB export failed:`, error);
433
+ }, { binary: true });
434
+ }
435
+ catch (error) {
436
+ console.error(`Error exporting GLB:`, error);
437
+ }
438
+ }
439
+ // Export Three.js scene as OBJ (Wavefront Object) file
440
+ // OBJ exports geometry with material references, widely supported format
441
+ export function export_structure_as_obj(scene, structure) {
442
+ try {
443
+ if (!scene) {
444
+ console.warn(`No scene available for OBJ export`);
445
+ return;
446
+ }
447
+ // Convert instanced meshes to regular meshes for export
448
+ const export_scene = convert_instanced_meshes_to_regular(scene);
449
+ const exporter = new OBJExporter();
450
+ const filename = create_structure_filename(structure, `obj`);
451
+ const result = exporter.parse(export_scene);
452
+ // OBJ exporter returns a string
453
+ const blob = new Blob([result], { type: `text/plain` });
454
+ download(blob, filename, `text/plain`);
455
+ }
456
+ catch (error) {
457
+ console.error(`Error exporting OBJ:`, error);
458
+ }
459
+ }