matterviz 0.4.0 → 0.4.1

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 (326) hide show
  1. package/dist/brillouin/BrillouinZone.svelte +68 -145
  2. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -14
  3. package/dist/brillouin/BrillouinZoneExportPane.svelte +43 -96
  4. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  5. package/dist/brillouin/BrillouinZoneInfoPane.svelte +9 -32
  6. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  7. package/dist/brillouin/BrillouinZoneScene.svelte +49 -203
  8. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  9. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  10. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  11. package/dist/brillouin/compute.d.ts +2 -0
  12. package/dist/brillouin/compute.js +80 -77
  13. package/dist/brillouin/geometry.d.ts +8 -0
  14. package/dist/brillouin/geometry.js +57 -0
  15. package/dist/brillouin/index.d.ts +2 -0
  16. package/dist/brillouin/index.js +2 -0
  17. package/dist/brillouin/types.d.ts +2 -2
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +100 -191
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +176 -464
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  23. package/dist/chempot-diagram/color.d.ts +3 -6
  24. package/dist/chempot-diagram/color.js +5 -5
  25. package/dist/chempot-diagram/compute.d.ts +3 -3
  26. package/dist/chempot-diagram/compute.js +3 -1
  27. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  28. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  29. package/dist/chempot-diagram/export.d.ts +47 -0
  30. package/dist/chempot-diagram/export.js +133 -0
  31. package/dist/chempot-diagram/index.d.ts +1 -0
  32. package/dist/chempot-diagram/index.js +1 -0
  33. package/dist/chempot-diagram/pointer.d.ts +0 -10
  34. package/dist/chempot-diagram/pointer.js +4 -4
  35. package/dist/chempot-diagram/types.d.ts +3 -3
  36. package/dist/colors/index.js +2 -2
  37. package/dist/composition/FormulaFilter.svelte +6 -5
  38. package/dist/composition/PieChart.svelte +5 -5
  39. package/dist/composition/chem-sys.js +3 -2
  40. package/dist/composition/format.js +3 -2
  41. package/dist/composition/parse.d.ts +0 -1
  42. package/dist/composition/parse.js +17 -19
  43. package/dist/controls.d.ts +1 -0
  44. package/dist/controls.js +0 -1
  45. package/dist/convex-hull/ConvexHull.svelte +8 -10
  46. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  47. package/dist/convex-hull/ConvexHull2D.svelte +94 -175
  48. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  49. package/dist/convex-hull/ConvexHull3D.svelte +176 -680
  50. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  51. package/dist/convex-hull/ConvexHull4D.svelte +180 -680
  52. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  53. package/dist/convex-hull/ConvexHullChrome.svelte +268 -0
  54. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  55. package/dist/convex-hull/ConvexHullControls.svelte +88 -7
  56. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  57. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  58. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  59. package/dist/convex-hull/ConvexHullStats.svelte +29 -168
  60. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  61. package/dist/convex-hull/ConvexHullTooltip.svelte +11 -2
  62. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  63. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  64. package/dist/convex-hull/barycentric-coords.js +6 -33
  65. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  66. package/dist/convex-hull/canvas-interactions.svelte.js +278 -0
  67. package/dist/convex-hull/helpers.d.ts +39 -7
  68. package/dist/convex-hull/helpers.js +154 -69
  69. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  70. package/dist/convex-hull/hull-state.svelte.js +124 -0
  71. package/dist/convex-hull/index.d.ts +9 -7
  72. package/dist/convex-hull/index.js +7 -2
  73. package/dist/convex-hull/thermodynamics.js +91 -920
  74. package/dist/convex-hull/types.d.ts +12 -4
  75. package/dist/convex-hull/types.js +12 -0
  76. package/dist/coordination/CoordinationBarPlot.svelte +4 -11
  77. package/dist/element/BohrAtom.svelte +2 -1
  78. package/dist/element/ElementTile.svelte.d.ts +1 -1
  79. package/dist/element/index.d.ts +4 -0
  80. package/dist/element/index.js +18 -0
  81. package/dist/feedback/DragOverlay.svelte +3 -1
  82. package/dist/feedback/DragOverlay.svelte.d.ts +1 -0
  83. package/dist/feedback/StatusMessage.svelte +13 -3
  84. package/dist/fermi-surface/FermiSurface.svelte +67 -146
  85. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -14
  86. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  87. package/dist/fermi-surface/FermiSurfaceScene.svelte +72 -224
  88. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  89. package/dist/fermi-surface/compute.js +11 -10
  90. package/dist/fermi-surface/export.js +4 -15
  91. package/dist/fermi-surface/index.d.ts +0 -1
  92. package/dist/fermi-surface/index.js +0 -1
  93. package/dist/fermi-surface/parse.d.ts +1 -1
  94. package/dist/fermi-surface/parse.js +64 -75
  95. package/dist/fermi-surface/types.d.ts +2 -2
  96. package/dist/heatmap-matrix/HeatmapMatrix.svelte +55 -40
  97. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +4 -3
  98. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +3 -2
  99. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  100. package/dist/heatmap-matrix/index.d.ts +3 -2
  101. package/dist/index.d.ts +1 -0
  102. package/dist/index.js +1 -0
  103. package/dist/io/ExportPane.svelte +166 -0
  104. package/dist/io/ExportPane.svelte.d.ts +17 -0
  105. package/dist/io/decompress.js +1 -2
  106. package/dist/io/export.d.ts +5 -1
  107. package/dist/io/export.js +32 -28
  108. package/dist/io/fetch.d.ts +2 -1
  109. package/dist/io/file-drop.d.ts +7 -0
  110. package/dist/io/file-drop.js +13 -0
  111. package/dist/io/index.d.ts +2 -0
  112. package/dist/io/index.js +10 -0
  113. package/dist/io/types.d.ts +13 -0
  114. package/dist/isosurface/parse.js +46 -44
  115. package/dist/labels.js +1 -1
  116. package/dist/layout/FullscreenButton.svelte +33 -0
  117. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  118. package/dist/layout/FullscreenToggle.svelte +8 -14
  119. package/dist/layout/ViewerChrome.svelte +116 -0
  120. package/dist/layout/ViewerChrome.svelte.d.ts +17 -0
  121. package/dist/layout/fullscreen.d.ts +4 -0
  122. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  123. package/dist/layout/fullscreen.svelte.js +37 -0
  124. package/dist/layout/index.d.ts +3 -0
  125. package/dist/layout/index.js +3 -0
  126. package/dist/math.d.ts +7 -3
  127. package/dist/math.js +18 -21
  128. package/dist/overlays/index.d.ts +4 -0
  129. package/dist/periodic-table/PeriodicTable.svelte +9 -8
  130. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +1 -1
  131. package/dist/phase-diagram/PhaseDiagramControls.svelte +3 -2
  132. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  133. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  134. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  135. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +47 -132
  136. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  137. package/dist/phase-diagram/colors.js +1 -1
  138. package/dist/phase-diagram/parse.d.ts +2 -1
  139. package/dist/plot/bar/BarPlot.svelte +79 -316
  140. package/dist/plot/bar/BarPlot.svelte.d.ts +7 -15
  141. package/dist/plot/bar/BarPlotControls.svelte.d.ts +1 -1
  142. package/dist/plot/bar/SpacegroupBarPlot.svelte +2 -1
  143. package/dist/plot/box/BoxPlot.svelte +76 -246
  144. package/dist/plot/box/BoxPlot.svelte.d.ts +4 -3
  145. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  146. package/dist/plot/box/Violin.svelte.d.ts +1 -1
  147. package/dist/plot/box/box-plot.d.ts +3 -2
  148. package/dist/plot/box/box-plot.js +5 -2
  149. package/dist/plot/box/kde.d.ts +2 -1
  150. package/dist/plot/box/kde.js +4 -4
  151. package/dist/plot/core/auto-place.d.ts +1 -1
  152. package/dist/plot/core/auto-place.js +4 -1
  153. package/dist/plot/core/components/ColorBar.svelte +5 -5
  154. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  155. package/dist/plot/core/components/Line.svelte +3 -2
  156. package/dist/plot/core/components/Line.svelte.d.ts +3 -2
  157. package/dist/plot/core/components/PlotAxis.svelte +2 -1
  158. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  159. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  160. package/dist/plot/core/components/ReferenceLine3D.svelte +2 -2
  161. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  162. package/dist/plot/core/components/ReferencePlane.svelte +2 -2
  163. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  164. package/dist/plot/core/data-cleaning.js +18 -18
  165. package/dist/plot/core/fill-utils.d.ts +4 -3
  166. package/dist/plot/core/fill-utils.js +6 -3
  167. package/dist/plot/core/interactions.d.ts +5 -1
  168. package/dist/plot/core/interactions.js +14 -0
  169. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  170. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  171. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  172. package/dist/plot/core/placed-tween.svelte.js +68 -0
  173. package/dist/plot/core/reference-line.d.ts +10 -10
  174. package/dist/plot/core/reference-line.js +6 -6
  175. package/dist/plot/core/scales.d.ts +17 -25
  176. package/dist/plot/core/scales.js +10 -8
  177. package/dist/plot/core/svg.d.ts +2 -1
  178. package/dist/plot/core/types.d.ts +18 -7
  179. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  180. package/dist/plot/core/utils/label-placement.js +3 -3
  181. package/dist/plot/core/utils.d.ts +2 -1
  182. package/dist/plot/histogram/Histogram.svelte +77 -314
  183. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  184. package/dist/plot/sankey/Sankey.svelte +2 -5
  185. package/dist/plot/sankey/Sankey.svelte.d.ts +1 -1
  186. package/dist/plot/sankey/sankey.js +3 -1
  187. package/dist/plot/scatter/BinnedScatterPlot.svelte +3 -5
  188. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +4 -4
  189. package/dist/plot/scatter/ScatterPlot.svelte +160 -450
  190. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +7 -15
  191. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  192. package/dist/plot/scatter/binned-scatter-types.d.ts +4 -11
  193. package/dist/plot/scatter/index.d.ts +1 -1
  194. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +15 -26
  195. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  196. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +9 -10
  197. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  198. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +122 -121
  199. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  200. package/dist/plot/scatter-3d/Surface3D.svelte +6 -5
  201. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  202. package/dist/plot/sunburst/Sunburst.svelte +16 -20
  203. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  204. package/dist/plot/sunburst/SunburstControls.svelte.d.ts +1 -1
  205. package/dist/plot/sunburst/sunburst.js +4 -1
  206. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  207. package/dist/sanitize.js +13 -2
  208. package/dist/scene/SceneCamera.svelte +62 -0
  209. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  210. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  211. package/dist/scene/bind-renderer.svelte.js +14 -0
  212. package/dist/scene/index.d.ts +4 -0
  213. package/dist/scene/index.js +5 -0
  214. package/dist/scene/props.js +52 -0
  215. package/dist/scene/types.d.ts +26 -0
  216. package/dist/scene/types.js +1 -0
  217. package/dist/settings.d.ts +14 -2
  218. package/dist/settings.js +59 -1
  219. package/dist/spectral/Bands.svelte +8 -7
  220. package/dist/spectral/Bands.svelte.d.ts +3 -2
  221. package/dist/spectral/BandsAndDos.svelte +22 -24
  222. package/dist/spectral/BrillouinBandsDos.svelte +3 -3
  223. package/dist/spectral/Dos.svelte +5 -4
  224. package/dist/spectral/Dos.svelte.d.ts +2 -1
  225. package/dist/spectral/helpers.d.ts +6 -6
  226. package/dist/spectral/helpers.js +43 -37
  227. package/dist/state.svelte.d.ts +0 -7
  228. package/dist/state.svelte.js +0 -6
  229. package/dist/structure/Arrow.svelte +2 -4
  230. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  231. package/dist/structure/CanvasTooltip.svelte +1 -0
  232. package/dist/structure/CellSelect.svelte +11 -3
  233. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  234. package/dist/structure/Lattice.svelte +2 -2
  235. package/dist/structure/Structure.svelte +291 -355
  236. package/dist/structure/Structure.svelte.d.ts +5 -15
  237. package/dist/structure/StructureControls.svelte +217 -2
  238. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  239. package/dist/structure/StructureExportPane.svelte +54 -156
  240. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  241. package/dist/structure/StructureInfoPane.svelte +5 -3
  242. package/dist/structure/StructureInfoPane.svelte.d.ts +5 -5
  243. package/dist/structure/StructureScene.svelte +365 -198
  244. package/dist/structure/StructureScene.svelte.d.ts +22 -20
  245. package/dist/structure/{label-placement.d.ts → atom-label-placement.d.ts} +3 -3
  246. package/dist/structure/{label-placement.js → atom-label-placement.js} +12 -2
  247. package/dist/structure/atom-properties.d.ts +1 -1
  248. package/dist/structure/atom-properties.js +11 -16
  249. package/dist/structure/bond-order-perception.js +2 -4
  250. package/dist/structure/bonding.d.ts +3 -0
  251. package/dist/structure/bonding.js +91 -48
  252. package/dist/structure/export.d.ts +24 -4
  253. package/dist/structure/export.js +64 -122
  254. package/dist/structure/index.d.ts +2 -0
  255. package/dist/structure/index.js +2 -0
  256. package/dist/structure/parse.d.ts +3 -2
  257. package/dist/structure/parse.js +333 -370
  258. package/dist/structure/partial-occupancy.d.ts +0 -1
  259. package/dist/structure/partial-occupancy.js +1 -1
  260. package/dist/structure/pbc.d.ts +1 -1
  261. package/dist/structure/pbc.js +186 -13
  262. package/dist/structure/polyhedra.d.ts +41 -0
  263. package/dist/structure/polyhedra.js +602 -0
  264. package/dist/structure/site.d.ts +4 -0
  265. package/dist/structure/site.js +1 -0
  266. package/dist/structure/supercell.js +3 -2
  267. package/dist/structure/validation.js +5 -6
  268. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  269. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  270. package/dist/symmetry/SymmetryElements.svelte +354 -0
  271. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  272. package/dist/symmetry/SymmetryStats.svelte +111 -6
  273. package/dist/symmetry/WyckoffTable.svelte +68 -7
  274. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  275. package/dist/symmetry/cell-transform.js +7 -14
  276. package/dist/symmetry/index.d.ts +14 -4
  277. package/dist/symmetry/index.js +301 -80
  278. package/dist/symmetry/spacegroups.d.ts +5 -1
  279. package/dist/symmetry/spacegroups.js +15 -1
  280. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  281. package/dist/symmetry/symmetry-elements.js +521 -0
  282. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  283. package/dist/symmetry/wyckoff-db.js +87 -0
  284. package/dist/table/HeatmapTable.svelte +4 -15
  285. package/dist/table/HeatmapTable.svelte.d.ts +1 -1
  286. package/dist/trajectory/Trajectory.svelte +58 -61
  287. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  288. package/dist/trajectory/TrajectoryExportPane.svelte +15 -24
  289. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  290. package/dist/trajectory/TrajectoryInfoPane.svelte +3 -2
  291. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  292. package/dist/trajectory/constants.js +6 -2
  293. package/dist/trajectory/extract.js +17 -37
  294. package/dist/trajectory/format-detect.d.ts +0 -1
  295. package/dist/trajectory/format-detect.js +3 -9
  296. package/dist/trajectory/frame-reader.d.ts +0 -1
  297. package/dist/trajectory/frame-reader.js +62 -128
  298. package/dist/trajectory/helpers.d.ts +10 -2
  299. package/dist/trajectory/helpers.js +56 -36
  300. package/dist/trajectory/parse/ase.d.ts +9 -1
  301. package/dist/trajectory/parse/ase.js +47 -32
  302. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  303. package/dist/trajectory/parse/diagnostics.js +14 -0
  304. package/dist/trajectory/parse/index.d.ts +1 -1
  305. package/dist/trajectory/parse/index.js +54 -102
  306. package/dist/trajectory/parse/lammps.d.ts +0 -2
  307. package/dist/trajectory/parse/lammps.js +8 -6
  308. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  309. package/dist/trajectory/parse/pymatgen.js +74 -0
  310. package/dist/trajectory/parse/vasp.js +4 -3
  311. package/dist/trajectory/parse/xyz.d.ts +9 -21
  312. package/dist/trajectory/parse/xyz.js +28 -33
  313. package/dist/trajectory/plotting.d.ts +0 -1
  314. package/dist/trajectory/plotting.js +3 -100
  315. package/dist/utils.d.ts +1 -0
  316. package/dist/utils.js +1 -1
  317. package/dist/xrd/XrdPlot.svelte +14 -29
  318. package/dist/xrd/broadening.d.ts +2 -1
  319. package/dist/xrd/calc-xrd.js +6 -11
  320. package/dist/xrd/index.d.ts +2 -2
  321. package/package.json +29 -16
  322. package/dist/element/data.json +0 -11864
  323. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  324. package/dist/fermi-surface/marching-cubes.js +0 -2
  325. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  326. package/dist/plot/core/hover-lock.svelte.js +0 -45
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { D3InterpolateName } from '../colors'
3
- import { AXIS_COLORS, get_d3_interpolator, NEG_AXIS_COLORS } from '../colors'
3
+ import { get_d3_interpolator } from '../colors'
4
4
  import type { ElementSymbol } from '../element'
5
5
  import { element_data } from '../element'
6
6
  import Isosurface from '../isosurface/Isosurface.svelte'
@@ -9,12 +9,9 @@
9
9
  import { format_num } from '../labels'
10
10
  import type { Vec3 } from '../math'
11
11
  import * as math from '../math'
12
- import type {
13
- CameraProjection,
14
- ShowBonds,
15
- VectorColorMode,
16
- VectorLayerConfig,
17
- } from '../settings'
12
+ import { bind_renderer, build_orbit_props, SceneCamera } from '../scene'
13
+ import type { SceneControlProps } from '../scene'
14
+ import type { ShowBonds, VectorColorMode, VectorLayerConfig } from '../settings'
18
15
  import { DEFAULTS } from '../settings'
19
16
  import { create_pulse_animation } from '../effects.svelte'
20
17
  import { colors } from '../state.svelte'
@@ -42,6 +39,9 @@
42
39
  get_orig_site_idx,
43
40
  get_property_colors,
44
41
  } from './atom-properties'
42
+ import type { SymmetryElement } from '../symmetry'
43
+ import { has_visible_symmetry_overlay } from '../symmetry/symmetry-elements'
44
+ import SymmetryElements from '../symmetry/SymmetryElements.svelte'
45
45
  import * as measure from './measure'
46
46
  import {
47
47
  compute_slice_geometry,
@@ -49,11 +49,12 @@
49
49
  PARTIAL_OCCUPANCY_CAP_ARC,
50
50
  } from './partial-occupancy'
51
51
  import type { MoyoDataset } from '@spglib/moyo-wasm'
52
- import { T, useThrelte } from '@threlte/core'
52
+ import { T } from '@threlte/core'
53
53
  import * as extras from '@threlte/extras'
54
54
  import { type ComponentProps, type Snippet, untrack } from 'svelte'
55
55
  import { SvelteMap, SvelteSet } from 'svelte/reactivity'
56
- import { type Camera, Color, type Mesh, type Object3D, type Scene, Vector3 } from 'three'
56
+ import { BufferAttribute, BufferGeometry, Color, DoubleSide, Vector3 } from 'three'
57
+ import type { Mesh, Object3D } from 'three'
57
58
  import Bond from './Bond.svelte'
58
59
  import type { BondEditResult, BondingStrategy, BondKeyTarget } from './bonding'
59
60
  import {
@@ -65,6 +66,7 @@
65
66
  get_bond_key,
66
67
  get_bond_render_matrices,
67
68
  get_explicit_bond_metadata,
69
+ get_majority_element,
68
70
  set_bond_order as apply_set_bond_order,
69
71
  structure_bond_to_bond_pair,
70
72
  } from './bonding'
@@ -77,7 +79,9 @@
77
79
  choose_site_label_offset,
78
80
  LABEL_OFFSET_EPS,
79
81
  make_label_position_calculator,
80
- } from './label-placement'
82
+ } from './atom-label-placement'
83
+ import type { PolyhedraColorMode, Polyhedron } from './polyhedra'
84
+ import { compute_polyhedra, merge_polyhedra_buffers } from './polyhedra'
81
85
 
82
86
  type InstancedAtomGroup = {
83
87
  element: string
@@ -168,12 +172,27 @@
168
172
  auto_bond_order = DEFAULTS.structure.auto_bond_order,
169
173
  aromatic_display = DEFAULTS.structure.aromatic_display,
170
174
  bonding_options = {},
175
+ show_polyhedra = DEFAULTS.structure.show_polyhedra,
176
+ polyhedra_opacity = DEFAULTS.structure.polyhedra_opacity,
177
+ polyhedra_show_edges = DEFAULTS.structure.polyhedra_show_edges,
178
+ polyhedra_edge_color = DEFAULTS.structure.polyhedra_edge_color,
179
+ polyhedra_color_mode = DEFAULTS.structure.polyhedra_color_mode,
180
+ polyhedra_color = DEFAULTS.structure.polyhedra_color,
181
+ polyhedra_hide_center_atoms = DEFAULTS.structure.polyhedra_hide_center_atoms,
182
+ polyhedra_min_neighbors = DEFAULTS.structure.polyhedra_min_neighbors,
183
+ polyhedra_max_neighbors = DEFAULTS.structure.polyhedra_max_neighbors,
184
+ polyhedra_excluded_elements = DEFAULTS.structure.polyhedra_excluded_elements,
185
+ polyhedra_included_elements = DEFAULTS.structure.polyhedra_included_elements,
186
+ polyhedra_rendered_elements = $bindable<string[]>([]),
171
187
  fov = DEFAULTS.structure.fov,
172
188
  initial_zoom = DEFAULTS.structure.initial_zoom,
173
189
  ambient_light = DEFAULTS.structure.ambient_light,
174
190
  directional_light = DEFAULTS.structure.directional_light,
175
191
  sphere_segments = DEFAULTS.structure.sphere_segments,
176
192
  lattice_props = {},
193
+ symmetry_elements = [],
194
+ symmetry_elements_props = {},
195
+ symmetry_declutter = true,
177
196
  atom_label,
178
197
  camera_is_moving = $bindable(false),
179
198
  width = 0,
@@ -218,7 +237,7 @@
218
237
  dragging_atoms = $bindable(false),
219
238
  volumetric_data = undefined,
220
239
  isosurface_settings = DEFAULT_ISOSURFACE_SETTINGS,
221
- }: {
240
+ }: SceneControlProps & {
222
241
  structure?: AnyStructure
223
242
  base_structure?: AnyStructure // The original structure without image atoms, used for property color calculation
224
243
  atom_radius?: number // scale factor for atomic radii
@@ -226,15 +245,6 @@
226
245
  // determined by the atomic radius of the element
227
246
  camera_position?: [x: number, y: number, z: number] // initial camera position from which to render the scene
228
247
  camera_target?: Vec3 // external orbit-controls target for pan synchronization
229
- camera_projection?: CameraProjection // camera projection type
230
- rotation_damping?: number // rotation damping factor (how quickly the rotation comes to rest after mouse release)
231
- // zoom level of the camera
232
- max_zoom?: number
233
- min_zoom?: number
234
- rotate_speed?: number // rotation speed. set to 0 to disable rotation.
235
- zoom_speed?: number // zoom speed. set to 0 to disable zooming.
236
- pan_speed?: number // pan speed. set to 0 to disable panning.
237
- zoom_to_cursor?: boolean // zoom toward cursor position instead of scene center
238
248
  show_atoms?: boolean
239
249
  show_bonds?: ShowBonds
240
250
  show_site_labels?: boolean
@@ -250,30 +260,46 @@
250
260
  vector_shaft_radius?: number
251
261
  vector_arrow_head_radius?: number
252
262
  vector_arrow_head_length?: number
253
- gizmo?: boolean | ComponentProps<typeof extras.Gizmo>
254
263
  hovered_idx?: number | null
255
264
  hovered_site?: Site | null
256
265
  float_fmt?: string
257
- auto_rotate?: number
258
- initial_zoom?: number
259
266
  bond_thickness?: number
260
267
  bond_color?: string
261
268
  bonding_strategy?: BondingStrategy
262
269
  auto_bond_order?: boolean
263
270
  aromatic_display?: `aromatic` | `kekule`
264
271
  bonding_options?: Record<string, unknown>
265
- fov?: number
266
- ambient_light?: number
267
- directional_light?: number
272
+ show_polyhedra?: ShowBonds // when to render coordination polyhedra
273
+ polyhedra_opacity?: number
274
+ polyhedra_show_edges?: boolean
275
+ polyhedra_edge_color?: string
276
+ polyhedra_color_mode?: PolyhedraColorMode
277
+ polyhedra_color?: string // custom color used when polyhedra_color_mode is 'uniform'
278
+ polyhedra_hide_center_atoms?: boolean
279
+ polyhedra_min_neighbors?: number // min coordination number to form a polyhedron
280
+ polyhedra_max_neighbors?: number // max CN - skips e.g. CN-12 cuboctahedra
281
+ polyhedra_excluded_elements?: readonly string[] // elements never used as polyhedra centers
282
+ polyhedra_included_elements?: readonly string[] // force-include (bypasses spectator hiding)
283
+ polyhedra_rendered_elements?: string[] // (output) elements that currently have polyhedra
268
284
  sphere_segments?: number
269
285
  lattice_props?: ComponentProps<typeof Lattice>
286
+ // Symmetry elements (from symmetry_elements_from_ops) to overlay on the structure.
287
+ // Fractional coords must refer to the SAME cell as the rendered lattice (moyo
288
+ // operations are in the input-cell frame, i.e. the original untransformed cell).
289
+ symmetry_elements?: SymmetryElement[]
290
+ symmetry_elements_props?: Omit<ComponentProps<typeof SymmetryElements>, `elements` | `lattice`>
291
+ // Auto-reduce visual clutter while a symmetry-element overlay is visible: hides
292
+ // coordination polyhedra and calculated bonds, and shrinks atoms so axes/planes/
293
+ // centers stay readable. Purely derived — toggling the overlay off restores the
294
+ // configured appearance.
295
+ symmetry_declutter?: boolean
270
296
  atom_label?: Snippet<[{ site: Site; site_idx: number }]>
271
297
  site_label_size?: number
272
298
  site_label_offset?: Vec3
273
299
  site_label_bg_color?: string
274
300
  site_label_color?: string
275
301
  site_label_padding?: number
276
- camera_is_moving?: boolean // used to prevent tooltip from showing while camera is moving
302
+ camera_is_moving?: boolean // bindable: true while orbit controls are active
277
303
  width?: number // Viewer dimensions for responsive zoom
278
304
  height?: number
279
305
  // measurement props
@@ -291,9 +317,6 @@
291
317
  active_sites?: number[]
292
318
  active_highlight_color?: string
293
319
  rotation?: Vec3 // rotation control prop
294
- // Expose scene and camera for external use (e.g. export pane)
295
- scene?: Scene
296
- camera?: Camera
297
320
  orbit_controls?: ComponentProps<typeof extras.OrbitControls>[`ref`] // OrbitControls instance
298
321
  rotation_target_ref?: Vec3 // Expose rotation target for reset
299
322
  initial_computed_zoom?: number // Expose initial zoom for reset
@@ -322,13 +345,9 @@
322
345
  )
323
346
  let pulse_opacity = $derived(0.15 + 0.25 * pulse.unit)
324
347
 
325
- const threlte = useThrelte()
326
- $effect(() => {
327
- scene = threlte.scene
328
- camera = threlte.camera.current
329
- if (threlte.renderer) {
330
- Object.assign(threlte.renderer.domElement, { __renderer: threlte.renderer })
331
- }
348
+ bind_renderer((threlte_scene, threlte_camera) => {
349
+ scene = threlte_scene
350
+ camera = threlte_camera
332
351
  })
333
352
 
334
353
  // Expose rotation target for external reset
@@ -737,6 +756,27 @@
737
756
  return true
738
757
  }
739
758
 
759
+ // Pointer props (hover + select) shared by instanced atoms and partial-occupancy
760
+ // hit targets. is_edit_image disables interaction for ghosted PBC image atoms.
761
+ const atom_pointer_props = (site_idx: number, is_edit_image: boolean) => ({
762
+ ...atom_hover_props(site_idx, is_edit_image),
763
+ onpointerdown(event: PointerEvent) {
764
+ if (is_edit_image || measure_mode !== `edit-bonds` || bond_edit_mode !== `add`) return
765
+ select_edit_bonds_site(site_idx, event)
766
+ },
767
+ onclick(event: MouseEvent) {
768
+ if (is_edit_image) return
769
+ if (measure_mode === `edit-bonds`) {
770
+ if (bond_edit_mode !== `add`) return
771
+ if (skip_duplicate_edit_bonds_click(site_idx)) {
772
+ event.stopPropagation()
773
+ return
774
+ }
775
+ }
776
+ toggle_selection(site_idx, event)
777
+ },
778
+ })
779
+
740
780
  function toggle_selection(site_index: number, evt?: Event) {
741
781
  evt?.stopPropagation?.()
742
782
  const event_with_native = evt as Event & { nativeEvent?: unknown } | undefined
@@ -852,7 +892,7 @@
852
892
 
853
893
  let rotation_target = $derived(
854
894
  lattice
855
- ? (math.scale(math.add(...lattice.matrix), 0.5) as Vec3)
895
+ ? math.scale(math.add(...lattice.matrix), 0.5)
856
896
  : structure
857
897
  ? get_center_of_mass(structure)
858
898
  : [0, 0, 0] as Vec3,
@@ -923,16 +963,43 @@
923
963
  camera_position = [distance, distance * 0.3, distance * 0.8]
924
964
  }
925
965
  })
966
+ // Whether a never|always|crystals|molecules setting applies to the current structure
967
+ const applies_to_structure = (when: ShowBonds): boolean =>
968
+ when === `always` ||
969
+ (when === `crystals` && Boolean(lattice)) ||
970
+ (when === `molecules` && !lattice)
971
+
972
+ // Declutter while a symmetry-element overlay actually draws something (elements present
973
+ // AND an enabled kind among them): hide coordination polyhedra/bonds and shrink atoms so
974
+ // axes/planes/centers stay readable. Gating on visibility (not just `symmetry_elements`)
975
+ // avoids hiding everything when nothing renders in its place — e.g. an inversion-only
976
+ // cell under the rotation-only default. Pure derived overrides: the configured
977
+ // appearance returns untouched the moment the overlay goes away.
978
+ const declutter_active = $derived(
979
+ symmetry_declutter &&
980
+ has_visible_symmetry_overlay(symmetry_elements, symmetry_elements_props.show_kinds),
981
+ )
982
+ const effective_show_polyhedra: ShowBonds = $derived(
983
+ declutter_active ? `never` : show_polyhedra,
984
+ )
985
+ // Calculated bonds are hidden in declutter mode (only their cylinders — bond_pairs
986
+ // stay computed so tooltips and manually added bonds keep working)
987
+ const effective_show_bonds: ShowBonds = $derived(
988
+ declutter_active ? `never` : show_bonds,
989
+ )
990
+ const effective_atom_radius = $derived(declutter_active ? atom_radius * 0.6 : atom_radius)
991
+
926
992
  $effect(() => {
927
- if (structure && show_bonds !== `never`) {
928
- // Determine if we should show bonds based on the setting and structure type
929
- const should_show_bonds = show_bonds === `always` ||
930
- (show_bonds === `crystals` && lattice) ||
931
- (show_bonds === `molecules` && !lattice)
932
-
933
- if (should_show_bonds) {
934
- bond_pairs = BONDING_STRATEGIES[bonding_strategy](structure, bonding_options)
935
- } else bond_pairs = []
993
+ // Bonds are computed when either bond rendering or polyhedra need them. The
994
+ // raw/effective mix is deliberate: RAW show_bonds keeps bond_pairs available
995
+ // during symmetry declutter (cylinders hide via effective_show_bonds in
996
+ // bonds_to_render, but tooltips + manually added bonds still need the data),
997
+ // while EFFECTIVE show_polyhedra skips computing bonds whose only consumer —
998
+ // the polyhedra $derived below, gated on the same effective value — won't run.
999
+ const want_bonds = applies_to_structure(show_bonds)
1000
+ const want_polyhedra = applies_to_structure(effective_show_polyhedra)
1001
+ if (structure && (want_bonds || want_polyhedra)) {
1002
+ bond_pairs = BONDING_STRATEGIES[bonding_strategy](structure, bonding_options)
936
1003
  } else bond_pairs = []
937
1004
  })
938
1005
 
@@ -957,9 +1024,16 @@
957
1024
  return total_occu > 0 ? weighted_sum / total_occu : 1
958
1025
  }
959
1026
 
1027
+ // Disordered sites are often stored as separate split sites (one species each)
1028
+ // at the same position; merge_split_partial_sites groups them into one render
1029
+ // site whose `species` holds every element. Shared by atom rendering and the
1030
+ // hover tooltip so it lists all elements, not just the majority one.
1031
+ let render_sites = $derived(
1032
+ structure?.sites ? merge_split_partial_sites(structure.sites, hidden_elements) : [],
1033
+ )
1034
+
960
1035
  let atom_data = $derived.by(() => {
961
- if (!show_atoms || !structure?.sites) return []
962
- const render_sites = merge_split_partial_sites(structure.sites, hidden_elements)
1036
+ if (!show_atoms) return []
963
1037
  return render_sites.flatMap(({ site_idx, site, is_image_atom }) => {
964
1038
  const orig_idx = get_orig_site_idx(site, site_idx)
965
1039
 
@@ -967,12 +1041,26 @@
967
1041
  const prop_val = property_colors?.values[orig_idx]
968
1042
  if (prop_val !== undefined && hidden_prop_vals.has(prop_val)) return []
969
1043
 
1044
+ // Optionally hide atoms at the center of a rendered polyhedron
1045
+ if (polyhedra_hide_center_atoms && polyhedra_center_site_idxs.has(site_idx)) {
1046
+ return []
1047
+ }
1048
+
1049
+ // Phase-2 PBC images exist only to complete bonds/coordination polyhedra at
1050
+ // cell faces. When neither renders (polyhedra toggled off, symmetry declutter,
1051
+ // …) they'd float disconnected outside the cell — hide them.
1052
+ if (
1053
+ site.properties?.completion_image &&
1054
+ !applies_to_structure(effective_show_bonds) &&
1055
+ !applies_to_structure(effective_show_polyhedra)
1056
+ ) return []
1057
+
970
1058
  // Calculate radius: same_size > site override > element override > default
971
1059
  // All radii scale uniformly with atom_radius for consistent slider behavior
972
1060
  const base_radius = same_size_atoms
973
1061
  ? 1
974
1062
  : site_radius_overrides?.get(site_idx) ?? calc_weighted_radius(site)
975
- const radius = base_radius * atom_radius
1063
+ const radius = base_radius * effective_atom_radius
976
1064
 
977
1065
  // Use property color if available (e.g. coordination number, Wyckoff position)
978
1066
  // Otherwise, each species gets its own element color (important for disordered sites)
@@ -1079,13 +1167,106 @@
1079
1167
  return [...calculated, ...added]
1080
1168
  })
1081
1169
 
1170
+ // Bonds drawn as cylinders. When show_bonds doesn't apply, calculated bonds are
1171
+ // hidden but manually added bonds stay visible (bond_pairs may still be computed
1172
+ // for polyhedra, so this can't rely on bond_pairs being empty).
1173
+ let bonds_to_render = $derived.by(() => {
1174
+ if (applies_to_structure(effective_show_bonds)) return filtered_bond_pairs
1175
+ const added_keys = new Set(added_bonds.map(bond_key_for))
1176
+ return filtered_bond_pairs.filter((bond) => added_keys.has(bond_key_for(bond)))
1177
+ })
1178
+
1082
1179
  let editable_bond_pairs = $derived(
1083
- bond_edits_enabled ? filtered_bond_pairs.filter(can_edit_bond) : [],
1180
+ bond_edits_enabled ? bonds_to_render.filter(can_edit_bond) : [],
1181
+ )
1182
+
1183
+ // Coordination polyhedra around cation-like centers, derived from the same
1184
+ // (edited, filtered) bond graph as rendered bonds so the two never disagree.
1185
+ // Colors are resolved in polyhedra_buffers below, so color-scheme/mode changes
1186
+ // never recompute the hull geometry.
1187
+ let polyhedra: Polyhedron[] = $derived.by(() => {
1188
+ if (
1189
+ !structure?.sites || dragging_atoms ||
1190
+ !applies_to_structure(effective_show_polyhedra) ||
1191
+ filtered_bond_pairs.length === 0
1192
+ ) return []
1193
+ return compute_polyhedra(structure, filtered_bond_pairs, {
1194
+ min_neighbors: polyhedra_min_neighbors,
1195
+ max_neighbors: polyhedra_max_neighbors,
1196
+ excluded_center_elements: polyhedra_excluded_elements,
1197
+ included_center_elements: polyhedra_included_elements,
1198
+ })
1199
+ })
1200
+
1201
+ // Color of a site: property color (coordination/Wyckoff modes) or element color
1202
+ const polyhedra_site_color = (site_idx: number): string => {
1203
+ const site = structure?.sites[site_idx]
1204
+ const orig_idx = get_orig_site_idx(site, site_idx)
1205
+ const element = get_majority_element(site)
1206
+ return property_colors?.colors[orig_idx] ??
1207
+ (element && colors.element?.[element]) ?? `#808080`
1208
+ }
1209
+
1210
+ // Separate derived so material-only changes (opacity, edge color) don't rebuild
1211
+ // buffers and color changes don't rebuild hulls
1212
+ let polyhedra_buffers = $derived.by(() => {
1213
+ if (polyhedra.length === 0) return null
1214
+ const get_vertex_color = (poly: Polyhedron, vertex_idx: number): string => {
1215
+ if (polyhedra_color_mode === `uniform`) return polyhedra_color
1216
+ if (polyhedra_color_mode === `center`) {
1217
+ return polyhedra_site_color(poly.center_site_idx)
1218
+ }
1219
+ // 'vertex' (default): each corner takes the color of the atom that forms it
1220
+ return polyhedra_site_color(poly.vertex_site_idxs[vertex_idx])
1221
+ }
1222
+ return merge_polyhedra_buffers(polyhedra, get_vertex_color)
1223
+ })
1224
+
1225
+ let polyhedra_center_site_idxs = $derived(
1226
+ new Set(polyhedra.map((poly) => poly.center_site_idx)),
1084
1227
  )
1085
1228
 
1229
+ // Publish which elements currently anchor polyhedra (consumed by controls so
1230
+ // per-element toggles reflect the actual render state incl. spectator hiding)
1231
+ $effect(() => {
1232
+ const elems = [...new Set(polyhedra.map((poly) => poly.center_element))].sort()
1233
+ if (elems.join(`,`) !== polyhedra_rendered_elements.join(`,`)) {
1234
+ polyhedra_rendered_elements = elems
1235
+ }
1236
+ })
1237
+
1238
+ // Geometries with proper disposal on dependency change (same pattern as ReferencePlane)
1239
+ const buffer_geometry = (attrs: Record<string, Float32Array>): BufferGeometry => {
1240
+ const geo = new BufferGeometry()
1241
+ for (const [name, array] of Object.entries(attrs)) {
1242
+ geo.setAttribute(name, new BufferAttribute(array, 3))
1243
+ }
1244
+ return geo
1245
+ }
1246
+ let polyhedra_geometry: BufferGeometry | null = $state(null)
1247
+ $effect(() => {
1248
+ let geo: BufferGeometry | null = null
1249
+ if (polyhedra_buffers && polyhedra_buffers.triangle_count > 0) {
1250
+ const { positions: position, colors: color } = polyhedra_buffers
1251
+ geo = buffer_geometry({ position, color })
1252
+ geo.computeVertexNormals() // non-indexed -> per-face normals (flat shading)
1253
+ }
1254
+ polyhedra_geometry = geo
1255
+ return () => geo?.dispose()
1256
+ })
1257
+
1258
+ let polyhedra_edge_geometry: BufferGeometry | null = $state(null)
1259
+ $effect(() => {
1260
+ const geo = polyhedra_show_edges && polyhedra_buffers && polyhedra_buffers.edge_count > 0
1261
+ ? buffer_geometry({ position: polyhedra_buffers.edge_positions })
1262
+ : null
1263
+ polyhedra_edge_geometry = geo
1264
+ return () => geo?.dispose()
1265
+ })
1266
+
1086
1267
  let smart_site_label_offsets = $derived.by(() => {
1087
1268
  const offsets = new SvelteMap<number, Vec3>()
1088
- if (filtered_bond_pairs.length === 0) return offsets
1269
+ if (bonds_to_render.length === 0) return offsets
1089
1270
 
1090
1271
  const bond_directions_by_site = new SvelteMap<number, Vec3[]>()
1091
1272
  const add_bond_direction = (site_idx: number, pos_1: Vec3, pos_2: Vec3) => {
@@ -1100,7 +1281,7 @@
1100
1281
  ])
1101
1282
  }
1102
1283
 
1103
- for (const { site_idx_1, site_idx_2, pos_1, pos_2 } of filtered_bond_pairs) {
1284
+ for (const { site_idx_1, site_idx_2, pos_1, pos_2 } of bonds_to_render) {
1104
1285
  add_bond_direction(site_idx_1, pos_1, pos_2)
1105
1286
  add_bond_direction(site_idx_2, pos_2, pos_1)
1106
1287
  }
@@ -1111,7 +1292,7 @@
1111
1292
  })
1112
1293
 
1113
1294
  let instanced_bond_groups = $derived.by(() => {
1114
- if (!structure?.sites || filtered_bond_pairs.length === 0) return []
1295
+ if (!structure?.sites || bonds_to_render.length === 0) return []
1115
1296
 
1116
1297
  const group = {
1117
1298
  thickness: bond_thickness,
@@ -1124,7 +1305,7 @@
1124
1305
  }[],
1125
1306
  }
1126
1307
 
1127
- for (const bond_data of filtered_bond_pairs) {
1308
+ for (const bond_data of bonds_to_render) {
1128
1309
  const site_a = structure.sites[bond_data.site_idx_1]
1129
1310
  const site_b = structure.sites[bond_data.site_idx_2]
1130
1311
 
@@ -1154,6 +1335,24 @@
1154
1335
  return map
1155
1336
  })
1156
1337
 
1338
+ // Partial-occupancy atoms render as separate wedge (lune) meshes that converge
1339
+ // to a point at the sphere's poles, leaving the ball hard to hover from some
1340
+ // angles. Give each such site one invisible full-sphere hit target so it's as
1341
+ // reliably hoverable as an ordered atom (single solid sphere). One per site.
1342
+ let partial_hit_targets = $derived.by(() => {
1343
+ const targets = new SvelteMap<number, EditableAtomHitTarget & { is_image_atom: boolean }>()
1344
+ for (const atom of atom_data) {
1345
+ if (!atom.has_partial_occupancy || targets.has(atom.site_idx)) continue
1346
+ targets.set(atom.site_idx, {
1347
+ site_idx: atom.site_idx,
1348
+ position: atom.position,
1349
+ radius: atom.radius,
1350
+ is_image_atom: atom.is_image_atom,
1351
+ })
1352
+ }
1353
+ return [...targets.values()]
1354
+ })
1355
+
1157
1356
  let editable_atom_hit_targets = $derived.by(() => {
1158
1357
  if (
1159
1358
  measure_mode !== `edit-bonds` ||
@@ -1183,7 +1382,7 @@
1183
1382
  ? site_radius_overrides?.get(site_idx)
1184
1383
  : undefined
1185
1384
  const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site)
1186
- return base_radius * atom_radius
1385
+ return base_radius * effective_atom_radius
1187
1386
  }
1188
1387
 
1189
1388
  // Interpolate between spin-down (#3498db blue) and spin-up (#e74c3c red)
@@ -1259,9 +1458,9 @@
1259
1458
  const offsets = new SvelteMap<string, Vec3>()
1260
1459
  for (const [idx, key] of site_keys.entries()) {
1261
1460
  const angle = (2 * Math.PI * idx) / n_keys
1262
- const dx = math.scale(u_vec, gap_abs * Math.cos(angle)) as Vec3
1263
- const dy = math.scale(v_vec, gap_abs * Math.sin(angle)) as Vec3
1264
- offsets.set(key, math.add(dx, dy) as Vec3)
1461
+ const dx = math.scale(u_vec, gap_abs * Math.cos(angle))
1462
+ const dy = math.scale(v_vec, gap_abs * Math.sin(angle))
1463
+ offsets.set(key, math.add(dx, dy))
1265
1464
  }
1266
1465
  return offsets
1267
1466
  })
@@ -1315,7 +1514,7 @@
1315
1514
  }
1316
1515
 
1317
1516
  const offset = site_offsets?.[site_idx]?.get(key)
1318
- const position = offset ? math.add(site.xyz, offset) as Vec3 : site.xyz
1517
+ const position = offset ? math.add(site.xyz, offset) : site.xyz
1319
1518
  const arrow_vec = vector_normalize ? math.normalize_vec(vec) : vec
1320
1519
 
1321
1520
  return {
@@ -1364,57 +1563,29 @@
1364
1563
  ),
1365
1564
  )
1366
1565
 
1367
- let gizmo_props = $derived.by(() => {
1368
- const axis_options = Object.fromEntries(
1369
- [...AXIS_COLORS, ...NEG_AXIS_COLORS].map(([axis, color, hover_color]) => [
1370
- axis,
1371
- {
1372
- color,
1373
- labelColor: `#111`,
1374
- opacity: axis.startsWith(`n`) ? 0.9 : 0.8,
1375
- hover: {
1376
- color: hover_color,
1377
- labelColor: `#222222`,
1378
- opacity: axis.startsWith(`n`) ? 1 : 0.9,
1379
- },
1380
- },
1381
- ]),
1382
- )
1383
- return {
1384
- background: { enabled: false },
1385
- className: `responsive-gizmo`,
1386
- ...axis_options,
1387
- ...(typeof gizmo === `boolean` ? {} : gizmo),
1388
- offset: { left: 5, bottom: 5 },
1389
- }
1390
- })
1391
-
1392
- let orbit_controls_props = $derived({
1393
- position: [0, 0, 0],
1394
- enableRotate: rotate_speed > 0,
1395
- rotateSpeed: rotate_speed,
1396
- enableZoom: zoom_speed > 0,
1397
- zoomSpeed: camera_projection === `orthographic` ? zoom_speed * 2 : zoom_speed,
1398
- zoomToCursor: zoom_to_cursor,
1399
- enablePan: pan_speed > 0,
1400
- panSpeed: pan_speed,
1566
+ let orbit_controls_props = $derived(build_orbit_props({
1567
+ camera_projection,
1401
1568
  target: camera_target ?? rotation_target,
1402
- maxZoom: max_zoom,
1403
- minZoom: min_zoom,
1404
- autoRotate: Boolean(auto_rotate),
1405
- autoRotateSpeed: auto_rotate,
1406
- enableDamping: Boolean(rotation_damping),
1407
- dampingFactor: rotation_damping,
1408
- onstart: () => {
1409
- camera_is_moving = true
1569
+ rotate_speed,
1570
+ zoom_speed,
1571
+ zoom_to_cursor,
1572
+ pan_speed,
1573
+ max_zoom,
1574
+ min_zoom,
1575
+ auto_rotate,
1576
+ rotation_damping,
1577
+ set_camera_is_moving: (moving) => (camera_is_moving = moving),
1578
+ // Close hover tooltips + bond context menu while the camera moves. Only hide the
1579
+ // VISIBLE menu (not bond_context_target): clicking a menu button fires this
1580
+ // orbit-controls start handler before the button's own handler runs, which still
1581
+ // needs the target bond to apply the edit (see bond_context_target comment).
1582
+ onstart_extra: () => {
1410
1583
  cancel_atom_hover_clear()
1411
1584
  hovered_idx = null
1585
+ hovered_bond_key = null
1412
1586
  bond_context_menu = null
1413
1587
  },
1414
- onend: () => {
1415
- camera_is_moving = false
1416
- },
1417
- })
1588
+ }))
1418
1589
 
1419
1590
  let measure_line_color = $derived.by(() => {
1420
1591
  if (typeof window === `undefined`) return
@@ -1491,31 +1662,17 @@
1491
1662
  </extras.HTML>
1492
1663
  {/snippet}
1493
1664
 
1494
- {#if camera_projection === `perspective`}
1495
- <T.PerspectiveCamera
1496
- makeDefault
1497
- position={camera_position}
1498
- {fov}
1499
- near={camera_near}
1500
- far={camera_far}
1501
- >
1502
- <extras.OrbitControls bind:ref={orbit_controls} {...orbit_controls_props}>
1503
- {#if gizmo}<extras.Gizmo {...gizmo_props} />{/if}
1504
- </extras.OrbitControls>
1505
- </T.PerspectiveCamera>
1506
- {:else}
1507
- <T.OrthographicCamera
1508
- makeDefault
1509
- position={camera_position}
1510
- zoom={computed_zoom}
1511
- near={-100}
1512
- far={camera_far}
1513
- >
1514
- <extras.OrbitControls bind:ref={orbit_controls} {...orbit_controls_props}>
1515
- {#if gizmo}<extras.Gizmo {...gizmo_props} />{/if}
1516
- </extras.OrbitControls>
1517
- </T.OrthographicCamera>
1518
- {/if}
1665
+ <SceneCamera
1666
+ {camera_projection}
1667
+ position={camera_position}
1668
+ {fov}
1669
+ zoom={computed_zoom}
1670
+ near={camera_near}
1671
+ far={camera_far}
1672
+ orbit_props={orbit_controls_props}
1673
+ {gizmo}
1674
+ bind:orbit_controls
1675
+ />
1519
1676
 
1520
1677
  <T.DirectionalLight position={[3, 10, 10]} intensity={directional_light} />
1521
1678
  <T.AmbientLight intensity={ambient_light} />
@@ -1545,28 +1702,7 @@
1545
1702
  <extras.Instance
1546
1703
  position={atom.position}
1547
1704
  scale={atom.radius}
1548
- {...atom_hover_props(atom.site_idx, edit_mode_image)}
1549
- onpointerdown={(event: PointerEvent) => {
1550
- if (
1551
- edit_mode_image ||
1552
- measure_mode !== `edit-bonds` ||
1553
- bond_edit_mode !== `add`
1554
- ) {
1555
- return
1556
- }
1557
- select_edit_bonds_site(atom.site_idx, event)
1558
- }}
1559
- onclick={(event: MouseEvent) => {
1560
- if (edit_mode_image) return
1561
- if (measure_mode === `edit-bonds`) {
1562
- if (bond_edit_mode !== `add`) return
1563
- if (skip_duplicate_edit_bonds_click(atom.site_idx)) {
1564
- event.stopPropagation()
1565
- return
1566
- }
1567
- }
1568
- toggle_selection(atom.site_idx, event)
1569
- }}
1705
+ {...atom_pointer_props(atom.site_idx, edit_mode_image)}
1570
1706
  />
1571
1707
  {/each}
1572
1708
  </extras.InstancedMesh>
@@ -1579,32 +1715,9 @@
1579
1715
  }
1580
1716
  {@const partial_edit_image = measure_mode === `edit-atoms` && atom.is_image_atom}
1581
1717
  {@const ghost_opacity = partial_edit_image ? 0.5 : 1}
1582
- <T.Group
1583
- position={atom.position}
1584
- scale={atom.radius}
1585
- {...atom_hover_props(atom.site_idx, partial_edit_image)}
1586
- onpointerdown={(event: PointerEvent) => {
1587
- if (
1588
- partial_edit_image ||
1589
- measure_mode !== `edit-bonds` ||
1590
- bond_edit_mode !== `add`
1591
- ) {
1592
- return
1593
- }
1594
- select_edit_bonds_site(atom.site_idx, event)
1595
- }}
1596
- onclick={(event: MouseEvent) => {
1597
- if (partial_edit_image) return
1598
- if (measure_mode === `edit-bonds`) {
1599
- if (bond_edit_mode !== `add`) return
1600
- if (skip_duplicate_edit_bonds_click(atom.site_idx)) {
1601
- event.stopPropagation()
1602
- return
1603
- }
1604
- }
1605
- toggle_selection(atom.site_idx, event)
1606
- }}
1607
- >
1718
+ <!-- Visual only: pointer interaction handled by the invisible full-sphere
1719
+ hit targets below (wedge meshes leave gaps at the poles). -->
1720
+ <T.Group position={atom.position} scale={atom.radius}>
1608
1721
  {@const partial_color = partial_edit_image
1609
1722
  ? desaturate(atom.color)
1610
1723
  : atom.color}
@@ -1625,7 +1738,7 @@
1625
1738
  />
1626
1739
  </T.Mesh>
1627
1740
 
1628
- {#if atom.has_partial_occupancy && atom.render_start_cap}
1741
+ {#if atom.render_start_cap}
1629
1742
  <T.Mesh rotation={[0, atom.start_phi, 0]}>
1630
1743
  <T.CircleGeometry
1631
1744
  args={[
@@ -1643,7 +1756,7 @@
1643
1756
  />
1644
1757
  </T.Mesh>
1645
1758
  {/if}
1646
- {#if atom.has_partial_occupancy && atom.render_end_cap}
1759
+ {#if atom.render_end_cap}
1647
1760
  <T.Mesh rotation={[0, atom.end_phi, 0]}>
1648
1761
  <T.CircleGeometry
1649
1762
  args={[
@@ -1670,6 +1783,20 @@
1670
1783
  {/if}
1671
1784
  {/each}
1672
1785
 
1786
+ <!-- Invisible full-sphere hit targets for partial-occupancy sites so the
1787
+ whole ball is hoverable/clickable (wedge meshes leave gaps at the poles). -->
1788
+ {#each partial_hit_targets as hit (hit.site_idx)}
1789
+ {@const hit_edit_image = measure_mode === `edit-atoms` && hit.is_image_atom}
1790
+ <T.Mesh
1791
+ position={hit.position}
1792
+ scale={hit.radius}
1793
+ {...atom_pointer_props(hit.site_idx, hit_edit_image)}
1794
+ >
1795
+ <T.SphereGeometry args={[0.5, sphere_segments, sphere_segments]} />
1796
+ <T.MeshBasicMaterial transparent opacity={0} depthWrite={false} />
1797
+ </T.Mesh>
1798
+ {/each}
1799
+
1673
1800
  <!-- Site labels/indices for instanced atoms -->
1674
1801
  {#if show_site_labels || show_site_indices}
1675
1802
  {#each unique_instanced_atoms as atom (atom.site_idx)}
@@ -1696,6 +1823,32 @@
1696
1823
  {/each}
1697
1824
  {/if}
1698
1825
 
1826
+ <!-- Coordination polyhedra: all faces in one merged mesh, edges in one
1827
+ LineSegments (1-2 draw calls regardless of supercell size) -->
1828
+ {#if polyhedra_geometry}
1829
+ <T.Mesh geometry={polyhedra_geometry} frustumCulled={false} raycast={() => null}>
1830
+ <!-- depthWrite when mostly opaque: VESTA-like occlusion between polyhedra;
1831
+ fully translucent settings fall back to see-through blending -->
1832
+ <T.MeshStandardMaterial
1833
+ vertexColors
1834
+ transparent={polyhedra_opacity < 1}
1835
+ opacity={polyhedra_opacity}
1836
+ side={DoubleSide}
1837
+ depthWrite={polyhedra_opacity >= 0.65}
1838
+ flatShading
1839
+ />
1840
+ </T.Mesh>
1841
+ {#if polyhedra_edge_geometry}
1842
+ <T.LineSegments
1843
+ geometry={polyhedra_edge_geometry}
1844
+ frustumCulled={false}
1845
+ raycast={() => null}
1846
+ >
1847
+ <T.LineBasicMaterial color={polyhedra_edge_color} />
1848
+ </T.LineSegments>
1849
+ {/if}
1850
+ {/if}
1851
+
1699
1852
  <!-- Clickable bond hit-test cylinders in edit-bonds mode -->
1700
1853
  {#if measure_mode === `edit-bonds` && editable_bond_pairs.length > 0}
1701
1854
  {#each editable_bond_pairs as
@@ -1910,8 +2063,8 @@
1910
2063
  {@const site = structure.sites[site_index]}
1911
2064
  {#if site}
1912
2065
  <!-- shift selected site labels down to avoid overlapping regular site labels-->
1913
- {@const selection_offset = math.add(site_label_offset, [0, -0.5, 0])}
1914
- {@const pos = math.add(site.xyz, selection_offset) as Vec3}
2066
+ {@const selection_offset = math.add<Vec3>(site_label_offset, [0, -0.5, 0])}
2067
+ {@const pos = math.add(site.xyz, selection_offset)}
1915
2068
  <extras.HTML center position={pos}>
1916
2069
  <span class="selection-label">{loop_idx + 1}</span>
1917
2070
  </extras.HTML>
@@ -1952,10 +2105,13 @@
1952
2105
  .map(([elem, count]) => `${elem}: ${count}`)
1953
2106
  return ` (${parts.join(`, `)})`
1954
2107
  })()}
2108
+ {@const tooltip_species =
2109
+ render_sites.find((rs) => rs.site_idx === hovered_idx)?.site.species ??
2110
+ hovered_site.species ?? []}
1955
2111
  <CanvasTooltip position={hovered_site.xyz}>
1956
2112
  <!-- Element symbols with occupancies for disordered sites -->
1957
2113
  <div class="elements">
1958
- {#each hovered_site.species ?? [] as
2114
+ {#each tooltip_species as
1959
2115
  { element, occu, oxidation_state: oxi_state },
1960
2116
  idx
1961
2117
  (`${element ?? ``}-${occu ?? ``}-${oxi_state ?? ``}-${idx}`)
@@ -1964,16 +2120,17 @@
1964
2120
  elem.symbol === element
1965
2121
  )?.name ??
1966
2122
  ``}
1967
- {#if idx > 0}&thinsp;{/if}
1968
- {#if occu !== 1}<span class="occupancy">{
1969
- format_num(occu, `.3~f`)
1970
- }</span>{/if}
1971
- <strong>
1972
- {element}{#if oxi_state != null && oxi_state !== 0}<sup>{Math.abs(
1973
- oxi_state,
1974
- )}{oxi_state > 0 ? `+` : `−`}</sup>{/if}
1975
- </strong>
1976
- {#if element_name}<span class="elem-name">{element_name}</span>{/if}
2123
+ <span class="species">
2124
+ {#if occu !== 1}<span class="occupancy">{
2125
+ format_num(occu, `.3~f`)
2126
+ }</span>{/if}
2127
+ <strong>
2128
+ {element}{#if oxi_state != null && oxi_state !== 0}<sup>{Math.abs(
2129
+ oxi_state,
2130
+ )}{oxi_state > 0 ? `+` : `−`}</sup>{/if}
2131
+ </strong>
2132
+ {#if element_name}<span class="elem-name">{element_name}</span>{/if}
2133
+ </span>
1977
2134
  {/each}
1978
2135
  </div>
1979
2136
  <div class="coordinates fractional">abc: ({abc})</div>
@@ -1986,6 +2143,13 @@
1986
2143
 
1987
2144
  {#if visual_lattice}
1988
2145
  <Lattice matrix={visual_lattice.matrix} {...lattice_props} />
2146
+ {#if symmetry_elements.length > 0}
2147
+ <SymmetryElements
2148
+ elements={symmetry_elements}
2149
+ lattice={visual_lattice.matrix}
2150
+ {...symmetry_elements_props}
2151
+ />
2152
+ {/if}
1989
2153
  {/if}
1990
2154
 
1991
2155
  <!-- TransformControls for editing atoms in edit-atoms mode -->
@@ -2163,10 +2327,6 @@
2163
2327
  </T.Group>
2164
2328
 
2165
2329
  <style>
2166
- :global(.structure .responsive-gizmo) {
2167
- width: clamp(70px, 18cqmin, 100px) !important;
2168
- height: clamp(70px, 18cqmin, 100px) !important;
2169
- }
2170
2330
  .atom-label {
2171
2331
  background: var(--struct-atom-label-bg, rgba(0, 0, 0, 0.1));
2172
2332
  border: 0;
@@ -2180,6 +2340,13 @@
2180
2340
  .elements {
2181
2341
  margin-bottom: var(--canvas-tooltip-elements-margin);
2182
2342
  }
2343
+ .species {
2344
+ display: inline-block;
2345
+ white-space: nowrap;
2346
+ &:not(:first-child) {
2347
+ margin-left: var(--canvas-tooltip-species-gap, 0.5em);
2348
+ }
2349
+ }
2183
2350
  .occupancy {
2184
2351
  font-size: var(--canvas-tooltip-occu-font-size);
2185
2352
  opacity: var(--canvas-tooltip-occu-opacity);