matterviz 0.3.1 → 0.3.3

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 (358) hide show
  1. package/dist/EmptyState.svelte +10 -2
  2. package/dist/FilePicker.svelte +154 -96
  3. package/dist/Icon.svelte +20 -14
  4. package/dist/MillerIndexInput.svelte +27 -21
  5. package/dist/api/optimade.js +6 -6
  6. package/dist/app.css +216 -178
  7. package/dist/brillouin/BrillouinZone.svelte +299 -198
  8. package/dist/brillouin/BrillouinZone.svelte.d.ts +1 -1
  9. package/dist/brillouin/BrillouinZoneControls.svelte +32 -5
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte +74 -55
  11. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte +99 -68
  13. package/dist/brillouin/BrillouinZoneScene.svelte +277 -165
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  15. package/dist/brillouin/BrillouinZoneTooltip.svelte +17 -7
  16. package/dist/brillouin/compute.js +11 -6
  17. package/dist/chempot-diagram/ChemPotDiagram.svelte +327 -0
  18. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +13 -0
  19. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +847 -0
  20. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +16 -0
  21. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +3194 -0
  22. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +16 -0
  23. package/dist/chempot-diagram/ChemPotScene3D.svelte +11 -0
  24. package/dist/chempot-diagram/ChemPotScene3D.svelte.d.ts +7 -0
  25. package/dist/chempot-diagram/async-compute.svelte.d.ts +3 -0
  26. package/dist/chempot-diagram/async-compute.svelte.js +77 -0
  27. package/dist/chempot-diagram/chempot-worker.d.ts +1 -0
  28. package/dist/chempot-diagram/chempot-worker.js +11 -0
  29. package/dist/chempot-diagram/color.d.ts +10 -0
  30. package/dist/chempot-diagram/color.js +32 -0
  31. package/dist/chempot-diagram/compute.d.ts +48 -0
  32. package/dist/chempot-diagram/compute.js +812 -0
  33. package/dist/chempot-diagram/index.d.ts +6 -0
  34. package/dist/chempot-diagram/index.js +6 -0
  35. package/dist/chempot-diagram/pointer.d.ts +16 -0
  36. package/dist/chempot-diagram/pointer.js +40 -0
  37. package/dist/chempot-diagram/temperature.d.ts +15 -0
  38. package/dist/chempot-diagram/temperature.js +36 -0
  39. package/dist/chempot-diagram/types.d.ts +86 -0
  40. package/dist/chempot-diagram/types.js +28 -0
  41. package/dist/colors/index.d.ts +3 -1
  42. package/dist/colors/index.js +9 -3
  43. package/dist/composition/BarChart.svelte +141 -77
  44. package/dist/composition/BubbleChart.svelte +107 -52
  45. package/dist/composition/Composition.svelte +100 -79
  46. package/dist/composition/Formula.svelte +108 -62
  47. package/dist/composition/FormulaFilter.svelte +973 -353
  48. package/dist/composition/FormulaFilter.svelte.d.ts +35 -1
  49. package/dist/composition/PieChart.svelte +199 -99
  50. package/dist/composition/PieChart.svelte.d.ts +1 -1
  51. package/dist/composition/format.d.ts +5 -0
  52. package/dist/composition/format.js +20 -3
  53. package/dist/composition/parse.js +14 -9
  54. package/dist/convex-hull/ConvexHull.svelte +93 -38
  55. package/dist/convex-hull/ConvexHull2D.svelte +551 -393
  56. package/dist/convex-hull/ConvexHull3D.svelte +1303 -825
  57. package/dist/convex-hull/ConvexHull4D.svelte +1012 -686
  58. package/dist/convex-hull/ConvexHullControls.svelte +115 -28
  59. package/dist/convex-hull/ConvexHullInfoPane.svelte +29 -3
  60. package/dist/convex-hull/ConvexHullStats.svelte +821 -249
  61. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +6 -1
  62. package/dist/convex-hull/ConvexHullTooltip.svelte +41 -16
  63. package/dist/convex-hull/GasPressureControls.svelte +104 -61
  64. package/dist/convex-hull/StructurePopup.svelte +25 -4
  65. package/dist/convex-hull/TemperatureSlider.svelte +45 -25
  66. package/dist/convex-hull/barycentric-coords.js +13 -7
  67. package/dist/convex-hull/demo-temperature.d.ts +6 -0
  68. package/dist/convex-hull/demo-temperature.js +40 -0
  69. package/dist/convex-hull/gas-thermodynamics.js +17 -12
  70. package/dist/convex-hull/helpers.d.ts +10 -1
  71. package/dist/convex-hull/helpers.js +79 -38
  72. package/dist/convex-hull/index.d.ts +1 -0
  73. package/dist/convex-hull/index.js +1 -0
  74. package/dist/convex-hull/thermodynamics.d.ts +8 -21
  75. package/dist/convex-hull/thermodynamics.js +163 -69
  76. package/dist/convex-hull/types.d.ts +12 -12
  77. package/dist/convex-hull/types.js +0 -12
  78. package/dist/coordination/CoordinationBarPlot.svelte +232 -176
  79. package/dist/element/BohrAtom.svelte +56 -13
  80. package/dist/element/ElementHeading.svelte +7 -2
  81. package/dist/element/ElementPhoto.svelte +15 -9
  82. package/dist/element/ElementStats.svelte +10 -4
  83. package/dist/element/ElementTile.svelte +137 -73
  84. package/dist/element/Nucleus.svelte +39 -11
  85. package/dist/element/data.js +2 -14
  86. package/dist/element/data.json.gz +0 -0
  87. package/dist/element/types.d.ts +1 -0
  88. package/dist/feedback/ClickFeedback.svelte +16 -5
  89. package/dist/feedback/DragOverlay.svelte +10 -2
  90. package/dist/feedback/Spinner.svelte +4 -2
  91. package/dist/feedback/StatusMessage.svelte +8 -2
  92. package/dist/fermi-surface/FermiSlice.svelte +118 -88
  93. package/dist/fermi-surface/FermiSurface.svelte +336 -239
  94. package/dist/fermi-surface/FermiSurface.svelte.d.ts +1 -1
  95. package/dist/fermi-surface/FermiSurfaceControls.svelte +113 -46
  96. package/dist/fermi-surface/FermiSurfaceScene.svelte +536 -343
  97. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  98. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +14 -5
  99. package/dist/fermi-surface/compute.js +16 -20
  100. package/dist/fermi-surface/parse.js +37 -33
  101. package/dist/fermi-surface/symmetry.js +2 -7
  102. package/dist/fermi-surface/types.d.ts +3 -5
  103. package/dist/heatmap-matrix/HeatmapMatrix.svelte +1527 -0
  104. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +110 -0
  105. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +225 -0
  106. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +30 -0
  107. package/dist/heatmap-matrix/index.d.ts +53 -0
  108. package/dist/heatmap-matrix/index.js +100 -0
  109. package/dist/heatmap-matrix/shared.d.ts +2 -0
  110. package/dist/heatmap-matrix/shared.js +4 -0
  111. package/dist/icons.d.ts +111 -0
  112. package/dist/icons.js +158 -0
  113. package/dist/index.d.ts +5 -2
  114. package/dist/index.js +5 -2
  115. package/dist/io/decompress.js +1 -1
  116. package/dist/io/export.d.ts +3 -0
  117. package/dist/io/export.js +138 -140
  118. package/dist/io/file-drop.d.ts +7 -0
  119. package/dist/io/file-drop.js +43 -0
  120. package/dist/io/index.d.ts +2 -2
  121. package/dist/io/index.js +2 -112
  122. package/dist/io/is-binary.js +2 -3
  123. package/dist/io/types.d.ts +1 -0
  124. package/dist/io/url-drop.d.ts +2 -0
  125. package/dist/io/url-drop.js +117 -0
  126. package/dist/isosurface/Isosurface.svelte +220 -110
  127. package/dist/isosurface/IsosurfaceControls.svelte +65 -28
  128. package/dist/isosurface/parse.js +104 -56
  129. package/dist/isosurface/slice.d.ts +2 -1
  130. package/dist/isosurface/slice.js +8 -13
  131. package/dist/isosurface/types.d.ts +14 -1
  132. package/dist/isosurface/types.js +152 -5
  133. package/dist/labels.d.ts +2 -1
  134. package/dist/labels.js +12 -8
  135. package/dist/layout/FullscreenToggle.svelte +11 -2
  136. package/dist/layout/InfoCard.svelte +38 -6
  137. package/dist/layout/InfoTag.svelte +125 -94
  138. package/dist/layout/PropertyFilter.svelte +82 -37
  139. package/dist/layout/SettingsSection.svelte +85 -55
  140. package/dist/layout/SubpageGrid.svelte +82 -0
  141. package/dist/layout/SubpageGrid.svelte.d.ts +14 -0
  142. package/dist/layout/index.d.ts +1 -0
  143. package/dist/layout/index.js +1 -0
  144. package/dist/layout/json-tree/JsonNode.svelte +266 -223
  145. package/dist/layout/json-tree/JsonTree.svelte +516 -429
  146. package/dist/layout/json-tree/JsonTree.svelte.d.ts +1 -1
  147. package/dist/layout/json-tree/JsonValue.svelte +281 -173
  148. package/dist/layout/json-tree/types.d.ts +10 -2
  149. package/dist/layout/json-tree/utils.d.ts +2 -0
  150. package/dist/layout/json-tree/utils.js +37 -2
  151. package/dist/marching-cubes.js +25 -2
  152. package/dist/math.d.ts +20 -17
  153. package/dist/math.js +474 -57
  154. package/dist/overlays/ContextMenu.svelte +66 -40
  155. package/dist/overlays/DraggablePane.svelte +331 -154
  156. package/dist/overlays/DraggablePane.svelte.d.ts +2 -0
  157. package/dist/periodic-table/PeriodicTable.svelte +278 -145
  158. package/dist/periodic-table/PeriodicTableControls.svelte +178 -128
  159. package/dist/periodic-table/PropertySelect.svelte +25 -7
  160. package/dist/periodic-table/TableInset.svelte +8 -3
  161. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +559 -267
  162. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -2
  163. package/dist/phase-diagram/PhaseDiagramControls.svelte +131 -51
  164. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +3 -2
  165. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +126 -0
  166. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +15 -0
  167. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +160 -110
  168. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +8 -1
  169. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +217 -86
  170. package/dist/phase-diagram/PhaseDiagramTooltip.svelte.d.ts +6 -3
  171. package/dist/phase-diagram/TdbInfoPanel.svelte +28 -4
  172. package/dist/phase-diagram/build-diagram.js +9 -9
  173. package/dist/phase-diagram/colors.js +1 -3
  174. package/dist/phase-diagram/index.d.ts +2 -0
  175. package/dist/phase-diagram/index.js +2 -0
  176. package/dist/phase-diagram/parse.js +10 -9
  177. package/dist/phase-diagram/svg-to-diagram.d.ts +2 -0
  178. package/dist/phase-diagram/svg-to-diagram.js +869 -0
  179. package/dist/phase-diagram/types.d.ts +10 -0
  180. package/dist/phase-diagram/utils.d.ts +8 -4
  181. package/dist/phase-diagram/utils.js +219 -74
  182. package/dist/plot/AxisLabel.svelte +51 -0
  183. package/dist/plot/AxisLabel.svelte.d.ts +16 -0
  184. package/dist/plot/BarPlot.svelte +1461 -768
  185. package/dist/plot/BarPlot.svelte.d.ts +3 -3
  186. package/dist/plot/BarPlotControls.svelte +33 -6
  187. package/dist/plot/BarPlotControls.svelte.d.ts +1 -1
  188. package/dist/plot/ColorBar.svelte +533 -383
  189. package/dist/plot/ColorBar.svelte.d.ts +1 -1
  190. package/dist/plot/ColorScaleSelect.svelte +28 -7
  191. package/dist/plot/ElementScatter.svelte +38 -16
  192. package/dist/plot/FillArea.svelte +152 -92
  193. package/dist/plot/Histogram.svelte +1162 -709
  194. package/dist/plot/Histogram.svelte.d.ts +1 -1
  195. package/dist/plot/HistogramControls.svelte +81 -18
  196. package/dist/plot/HistogramControls.svelte.d.ts +6 -2
  197. package/dist/plot/InteractiveAxisLabel.svelte +34 -11
  198. package/dist/plot/InteractiveAxisLabel.svelte.d.ts +1 -1
  199. package/dist/plot/Line.svelte +63 -28
  200. package/dist/plot/PlotControls.svelte +221 -96
  201. package/dist/plot/PlotControls.svelte.d.ts +1 -1
  202. package/dist/plot/PlotLegend.svelte +174 -91
  203. package/dist/plot/PlotTooltip.svelte +45 -6
  204. package/dist/plot/PortalSelect.svelte +175 -146
  205. package/dist/plot/ReferenceLine.svelte +77 -22
  206. package/dist/plot/ReferenceLine.svelte.d.ts +1 -0
  207. package/dist/plot/ReferenceLine3D.svelte +132 -107
  208. package/dist/plot/ReferencePlane.svelte +146 -123
  209. package/dist/plot/ScatterPlot.svelte +1880 -1156
  210. package/dist/plot/ScatterPlot.svelte.d.ts +3 -3
  211. package/dist/plot/ScatterPlot3D.svelte +256 -131
  212. package/dist/plot/ScatterPlot3D.svelte.d.ts +2 -2
  213. package/dist/plot/ScatterPlot3DControls.svelte +300 -297
  214. package/dist/plot/ScatterPlot3DControls.svelte.d.ts +2 -1
  215. package/dist/plot/ScatterPlot3DScene.svelte +608 -406
  216. package/dist/plot/ScatterPlot3DScene.svelte.d.ts +2 -2
  217. package/dist/plot/ScatterPlotControls.svelte +150 -70
  218. package/dist/plot/ScatterPlotControls.svelte.d.ts +1 -1
  219. package/dist/plot/ScatterPoint.svelte +98 -26
  220. package/dist/plot/ScatterPoint.svelte.d.ts +1 -0
  221. package/dist/plot/SpacegroupBarPlot.svelte +142 -85
  222. package/dist/plot/Surface3D.svelte +159 -108
  223. package/dist/plot/ZeroLines.svelte +96 -0
  224. package/dist/plot/ZeroLines.svelte.d.ts +32 -0
  225. package/dist/plot/ZoomRect.svelte +23 -0
  226. package/dist/plot/ZoomRect.svelte.d.ts +8 -0
  227. package/dist/plot/axis-utils.d.ts +1 -1
  228. package/dist/plot/axis-utils.js +1 -3
  229. package/dist/plot/data-cleaning.js +12 -28
  230. package/dist/plot/data-transform.js +2 -1
  231. package/dist/plot/fill-utils.js +2 -0
  232. package/dist/plot/index.d.ts +6 -2
  233. package/dist/plot/index.js +6 -2
  234. package/dist/plot/interactions.d.ts +8 -10
  235. package/dist/plot/interactions.js +2 -3
  236. package/dist/plot/layout.d.ts +11 -2
  237. package/dist/plot/layout.js +44 -17
  238. package/dist/plot/reference-line.d.ts +5 -22
  239. package/dist/plot/reference-line.js +12 -84
  240. package/dist/plot/scales.js +24 -36
  241. package/dist/plot/types.d.ts +53 -40
  242. package/dist/plot/types.js +12 -7
  243. package/dist/plot/utils/label-placement.d.ts +32 -15
  244. package/dist/plot/utils/label-placement.js +227 -63
  245. package/dist/plot/utils/series-visibility.js +2 -3
  246. package/dist/plot/utils.d.ts +1 -0
  247. package/dist/plot/utils.js +14 -0
  248. package/dist/rdf/RdfPlot.svelte +173 -132
  249. package/dist/rdf/calc-rdf.js +4 -5
  250. package/dist/sanitize.d.ts +4 -0
  251. package/dist/sanitize.js +107 -0
  252. package/dist/settings.d.ts +21 -6
  253. package/dist/settings.js +63 -19
  254. package/dist/spectral/Bands.svelte +963 -412
  255. package/dist/spectral/Bands.svelte.d.ts +22 -2
  256. package/dist/spectral/BandsAndDos.svelte +90 -49
  257. package/dist/spectral/BrillouinBandsDos.svelte +151 -93
  258. package/dist/spectral/Dos.svelte +389 -258
  259. package/dist/spectral/helpers.d.ts +23 -1
  260. package/dist/spectral/helpers.js +119 -51
  261. package/dist/spectral/types.d.ts +2 -0
  262. package/dist/state.svelte.d.ts +1 -1
  263. package/dist/state.svelte.js +3 -2
  264. package/dist/structure/Arrow.svelte +59 -20
  265. package/dist/structure/AtomLegend.svelte +231 -129
  266. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  267. package/dist/structure/Bond.svelte +73 -47
  268. package/dist/structure/CanvasTooltip.svelte +10 -2
  269. package/dist/structure/CellSelect.svelte +148 -51
  270. package/dist/structure/Cylinder.svelte +33 -17
  271. package/dist/structure/Lattice.svelte +88 -33
  272. package/dist/structure/Structure.svelte +1077 -821
  273. package/dist/structure/Structure.svelte.d.ts +1 -1
  274. package/dist/structure/StructureControls.svelte +373 -139
  275. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  276. package/dist/structure/StructureExportPane.svelte +124 -89
  277. package/dist/structure/StructureExportPane.svelte.d.ts +1 -1
  278. package/dist/structure/StructureInfoPane.svelte +304 -231
  279. package/dist/structure/StructureScene.svelte +919 -445
  280. package/dist/structure/StructureScene.svelte.d.ts +16 -7
  281. package/dist/structure/atom-properties.d.ts +6 -2
  282. package/dist/structure/atom-properties.js +42 -29
  283. package/dist/structure/bonding.js +6 -7
  284. package/dist/structure/export.js +22 -34
  285. package/dist/structure/ferrox-wasm-types.d.ts +3 -2
  286. package/dist/structure/ferrox-wasm-types.js +0 -3
  287. package/dist/structure/ferrox-wasm.d.ts +3 -2
  288. package/dist/structure/ferrox-wasm.js +2 -3
  289. package/dist/structure/index.d.ts +16 -0
  290. package/dist/structure/index.js +88 -6
  291. package/dist/structure/measure.d.ts +2 -2
  292. package/dist/structure/measure.js +4 -44
  293. package/dist/structure/parse.js +130 -155
  294. package/dist/structure/partial-occupancy.d.ts +25 -0
  295. package/dist/structure/partial-occupancy.js +99 -0
  296. package/dist/structure/pbc.d.ts +1 -0
  297. package/dist/structure/pbc.js +16 -6
  298. package/dist/structure/supercell.d.ts +2 -2
  299. package/dist/structure/supercell.js +12 -22
  300. package/dist/structure/validation.js +5 -3
  301. package/dist/symmetry/SymmetryStats.svelte +94 -37
  302. package/dist/symmetry/WyckoffTable.svelte +42 -14
  303. package/dist/symmetry/cell-transform.js +5 -3
  304. package/dist/symmetry/index.d.ts +7 -4
  305. package/dist/symmetry/index.js +87 -21
  306. package/dist/symmetry/spacegroups.js +148 -148
  307. package/dist/table/HeatmapTable.svelte +1112 -516
  308. package/dist/table/HeatmapTable.svelte.d.ts +12 -1
  309. package/dist/table/ToggleMenu.svelte +125 -90
  310. package/dist/table/index.d.ts +2 -0
  311. package/dist/table/index.js +2 -4
  312. package/dist/theme/ThemeControl.svelte +21 -12
  313. package/dist/time.js +4 -1
  314. package/dist/tooltip/TooltipContent.svelte +33 -8
  315. package/dist/trajectory/Trajectory.svelte +889 -687
  316. package/dist/trajectory/TrajectoryError.svelte +14 -3
  317. package/dist/trajectory/TrajectoryExportPane.svelte +148 -90
  318. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +1 -1
  319. package/dist/trajectory/TrajectoryInfoPane.svelte +272 -143
  320. package/dist/trajectory/constants.d.ts +6 -0
  321. package/dist/trajectory/constants.js +7 -0
  322. package/dist/trajectory/extract.js +13 -31
  323. package/dist/trajectory/format-detect.d.ts +9 -0
  324. package/dist/trajectory/format-detect.js +76 -0
  325. package/dist/trajectory/frame-reader.d.ts +17 -0
  326. package/dist/trajectory/frame-reader.js +332 -0
  327. package/dist/trajectory/helpers.d.ts +14 -0
  328. package/dist/trajectory/helpers.js +172 -0
  329. package/dist/trajectory/index.d.ts +1 -0
  330. package/dist/trajectory/index.js +23 -14
  331. package/dist/trajectory/parse/ase.d.ts +2 -0
  332. package/dist/trajectory/parse/ase.js +77 -0
  333. package/dist/trajectory/parse/hdf5.d.ts +2 -0
  334. package/dist/trajectory/parse/hdf5.js +129 -0
  335. package/dist/trajectory/parse/index.d.ts +12 -0
  336. package/dist/trajectory/parse/index.js +299 -0
  337. package/dist/trajectory/parse/lammps.d.ts +5 -0
  338. package/dist/trajectory/parse/lammps.js +179 -0
  339. package/dist/trajectory/parse/vasp.d.ts +2 -0
  340. package/dist/trajectory/parse/vasp.js +68 -0
  341. package/dist/trajectory/parse/xyz.d.ts +2 -0
  342. package/dist/trajectory/parse/xyz.js +110 -0
  343. package/dist/trajectory/plotting.js +13 -8
  344. package/dist/trajectory/types.d.ts +11 -0
  345. package/dist/trajectory/types.js +1 -0
  346. package/dist/utils.d.ts +3 -0
  347. package/dist/utils.js +17 -0
  348. package/dist/xrd/XrdPlot.svelte +337 -245
  349. package/dist/xrd/broadening.js +14 -9
  350. package/dist/xrd/calc-xrd.js +12 -19
  351. package/dist/xrd/parse.d.ts +1 -1
  352. package/dist/xrd/parse.js +17 -17
  353. package/package.json +103 -101
  354. package/readme.md +4 -4
  355. package/dist/trajectory/parse.d.ts +0 -42
  356. package/dist/trajectory/parse.js +0 -1267
  357. /package/dist/element/{data.json.d.ts → data.json.gz.d.ts} +0 -0
  358. /package/dist/theme/{themes.js → themes.mjs} +0 -0
@@ -1,449 +1,907 @@
1
- <script lang="ts">import { AXIS_COLORS, NEG_AXIS_COLORS } from '../colors';
2
- import { element_data } from '../element';
3
- import Isosurface from '../isosurface/Isosurface.svelte';
4
- import { DEFAULT_ISOSURFACE_SETTINGS } from '../isosurface/types';
5
- import { format_num } from '../labels';
6
- import * as math from '../math';
7
- import { DEFAULTS } from '../settings';
8
- import { colors } from '../state.svelte';
9
- import { Arrow, atomic_radii, Cylinder, get_center_of_mass, Lattice, } from './';
10
- import { get_orig_site_idx, get_property_colors, } from './atom-properties';
11
- import * as measure from './measure';
12
- import { T, useThrelte } from '@threlte/core';
13
- import * as extras from '@threlte/extras';
14
- import { untrack } from 'svelte';
15
- import { SvelteMap, SvelteSet } from 'svelte/reactivity';
16
- import { Color } from 'three';
17
- import Bond from './Bond.svelte';
18
- import { BONDING_STRATEGIES, compute_bond_transform } from './bonding';
19
- import { CanvasTooltip } from './index';
20
- let pulse_time = $state(0);
21
- let pulse_opacity = $derived(0.15 + 0.25 * Math.sin(pulse_time * 5));
22
- $effect(() => {
23
- if (!selected_sites?.length && !active_sites?.length)
24
- return;
25
- if (typeof globalThis === `undefined`)
26
- return;
27
- const reduce = globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches;
28
- if (reduce)
29
- return;
30
- let frame_id = 0;
1
+ <script lang="ts">
2
+ import type { D3InterpolateName } from '../colors'
3
+ import { AXIS_COLORS, get_d3_interpolator, NEG_AXIS_COLORS } from '../colors'
4
+ import type { ElementSymbol } from '../element'
5
+ import { element_data } from '../element'
6
+ import Isosurface from '../isosurface/Isosurface.svelte'
7
+ import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
8
+ import { DEFAULT_ISOSURFACE_SETTINGS } from '../isosurface/types'
9
+ import { format_num } from '../labels'
10
+ import type { Vec3 } from '../math'
11
+ import * as math from '../math'
12
+ import type {
13
+ CameraProjection,
14
+ ShowBonds,
15
+ VectorColorMode,
16
+ VectorLayerConfig,
17
+ } from '../settings'
18
+ import { DEFAULTS } from '../settings'
19
+ import { sanitize_html } from '../sanitize'
20
+ import { colors } from '../state.svelte'
21
+ import type { AnyStructure, BondPair, MeasureMode, Site } from './'
22
+ import {
23
+ Arrow,
24
+ atomic_radii,
25
+ Cylinder,
26
+ get_all_site_vectors,
27
+ get_center_of_mass,
28
+ get_structure_vector_keys,
29
+ Lattice,
30
+ VECTOR_PALETTE,
31
+ } from './'
32
+ import type { AtomColorConfig } from './atom-properties'
33
+ import {
34
+ get_orig_site_idx,
35
+ get_property_colors,
36
+ } from './atom-properties'
37
+ import * as measure from './measure'
38
+ import {
39
+ compute_slice_geometry,
40
+ merge_split_partial_sites,
41
+ PARTIAL_OCCUPANCY_CAP_ARC,
42
+ } from './partial-occupancy'
43
+ import type { MoyoDataset } from '@spglib/moyo-wasm'
44
+ import { T, useThrelte } from '@threlte/core'
45
+ import * as extras from '@threlte/extras'
46
+ import { type ComponentProps, type Snippet, untrack } from 'svelte'
47
+ import { SvelteMap, SvelteSet } from 'svelte/reactivity'
48
+ import { type Camera, Color, type Mesh, type Scene } from 'three'
49
+ import Bond from './Bond.svelte'
50
+ import type { BondingStrategy } from './bonding'
51
+ import { BONDING_STRATEGIES, compute_bond_transform } from './bonding'
52
+ import { CanvasTooltip } from './index'
53
+
54
+ type InstancedAtomGroup = {
55
+ element: string
56
+ radius: number
57
+ color: string
58
+ is_image_atom: boolean
59
+ atoms: (typeof atom_data)[number][]
60
+ }
61
+
62
+ let pulse_time = $state(0)
63
+ let pulse_opacity = $derived(0.15 + 0.25 * Math.sin(pulse_time * 5))
64
+ $effect(() => {
65
+ if (!selected_sites?.length && !active_sites?.length) return
66
+ if (typeof globalThis === `undefined`) return
67
+ const reduce = globalThis.matchMedia?.(`(prefers-reduced-motion: reduce)`).matches
68
+ if (reduce) return
69
+ let frame_id = 0
31
70
  const animate = () => {
32
- pulse_time += 0.015;
33
- frame_id = requestAnimationFrame(animate);
34
- };
35
- frame_id = requestAnimationFrame(animate);
36
- return () => cancelAnimationFrame(frame_id);
37
- });
38
- let { structure = undefined, base_structure = undefined, atom_radius = DEFAULTS.structure.atom_radius, same_size_atoms = false, camera_position = DEFAULTS.structure.camera_position, camera_projection = DEFAULTS.structure.camera_projection, rotation_damping = DEFAULTS.structure.rotation_damping, max_zoom = DEFAULTS.structure.max_zoom, min_zoom = DEFAULTS.structure.min_zoom, rotate_speed = DEFAULTS.structure.rotate_speed, zoom_speed = DEFAULTS.structure.zoom_speed, pan_speed = DEFAULTS.structure.pan_speed, zoom_to_cursor = DEFAULTS.structure.zoom_to_cursor, show_atoms = DEFAULTS.structure.show_atoms, show_bonds = DEFAULTS.structure.show_bonds, show_site_labels = DEFAULTS.structure.show_site_labels, show_site_indices = DEFAULTS.structure.show_site_indices, site_label_size = DEFAULTS.structure.site_label_size, site_label_offset = $bindable(DEFAULTS.structure.site_label_offset), site_label_bg_color = `color-mix(in srgb, #000000 0%, transparent)`, site_label_color = `#ffffff`, site_label_padding = 3, show_force_vectors = DEFAULTS.structure.show_force_vectors, force_scale = DEFAULTS.structure.force_scale, force_color = DEFAULTS.structure.force_color, gizmo = DEFAULTS.structure.show_gizmo, hovered_idx = $bindable(null), hovered_site = $bindable(null), float_fmt = `.3~f`, auto_rotate = DEFAULTS.structure.auto_rotate, bond_thickness = DEFAULTS.structure.bond_thickness, bond_color = DEFAULTS.structure.bond_color, bonding_strategy = DEFAULTS.structure.bonding_strategy, bonding_options = {}, fov = DEFAULTS.structure.fov, initial_zoom = DEFAULTS.structure.initial_zoom, ambient_light = DEFAULTS.structure.ambient_light, directional_light = DEFAULTS.structure.directional_light, sphere_segments = DEFAULTS.structure.sphere_segments, lattice_props = {}, atom_label, camera_is_moving = $bindable(false), width = 0, height = 0, measure_mode = `distance`, selected_sites = $bindable([]), measured_sites = $bindable([]), added_bonds = $bindable([]), removed_bonds = $bindable([]), selection_highlight_color = `#6cf0ff`,
39
- // Active highlight group with different color
40
- active_sites = $bindable([]), active_highlight_color = `var(--struct-active-highlight-color, #2563eb)`, rotation = DEFAULTS.structure.rotation, scene = $bindable(), camera = $bindable(), orbit_controls = $bindable(), rotation_target_ref = $bindable(), initial_computed_zoom = $bindable(), hidden_elements = $bindable(new SvelteSet()), hidden_prop_vals = $bindable(new SvelteSet()), element_radius_overrides = $bindable({}), site_radius_overrides = $bindable(new SvelteMap()), atom_color_config = {
41
- mode: DEFAULTS.structure.atom_color_mode,
42
- scale: DEFAULTS.structure.atom_color_scale,
43
- scale_type: DEFAULTS.structure.atom_color_scale_type,
44
- }, sym_data = null,
45
- // Edit-atoms mode callbacks
46
- on_sites_moved, on_operation_start, on_add_atom, add_atom_mode = $bindable(false), add_element = $bindable(`C`), cursor = $bindable(`default`), dragging_atoms = $bindable(false), volumetric_data = undefined, isosurface_settings = DEFAULT_ISOSURFACE_SETTINGS, } = $props();
47
- const threlte = useThrelte();
48
- $effect(() => {
49
- scene = threlte.scene;
50
- camera = threlte.camera.current;
71
+ pulse_time += 0.015
72
+ frame_id = requestAnimationFrame(animate)
73
+ }
74
+ frame_id = requestAnimationFrame(animate)
75
+ return () => cancelAnimationFrame(frame_id)
76
+ })
77
+
78
+ let {
79
+ structure = undefined,
80
+ base_structure = undefined,
81
+ atom_radius = DEFAULTS.structure.atom_radius,
82
+ same_size_atoms = false,
83
+ camera_position = DEFAULTS.structure.camera_position,
84
+ camera_target = undefined,
85
+ camera_projection = DEFAULTS.structure.camera_projection,
86
+ rotation_damping = DEFAULTS.structure.rotation_damping,
87
+ max_zoom = DEFAULTS.structure.max_zoom,
88
+ min_zoom = DEFAULTS.structure.min_zoom,
89
+ rotate_speed = DEFAULTS.structure.rotate_speed,
90
+ zoom_speed = DEFAULTS.structure.zoom_speed,
91
+ pan_speed = DEFAULTS.structure.pan_speed,
92
+ zoom_to_cursor = DEFAULTS.structure.zoom_to_cursor,
93
+ show_atoms = DEFAULTS.structure.show_atoms,
94
+ show_bonds = DEFAULTS.structure.show_bonds,
95
+ show_site_labels = DEFAULTS.structure.show_site_labels,
96
+ show_site_indices = DEFAULTS.structure.show_site_indices,
97
+ site_label_size = DEFAULTS.structure.site_label_size,
98
+ site_label_offset = $bindable(DEFAULTS.structure.site_label_offset),
99
+ site_label_bg_color = `color-mix(in srgb, #000000 0%, transparent)`,
100
+ site_label_color = `#ffffff`,
101
+ site_label_padding = 3,
102
+ vector_configs = $bindable<Record<string, VectorLayerConfig>>({}),
103
+ vector_scale = DEFAULTS.structure.vector_scale,
104
+ vector_color = DEFAULTS.structure.vector_color,
105
+ vector_color_mode = DEFAULTS.structure.vector_color_mode as VectorColorMode,
106
+ vector_color_scale = DEFAULTS.structure.vector_color_scale,
107
+ vector_normalize = DEFAULTS.structure.vector_normalize,
108
+ vector_uniform_thickness = DEFAULTS.structure.vector_uniform_thickness,
109
+ vector_origin_gap = DEFAULTS.structure.vector_origin_gap,
110
+ vector_shaft_radius = DEFAULTS.structure.vector_shaft_radius,
111
+ vector_arrow_head_radius = DEFAULTS.structure.vector_arrow_head_radius,
112
+ vector_arrow_head_length = DEFAULTS.structure.vector_arrow_head_length,
113
+ gizmo = DEFAULTS.structure.show_gizmo,
114
+ hovered_idx = $bindable(null),
115
+ hovered_site = $bindable(null),
116
+ float_fmt = `.3~f`,
117
+ auto_rotate = DEFAULTS.structure.auto_rotate,
118
+ bond_thickness = DEFAULTS.structure.bond_thickness,
119
+ bond_color = DEFAULTS.structure.bond_color,
120
+ bonding_strategy = DEFAULTS.structure.bonding_strategy,
121
+ bonding_options = {},
122
+ fov = DEFAULTS.structure.fov,
123
+ initial_zoom = DEFAULTS.structure.initial_zoom,
124
+ ambient_light = DEFAULTS.structure.ambient_light,
125
+ directional_light = DEFAULTS.structure.directional_light,
126
+ sphere_segments = DEFAULTS.structure.sphere_segments,
127
+ lattice_props = {},
128
+ atom_label,
129
+ camera_is_moving = $bindable(false),
130
+ width = 0,
131
+ height = 0,
132
+ measure_mode = `distance`,
133
+ selected_sites = $bindable([]),
134
+ measured_sites = $bindable([]),
135
+ added_bonds = $bindable([]),
136
+ removed_bonds = $bindable([]),
137
+ selection_highlight_color = `#6cf0ff`,
138
+ // Active highlight group with different color
139
+ active_sites = $bindable([]),
140
+ active_highlight_color = `var(--struct-active-highlight-color, #2563eb)`,
141
+ rotation = DEFAULTS.structure.rotation,
142
+ scene = $bindable(),
143
+ camera = $bindable(),
144
+ orbit_controls = $bindable(),
145
+ rotation_target_ref = $bindable(),
146
+ initial_computed_zoom = $bindable(),
147
+ hidden_elements = $bindable(new SvelteSet()),
148
+ hidden_prop_vals = $bindable(new SvelteSet<number | string>()),
149
+ element_radius_overrides = $bindable<Partial<Record<ElementSymbol, number>>>({}),
150
+ site_radius_overrides = $bindable<SvelteMap<number, number>>(new SvelteMap()),
151
+ atom_color_config = {
152
+ mode: DEFAULTS.structure.atom_color_mode,
153
+ scale: DEFAULTS.structure.atom_color_scale as D3InterpolateName,
154
+ scale_type: DEFAULTS.structure.atom_color_scale_type,
155
+ },
156
+ sym_data = null,
157
+ // Edit-atoms mode callbacks
158
+ on_sites_moved,
159
+ on_operation_start,
160
+ on_add_atom,
161
+ add_atom_mode = $bindable(false),
162
+ add_element = $bindable(`C`),
163
+ cursor = $bindable(`default`),
164
+ dragging_atoms = $bindable(false),
165
+ volumetric_data = undefined,
166
+ isosurface_settings = DEFAULT_ISOSURFACE_SETTINGS,
167
+ }: {
168
+ structure?: AnyStructure
169
+ base_structure?: AnyStructure // The original structure without image atoms, used for property color calculation
170
+ atom_radius?: number // scale factor for atomic radii
171
+ same_size_atoms?: boolean // whether to use the same radius for all atoms. if not, the radius will be
172
+ // determined by the atomic radius of the element
173
+ camera_position?: [x: number, y: number, z: number] // initial camera position from which to render the scene
174
+ camera_target?: Vec3 // external orbit-controls target for pan synchronization
175
+ camera_projection?: CameraProjection // camera projection type
176
+ rotation_damping?: number // rotation damping factor (how quickly the rotation comes to rest after mouse release)
177
+ // zoom level of the camera
178
+ max_zoom?: number
179
+ min_zoom?: number
180
+ rotate_speed?: number // rotation speed. set to 0 to disable rotation.
181
+ zoom_speed?: number // zoom speed. set to 0 to disable zooming.
182
+ pan_speed?: number // pan speed. set to 0 to disable panning.
183
+ zoom_to_cursor?: boolean // zoom toward cursor position instead of scene center
184
+ show_atoms?: boolean
185
+ show_bonds?: ShowBonds
186
+ show_site_labels?: boolean
187
+ show_site_indices?: boolean
188
+ vector_configs?: Record<string, VectorLayerConfig>
189
+ vector_scale?: number
190
+ vector_color?: string
191
+ vector_color_mode?: VectorColorMode
192
+ vector_color_scale?: D3InterpolateName
193
+ vector_normalize?: boolean
194
+ vector_uniform_thickness?: boolean
195
+ vector_origin_gap?: number
196
+ vector_shaft_radius?: number
197
+ vector_arrow_head_radius?: number
198
+ vector_arrow_head_length?: number
199
+ gizmo?: boolean | ComponentProps<typeof extras.Gizmo>
200
+ hovered_idx?: number | null
201
+ hovered_site?: Site | null
202
+ float_fmt?: string
203
+ auto_rotate?: number
204
+ initial_zoom?: number
205
+ bond_thickness?: number
206
+ bond_color?: string
207
+ bonding_strategy?: BondingStrategy
208
+ bonding_options?: Record<string, unknown>
209
+ fov?: number
210
+ ambient_light?: number
211
+ directional_light?: number
212
+ sphere_segments?: number
213
+ lattice_props?: ComponentProps<typeof Lattice>
214
+ atom_label?: Snippet<[{ site: Site; site_idx: number }]>
215
+ site_label_size?: number
216
+ site_label_offset?: Vec3
217
+ site_label_bg_color?: string
218
+ site_label_color?: string
219
+ site_label_padding?: number
220
+ camera_is_moving?: boolean // used to prevent tooltip from showing while camera is moving
221
+ width?: number // Viewer dimensions for responsive zoom
222
+ height?: number
223
+ // measurement props
224
+ measure_mode?: MeasureMode
225
+ selected_sites?: number[]
226
+ measured_sites?: number[]
227
+ added_bonds?: [number, number][]
228
+ removed_bonds?: [number, number][]
229
+ selection_highlight_color?: string
230
+ // Support for active highlight group with different color
231
+ active_sites?: number[]
232
+ active_highlight_color?: string
233
+ rotation?: Vec3 // rotation control prop
234
+ // Expose scene and camera for external use (e.g. export pane)
235
+ scene?: Scene
236
+ camera?: Camera
237
+ orbit_controls?: ComponentProps<typeof extras.OrbitControls>[`ref`] // OrbitControls instance
238
+ rotation_target_ref?: Vec3 // Expose rotation target for reset
239
+ initial_computed_zoom?: number // Expose initial zoom for reset
240
+ hidden_elements?: Set<ElementSymbol>
241
+ hidden_prop_vals?: Set<number | string> // Track hidden property values (e.g. Wyckoff positions, coordination numbers)
242
+ element_radius_overrides?: Partial<Record<ElementSymbol, number>> // Per-element absolute radius in Angstroms
243
+ site_radius_overrides?: Map<number, number> | SvelteMap<number, number> // Per-site absolute radius in Angstroms
244
+ atom_color_config?: Partial<AtomColorConfig> // Atom coloring configuration
245
+ sym_data?: MoyoDataset | null // Symmetry data for Wyckoff coloring
246
+ // Edit-atoms mode callbacks and state
247
+ on_sites_moved?: (scene_indices: number[], delta: Vec3) => void
248
+ on_operation_start?: () => void
249
+ on_add_atom?: (xyz: Vec3, element: ElementSymbol) => void
250
+ add_atom_mode?: boolean // whether user is in click-to-place add-atom sub-mode
251
+ add_element?: ElementSymbol // element to add when clicking in add-atom mode
252
+ cursor?: string // cursor style for the 3D canvas
253
+ dragging_atoms?: boolean // true while TransformControls drag is active (skips expensive recalculations)
254
+ volumetric_data?: VolumetricData // Active volumetric data for isosurface rendering
255
+ isosurface_settings?: IsosurfaceSettings // Isosurface rendering settings
256
+ } = $props()
257
+
258
+ const threlte = useThrelte()
259
+ $effect(() => {
260
+ scene = threlte.scene
261
+ camera = threlte.camera.current
51
262
  if (threlte.renderer) {
52
- Object.assign(threlte.renderer.domElement, {
53
- __renderer: threlte.renderer,
54
- });
263
+ Object.assign(threlte.renderer.domElement, { __renderer: threlte.renderer })
55
264
  }
56
- });
57
- // Expose rotation target for external reset
58
- $effect(() => {
59
- rotation_target_ref = rotation_target;
60
- });
61
- // Track initial computed zoom for reset
62
- let stored_initial_zoom = $state(undefined);
63
- $effect(() => {
265
+ })
266
+
267
+ // Expose rotation target for external reset
268
+ $effect(() => {
269
+ rotation_target_ref = rotation_target
270
+ })
271
+
272
+ // Track initial computed zoom for reset
273
+ let stored_initial_zoom = $state<number | undefined>(undefined)
274
+ $effect(() => {
64
275
  if (stored_initial_zoom === undefined && computed_zoom > 0) {
65
- stored_initial_zoom = computed_zoom;
276
+ stored_initial_zoom = computed_zoom
66
277
  }
67
- initial_computed_zoom = stored_initial_zoom;
68
- });
69
- let bond_pairs = $state([]);
70
- let active_tooltip = $state(null);
71
- let hovered_bond_key = $state(null);
72
- // Cursor style for the canvas, derived from mode and hover state
73
- let canvas_cursor = $derived.by(() => {
74
- if (measure_mode === `edit-atoms` && add_atom_mode)
75
- return `crosshair`;
278
+ initial_computed_zoom = stored_initial_zoom
279
+ })
280
+
281
+ let bond_pairs: BondPair[] = $state([])
282
+ let active_tooltip = $state<`atom` | `bond` | null>(null)
283
+ let hovered_bond_key = $state<string | null>(null)
284
+
285
+ // Cursor style for the canvas, derived from mode and hover state
286
+ let canvas_cursor = $derived.by(() => {
287
+ if (measure_mode === `edit-atoms` && add_atom_mode) return `crosshair`
76
288
  if (hovered_idx != null) {
77
- if (measure_mode === `edit-atoms`) {
78
- const site = structure?.sites?.[hovered_idx];
79
- if (site?.properties?.orig_site_idx != null)
80
- return `not-allowed`;
81
- return `pointer`;
82
- }
83
- return `pointer`;
289
+ if (measure_mode === `edit-atoms`) {
290
+ const site = structure?.sites?.[hovered_idx]
291
+ if (site?.properties?.orig_site_idx != null) return `not-allowed`
292
+ return `pointer`
293
+ }
294
+ return `pointer`
84
295
  }
85
- return `default`;
86
- });
87
- // Desaturate a color by blending it toward gray (for ghosting image atoms in edit mode)
88
- const gray = new Color(0x999999);
89
- function desaturate(hex, amount = 0.4) {
90
- return `#${new Color(hex ?? 0x999999).lerp(gray, amount).getHexString()}`;
91
- }
92
- // === Edit-atoms mode state ===
93
- let transform_object = $state(undefined);
94
- // Plain variable only used imperatively in TransformControls drag handlers
95
- let drag_start_centroid = null;
96
- // Frozen centroid set on drag start. While non-null, the TransformControls mesh
97
- // position stays at this fixed value so Svelte's reactive centroid updates (from
98
- // PBC wrapping) don't fight TransformControls. Cleared on mouseUp so the mesh
99
- // snaps to the new wrapped centroid.
100
- let frozen_centroid = $state(null);
101
- function get_bond_key(idx1, idx2) {
102
- return idx1 < idx2 ? `${idx1}-${idx2}` : `${idx2}-${idx1}`;
103
- }
104
- // Toggle a bond between two atoms: cycles through add → remove → restore states
105
- function toggle_bond(site_1, site_2) {
106
- const idx_i = Math.min(site_1, site_2);
107
- const idx_j = Math.max(site_1, site_2);
296
+ return `default`
297
+ })
298
+
299
+ // Desaturate a color by blending it toward gray (for ghosting image atoms in edit mode)
300
+ const gray = new Color(0x999999)
301
+ function desaturate(hex: string | undefined, amount = 0.4): string {
302
+ return `#${new Color(hex ?? 0x999999).lerp(gray, amount).getHexString()}`
303
+ }
304
+
305
+ // === Edit-atoms mode state ===
306
+ let transform_object = $state<Mesh | undefined>(undefined)
307
+ // Plain variable only used imperatively in TransformControls drag handlers
308
+ let drag_start_centroid: Vec3 | null = null
309
+ // Frozen centroid set on drag start. While non-null, the TransformControls mesh
310
+ // position stays at this fixed value so Svelte's reactive centroid updates (from
311
+ // PBC wrapping) don't fight TransformControls. Cleared on mouseUp so the mesh
312
+ // snaps to the new wrapped centroid.
313
+ let frozen_centroid = $state<Vec3 | null>(null)
314
+
315
+ const get_bond_key = (idx1: number, idx2: number): string =>
316
+ idx1 < idx2 ? `${idx1}-${idx2}` : `${idx2}-${idx1}`
317
+
318
+ // Toggle a bond between two atoms: cycles through add → remove → restore states
319
+ function toggle_bond(site_1: number, site_2: number) {
320
+ const idx_i = Math.min(site_1, site_2)
321
+ const idx_j = Math.max(site_1, site_2)
108
322
  // added/removed pairs are stored sorted, so direct comparison works
109
- const match = ([a, b]) => a === idx_i && b === idx_j;
110
- const added_idx = added_bonds.findIndex(match);
323
+ const match = ([a, b]: [number, number]) => a === idx_i && b === idx_j
324
+
325
+ const added_idx = added_bonds.findIndex(match)
111
326
  if (added_idx >= 0) {
112
- added_bonds = added_bonds.toSpliced(added_idx, 1);
113
- return;
327
+ added_bonds = added_bonds.toSpliced(added_idx, 1)
328
+ return
114
329
  }
115
- const removed_idx = removed_bonds.findIndex(match);
330
+
331
+ const removed_idx = removed_bonds.findIndex(match)
116
332
  if (removed_idx >= 0) {
117
- removed_bonds = removed_bonds.toSpliced(removed_idx, 1);
118
- return;
333
+ removed_bonds = removed_bonds.toSpliced(removed_idx, 1)
334
+ return
119
335
  }
336
+
120
337
  // bond_pairs may not be sorted, so use get_bond_key for comparison
121
- const key = `${idx_i}-${idx_j}`;
122
- if (bond_pairs.some((bond) => get_bond_key(bond.site_idx_1, bond.site_idx_2) === key)) {
123
- removed_bonds = [...removed_bonds, [idx_i, idx_j]];
124
- }
125
- else {
126
- added_bonds = [...added_bonds, [idx_i, idx_j]];
338
+ const key = `${idx_i}-${idx_j}`
339
+ if (
340
+ bond_pairs.some((bond) =>
341
+ get_bond_key(bond.site_idx_1, bond.site_idx_2) === key
342
+ )
343
+ ) removed_bonds = [...removed_bonds, [idx_i, idx_j]]
344
+ else added_bonds = [...added_bonds, [idx_i, idx_j]]
345
+ }
346
+
347
+ // Deduplicate clicks: when a highlight sphere and the underlying atom both
348
+ // intercept the same native click, only the first intersection should fire.
349
+ // All threlte intersection events from one click share the same nativeEvent ref.
350
+ let last_native_event: Event | null = null
351
+
352
+ function toggle_selection(site_index: number, evt?: Event) {
353
+ evt?.stopPropagation?.()
354
+ const native_event = (evt as Event & { nativeEvent?: unknown } | undefined)
355
+ ?.nativeEvent
356
+ if (native_event instanceof Event) {
357
+ if (native_event === last_native_event) return
358
+ last_native_event = native_event
127
359
  }
128
- }
129
- // Deduplicate clicks: when a highlight sphere and the underlying atom both
130
- // intercept the same native click, only the first intersection should fire.
131
- // All threlte intersection events from one click share the same nativeEvent ref.
132
- let last_native_event = null;
133
- function toggle_selection(site_index, evt) {
134
- evt?.stopPropagation?.();
135
- const native = evt?.nativeEvent;
136
- if (native && native === last_native_event)
137
- return;
138
- if (native)
139
- last_native_event = native;
360
+
140
361
  if (measure_mode === `edit-bonds`) {
141
- // In edit-bonds mode, select atoms to add/remove bonds between them
142
- const new_sites = measured_sites.includes(site_index)
143
- ? measured_sites.filter((idx) => idx !== site_index)
144
- : [...measured_sites, site_index];
145
- measured_sites = new_sites;
146
- selected_sites = new_sites;
147
- // When two atoms are selected, toggle the bond between them
148
- if (measured_sites.length === 2) {
149
- toggle_bond(measured_sites[0], measured_sites[1]);
150
- measured_sites = [];
151
- selected_sites = [];
152
- }
153
- return;
362
+ // In edit-bonds mode, select atoms to add/remove bonds between them
363
+ const new_sites = measured_sites.includes(site_index)
364
+ ? measured_sites.filter((idx) => idx !== site_index)
365
+ : [...measured_sites, site_index]
366
+
367
+ measured_sites = new_sites
368
+ selected_sites = new_sites
369
+
370
+ // When two atoms are selected, toggle the bond between them
371
+ if (measured_sites.length === 2) {
372
+ toggle_bond(measured_sites[0], measured_sites[1])
373
+ measured_sites = []
374
+ selected_sites = []
375
+ }
376
+ return
154
377
  }
378
+
155
379
  if (measure_mode === `edit-atoms`) {
156
- // Block image atoms (detected by orig_site_idx property from PBC)
157
- const site = structure?.sites?.[site_index];
158
- if (site?.properties?.orig_site_idx != null)
159
- return;
160
- const is_selected = selected_sites.includes(site_index);
161
- const is_shift = evt instanceof MouseEvent && evt.shiftKey;
162
- // In edit-atoms mode, selected_sites and measured_sites always stay in sync
163
- let new_sites;
164
- if (is_shift) {
165
- // Multi-select: toggle this site in/out of selection
166
- new_sites = is_selected
167
- ? selected_sites.filter((idx) => idx !== site_index)
168
- : [...selected_sites, site_index];
169
- }
170
- else {
171
- // Single-select: replace selection (or deselect if already selected)
172
- new_sites = is_selected ? [] : [site_index];
173
- }
174
- selected_sites = new_sites;
175
- measured_sites = new_sites;
176
- return;
380
+ // Block image atoms (detected by orig_site_idx property from PBC)
381
+ const site = structure?.sites?.[site_index]
382
+ if (site?.properties?.orig_site_idx != null) return
383
+
384
+ const is_selected = selected_sites.includes(site_index)
385
+ const is_shift = evt instanceof MouseEvent && evt.shiftKey
386
+
387
+ // In edit-atoms mode, selected_sites and measured_sites always stay in sync
388
+ let new_sites: number[]
389
+ if (is_shift) {
390
+ // Multi-select: toggle this site in/out of selection
391
+ new_sites = is_selected
392
+ ? selected_sites.filter((idx) => idx !== site_index)
393
+ : [...selected_sites, site_index]
394
+ } else {
395
+ // Single-select: replace selection (or deselect if already selected)
396
+ new_sites = is_selected ? [] : [site_index]
397
+ }
398
+ selected_sites = new_sites
399
+ measured_sites = new_sites
400
+ return
177
401
  }
178
- if (!measured_sites.includes(site_index) &&
179
- measured_sites.length >= measure.MAX_SELECTED_SITES) {
180
- console.warn(`Selection size limit reached (${measure.MAX_SELECTED_SITES}). Deselect some sites first.`);
181
- return;
402
+
403
+ if (
404
+ !measured_sites.includes(site_index) &&
405
+ measured_sites.length >= measure.MAX_SELECTED_SITES
406
+ ) {
407
+ console.warn(
408
+ `Selection size limit reached (${measure.MAX_SELECTED_SITES}). Deselect some sites first.`,
409
+ )
410
+ return
182
411
  }
412
+
183
413
  measured_sites = measured_sites.includes(site_index)
184
- ? measured_sites.filter((idx) => idx !== site_index)
185
- : [...measured_sites, site_index];
414
+ ? measured_sites.filter((idx) => idx !== site_index)
415
+ : [...measured_sites, site_index]
186
416
  selected_sites = selected_sites.includes(site_index)
187
- ? selected_sites.filter((idx) => idx !== site_index)
188
- : [...selected_sites, site_index];
189
- }
190
- $effect(() => {
191
- const count = structure?.sites?.length ?? 0;
417
+ ? selected_sites.filter((idx) => idx !== site_index)
418
+ : [...selected_sites, site_index]
419
+ }
420
+ $effect(() => {
421
+ const count = structure?.sites?.length ?? 0
192
422
  if (count <= 0) {
193
- measured_sites = [];
194
- return;
423
+ measured_sites = []
424
+ return
195
425
  }
196
426
  untrack(() => {
197
- measured_sites = measured_sites.filter((idx) => idx >= 0 && idx < count);
198
- });
199
- });
200
- $effect(() => {
201
- cursor = canvas_cursor;
202
- });
203
- extras.interactivity();
204
- $effect.pre(() => {
205
- hovered_site = structure?.sites?.[hovered_idx ?? -1] ?? null;
206
- });
207
- let lattice = $derived(structure && `lattice` in structure ? structure.lattice : null);
208
- let visual_lattice = $derived(base_structure && `lattice` in base_structure ? base_structure.lattice : lattice);
209
- let rotation_target = $derived(lattice
210
- ? math.scale(math.add(...lattice.matrix), 0.5)
211
- : structure
212
- ? get_center_of_mass(structure)
213
- : [0, 0, 0]);
214
- let structure_size = $derived(lattice ? (lattice.a + lattice.b + lattice.c) / 2 : 10);
215
- // Compute dynamic camera clipping planes based on structure size
216
- // This prevents z-fighting and disappearing objects when zooming in close on large supercells
217
- let camera_near = $derived(Math.max(0.01, structure_size * 0.01));
218
- let camera_far = $derived(Math.max(1000, structure_size * 100));
219
- // Using $state because this is mutated in an effect based on viewport/structure size
220
- let computed_zoom = $state(untrack(() => initial_zoom));
221
- $effect(() => {
222
- if (!(width > 0) || !(height > 0))
223
- return;
224
- const structure_max_dim = Math.max(1, untrack(() => structure_size));
225
- const viewer_min_dim = Math.min(width, height);
226
- const scale_factor = viewer_min_dim / (structure_max_dim * 50); // 50px per unit
227
- let new_zoom = initial_zoom * scale_factor;
228
- if (min_zoom && min_zoom > 0)
229
- new_zoom = Math.max(min_zoom, new_zoom);
230
- if (max_zoom && max_zoom > 0)
231
- new_zoom = Math.min(max_zoom, new_zoom);
232
- computed_zoom = new_zoom;
233
- });
234
- $effect.pre(() => {
235
- if (camera_position.every((v) => v === 0) && structure) {
236
- const distance = Math.max(1, structure_size) * (60 / fov);
237
- camera_position = [distance, distance * 0.3, distance * 0.8];
427
+ measured_sites = measured_sites.filter((idx) => idx >= 0 && idx < count)
428
+ })
429
+ })
430
+
431
+ $effect(() => {
432
+ cursor = canvas_cursor
433
+ })
434
+
435
+ extras.interactivity()
436
+ $effect.pre(() => {
437
+ hovered_site = structure?.sites?.[hovered_idx ?? -1] ?? null
438
+ })
439
+ let lattice = $derived(
440
+ structure && `lattice` in structure ? structure.lattice : null,
441
+ )
442
+
443
+ let visual_lattice = $derived(
444
+ base_structure && `lattice` in base_structure ? base_structure.lattice : lattice,
445
+ )
446
+
447
+ let rotation_target = $derived(
448
+ lattice
449
+ ? (math.scale(math.add(...lattice.matrix), 0.5) as Vec3)
450
+ : structure
451
+ ? get_center_of_mass(structure)
452
+ : [0, 0, 0] as Vec3,
453
+ )
454
+
455
+ let structure_size = $derived(
456
+ lattice ? (lattice.a + lattice.b + lattice.c) / 2 : 10,
457
+ )
458
+
459
+ // Characteristic inter-atomic spacing: cube root of volume per atom.
460
+ // Excludes PBC image atoms (orig_site_idx) so toggling image atoms doesn't affect arrow sizing.
461
+ let char_atom_spacing = $derived.by(() => {
462
+ if (!lattice || !structure?.sites?.length) return structure_size
463
+ const n_real = structure.sites.filter((site) =>
464
+ site.properties?.orig_site_idx == null
465
+ ).length
466
+ return n_real > 0 ? Math.cbrt(lattice.volume / n_real) : structure_size
467
+ })
468
+
469
+ // When uniform thickness is on, convert negative (length-relative) radii to
470
+ // positive (absolute) values scaled by inter-atomic spacing.
471
+ // Already-positive (absolute) values are preserved as-is.
472
+ let eff_shaft_radius = $derived(
473
+ vector_uniform_thickness && vector_shaft_radius < 0
474
+ ? char_atom_spacing * -vector_shaft_radius
475
+ : vector_shaft_radius,
476
+ )
477
+ let eff_head_radius = $derived(
478
+ vector_uniform_thickness && vector_arrow_head_radius < 0
479
+ ? char_atom_spacing * -vector_arrow_head_radius
480
+ : vector_arrow_head_radius,
481
+ )
482
+ let eff_head_length = $derived(
483
+ vector_uniform_thickness && vector_arrow_head_length < 0
484
+ ? char_atom_spacing * -vector_arrow_head_length
485
+ : vector_arrow_head_length,
486
+ )
487
+
488
+ // Compute dynamic camera clipping planes based on structure size
489
+ // This prevents z-fighting and disappearing objects when zooming in close on large supercells
490
+ let camera_near = $derived(Math.max(0.01, structure_size * 0.01))
491
+ let camera_far = $derived(Math.max(1000, structure_size * 100))
492
+
493
+ // Using $state because this is mutated in an effect based on viewport/structure size
494
+ let computed_zoom = $state(untrack(() => initial_zoom))
495
+ $effect(() => {
496
+ if (!(width > 0) || !(height > 0)) return
497
+ const structure_max_dim = Math.max(1, untrack(() => structure_size))
498
+ const viewer_min_dim = Math.min(width, height)
499
+ const scale_factor = viewer_min_dim / (structure_max_dim * 50) // 50px per unit
500
+ let new_zoom = initial_zoom * scale_factor
501
+ if (min_zoom && min_zoom > 0) new_zoom = Math.max(min_zoom, new_zoom)
502
+ if (max_zoom && max_zoom > 0) new_zoom = Math.min(max_zoom, new_zoom)
503
+ computed_zoom = new_zoom
504
+ })
505
+
506
+ $effect.pre(() => { // Simple initial camera auto-position: proportional to structure size and fov
507
+ if (camera_position.every((val) => val === 0) && structure) {
508
+ stored_initial_zoom = undefined
509
+ const distance = Math.max(1, structure_size) * (60 / fov)
510
+ camera_position = [distance, distance * 0.3, distance * 0.8]
238
511
  }
239
- });
240
- $effect(() => {
512
+ })
513
+ $effect(() => {
241
514
  if (structure && show_bonds !== `never`) {
242
- // Determine if we should show bonds based on the setting and structure type
243
- const should_show_bonds = show_bonds === `always` ||
244
- (show_bonds === `crystals` && lattice) ||
245
- (show_bonds === `molecules` && !lattice);
246
- if (should_show_bonds) {
247
- bond_pairs = BONDING_STRATEGIES[bonding_strategy](structure, bonding_options);
248
- }
249
- else
250
- bond_pairs = [];
251
- }
252
- else
253
- bond_pairs = [];
254
- });
255
- // Compute property-based colors when not using element coloring
256
- // Use base_structure (original unit cell) for color calculation
257
- let property_colors = $derived(get_property_colors(base_structure || structure, atom_color_config, bonding_strategy, sym_data));
258
- // Compute weighted average radius for a site based on species occupancies
259
- // Normalizes by total occupancy so vacancy-containing sites render at full size
260
- const calc_weighted_radius = (site) => {
261
- const total_occu = site.species.reduce((sum, { occu }) => sum + occu, 0);
515
+ // Determine if we should show bonds based on the setting and structure type
516
+ const should_show_bonds = show_bonds === `always` ||
517
+ (show_bonds === `crystals` && lattice) ||
518
+ (show_bonds === `molecules` && !lattice)
519
+
520
+ if (should_show_bonds) {
521
+ bond_pairs = BONDING_STRATEGIES[bonding_strategy](structure, bonding_options)
522
+ } else bond_pairs = []
523
+ } else bond_pairs = []
524
+ })
525
+
526
+ // Compute property-based colors when not using element coloring
527
+ // Use base_structure (original unit cell) for color calculation
528
+ let property_colors = $derived(
529
+ get_property_colors(
530
+ base_structure || structure,
531
+ atom_color_config,
532
+ bonding_strategy,
533
+ sym_data,
534
+ ),
535
+ )
536
+ // Compute weighted average radius for a site based on species occupancies
537
+ // Normalizes by total occupancy so vacancy-containing sites render at full size
538
+ const calc_weighted_radius = (site: Site): number => {
539
+ const total_occu = site.species.reduce((sum, { occu }) => sum + occu, 0)
262
540
  const weighted_sum = site.species.reduce((sum, { element, occu }) => {
263
- const override = element_radius_overrides?.[element];
264
- return sum + occu * (override ?? atomic_radii[element] ?? 1);
265
- }, 0);
266
- return total_occu > 0 ? weighted_sum / total_occu : 1;
267
- };
268
- let atom_data = $derived.by(() => {
269
- if (!show_atoms || !structure?.sites)
270
- return [];
271
- return structure.sites.flatMap((site, site_idx) => {
272
- const orig_idx = get_orig_site_idx(site, site_idx);
273
- // Skip sites with hidden property values
274
- const prop_val = property_colors?.values[orig_idx];
275
- if (prop_val !== undefined && hidden_prop_vals.has(prop_val))
276
- return [];
277
- // Calculate radius: same_size > site override > element override > default
278
- // All radii scale uniformly with atom_radius for consistent slider behavior
279
- const base_radius = same_size_atoms
280
- ? 1
281
- : site_radius_overrides?.get(site_idx) ?? calc_weighted_radius(site);
282
- const radius = base_radius * atom_radius;
283
- // Use property color if available (e.g. coordination number, Wyckoff position)
284
- // Otherwise, each species gets its own element color (important for disordered sites)
285
- const site_property_color = property_colors?.colors[orig_idx];
286
- // Detect image atoms by presence of orig_site_idx property (set by get_pbc_image_sites)
287
- const is_image_atom = site.properties?.orig_site_idx != null;
288
- let start_angle = 0;
289
- return site.species
290
- .filter(({ element }) => !hidden_elements.has(element))
291
- .map(({ element, occu }) => ({
292
- site_idx,
293
- element,
294
- occupancy: occu,
295
- position: site.xyz,
296
- radius,
297
- color: site_property_color ?? colors.element?.[element],
298
- has_partial_occupancy: occu < 1,
299
- start_phi: 2 * Math.PI * start_angle,
300
- end_phi: 2 * Math.PI * (start_angle += occu),
301
- is_image_atom,
302
- }));
303
- });
304
- });
305
- let filtered_bond_pairs = $derived.by(() => {
306
- if (!structure?.sites)
307
- return bond_pairs;
308
- const is_site_visible = (site_idx) => {
309
- const site = structure.sites[site_idx];
310
- const has_visible_element = site?.species.some(({ element }) => !hidden_elements.has(element));
311
- const orig_idx = get_orig_site_idx(site, site_idx);
312
- const prop_val = property_colors?.values[orig_idx];
313
- const prop_visible = prop_val === undefined ||
314
- !hidden_prop_vals.has(prop_val);
315
- return has_visible_element && prop_visible;
316
- };
541
+ const override = element_radius_overrides?.[element as ElementSymbol]
542
+ return sum + occu * (override ?? atomic_radii[element] ?? 1)
543
+ }, 0)
544
+ return total_occu > 0 ? weighted_sum / total_occu : 1
545
+ }
546
+
547
+ let atom_data = $derived.by(() => {
548
+ if (!show_atoms || !structure?.sites) return []
549
+ const render_sites = merge_split_partial_sites(structure.sites, hidden_elements)
550
+ return render_sites.flatMap(({ site_idx, site, is_image_atom }) => {
551
+ const orig_idx = get_orig_site_idx(site, site_idx)
552
+
553
+ // Skip sites with hidden property values
554
+ const prop_val = property_colors?.values[orig_idx]
555
+ if (prop_val !== undefined && hidden_prop_vals.has(prop_val)) return []
556
+
557
+ // Calculate radius: same_size > site override > element override > default
558
+ // All radii scale uniformly with atom_radius for consistent slider behavior
559
+ const base_radius = same_size_atoms
560
+ ? 1
561
+ : site_radius_overrides?.get(site_idx) ?? calc_weighted_radius(site)
562
+ const radius = base_radius * atom_radius
563
+
564
+ // Use property color if available (e.g. coordination number, Wyckoff position)
565
+ // Otherwise, each species gets its own element color (important for disordered sites)
566
+ const site_property_color = property_colors?.colors[orig_idx]
567
+
568
+ const visible_species = site.species.filter(({ element }) =>
569
+ !hidden_elements.has(element)
570
+ )
571
+ const slice_geometry = compute_slice_geometry(visible_species)
572
+ return slice_geometry.map((slice_data) => {
573
+ return {
574
+ site_idx,
575
+ element: slice_data.element,
576
+ occupancy: slice_data.occupancy,
577
+ position: site.xyz,
578
+ radius,
579
+ color: site_property_color ?? colors.element?.[slice_data.element],
580
+ has_partial_occupancy: slice_data.occupancy < 1,
581
+ start_phi: slice_data.start_phi,
582
+ end_phi: slice_data.end_phi,
583
+ phi_length: slice_data.phi_length,
584
+ render_start_cap: slice_data.render_start_cap,
585
+ render_end_cap: slice_data.render_end_cap,
586
+ is_image_atom,
587
+ }
588
+ })
589
+ })
590
+ })
591
+
592
+ // Shared visibility check: site has at least one non-hidden element and
593
+ // its property value (if any) isn't hidden. Used by both bond and vector filtering.
594
+ const is_site_visible = (site_idx: number): boolean => {
595
+ if (!structure?.sites) return false
596
+ const site = structure.sites[site_idx]
597
+ const has_visible_element = site?.species.some(({ element }) =>
598
+ !hidden_elements.has(element)
599
+ )
600
+ const orig_idx = get_orig_site_idx(site, site_idx)
601
+ const prop_val = property_colors?.values[orig_idx]
602
+ const prop_visible = prop_val === undefined ||
603
+ !hidden_prop_vals.has(prop_val)
604
+ return has_visible_element && prop_visible
605
+ }
606
+
607
+ let filtered_bond_pairs = $derived.by(() => {
608
+ if (!structure?.sites) return bond_pairs
609
+
317
610
  // Build set of removed bond keys for efficient lookup
318
- const removed_keys = new Set(removed_bonds.map(([idx_i, idx_j]) => get_bond_key(idx_i, idx_j)));
611
+ const removed_keys = new Set(
612
+ removed_bonds.map(([idx_i, idx_j]) => get_bond_key(idx_i, idx_j)),
613
+ )
614
+
319
615
  // Filter calculated bonds: exclude removed and hidden
320
616
  const calculated = bond_pairs.filter(({ site_idx_1, site_idx_2 }) => {
321
- if (removed_keys.has(get_bond_key(site_idx_1, site_idx_2)))
322
- return false;
323
- return is_site_visible(site_idx_1) && is_site_visible(site_idx_2);
324
- });
617
+ if (removed_keys.has(get_bond_key(site_idx_1, site_idx_2))) return false
618
+ return is_site_visible(site_idx_1) && is_site_visible(site_idx_2)
619
+ })
620
+
325
621
  // Create BondPair objects for manually added bonds
326
- const added = added_bonds
327
- .map(([idx_i, idx_j]) => {
328
- if (!is_site_visible(idx_i) || !is_site_visible(idx_j))
329
- return null;
330
- const site1 = structure.sites[idx_i];
331
- const site2 = structure.sites[idx_j];
332
- if (!site1 || !site2)
333
- return null;
334
- const pos_1 = site1.xyz;
335
- const pos_2 = site2.xyz;
336
- const dist = math.euclidean_dist(pos_1, pos_2);
622
+ const added: BondPair[] = added_bonds
623
+ .map(([idx_i, idx_j]) => {
624
+ if (!is_site_visible(idx_i) || !is_site_visible(idx_j)) return null
625
+ const site1 = structure.sites[idx_i]
626
+ const site2 = structure.sites[idx_j]
627
+ if (!site1 || !site2) return null
628
+
629
+ const pos_1 = site1.xyz
630
+ const pos_2 = site2.xyz
631
+ const dist = math.euclidean_dist(pos_1, pos_2)
632
+
337
633
  return {
338
- pos_1,
339
- pos_2,
340
- site_idx_1: idx_i,
341
- site_idx_2: idx_j,
342
- bond_length: dist,
343
- strength: 1.0,
344
- transform_matrix: compute_bond_transform(pos_1, pos_2),
345
- };
346
- })
347
- .filter((bond) => bond !== null);
348
- return [...calculated, ...added];
349
- });
350
- let instanced_bond_groups = $derived.by(() => {
351
- if (!structure?.sites || filtered_bond_pairs.length === 0)
352
- return [];
634
+ pos_1,
635
+ pos_2,
636
+ site_idx_1: idx_i,
637
+ site_idx_2: idx_j,
638
+ bond_length: dist,
639
+ strength: 1.0,
640
+ transform_matrix: compute_bond_transform(pos_1, pos_2),
641
+ }
642
+ })
643
+ .filter((bond): bond is BondPair => bond !== null)
644
+
645
+ return [...calculated, ...added]
646
+ })
647
+
648
+ let instanced_bond_groups = $derived.by(() => {
649
+ if (!structure?.sites || filtered_bond_pairs.length === 0) return []
650
+
353
651
  const group = {
354
- thickness: bond_thickness,
355
- ambient_light,
356
- directional_light,
357
- instances: [],
358
- };
652
+ thickness: bond_thickness,
653
+ ambient_light,
654
+ directional_light,
655
+ instances: [] as {
656
+ matrix: Float32Array
657
+ color_start: string
658
+ color_end: string
659
+ }[],
660
+ }
661
+
359
662
  for (const bond_data of filtered_bond_pairs) {
360
- const site_a = structure.sites[bond_data.site_idx_1];
361
- const site_b = structure.sites[bond_data.site_idx_2];
362
- const get_majority_color = (site) => {
363
- if (!site?.species || site.species.length === 0)
364
- return bond_color;
365
- const majority_species = site.species.reduce((max, spec) => spec.occu > max.occu ? spec : max);
366
- return colors.element?.[majority_species.element] || bond_color;
367
- };
368
- const color_start = get_majority_color(site_a);
369
- const color_end = get_majority_color(site_b);
370
- const instance = { matrix: bond_data.transform_matrix, color_start, color_end };
371
- group.instances.push(instance);
663
+ const site_a = structure.sites[bond_data.site_idx_1]
664
+ const site_b = structure.sites[bond_data.site_idx_2]
665
+
666
+ const get_majority_color = (site: typeof site_a) => {
667
+ if (!site?.species || site.species.length === 0) return bond_color
668
+ const majority_species = site.species.reduce((max, spec) =>
669
+ spec.occu > max.occu ? spec : max
670
+ )
671
+ return colors.element?.[majority_species.element] || bond_color
672
+ }
673
+
674
+ const color_start = get_majority_color(site_a)
675
+ const color_end = get_majority_color(site_b)
676
+ const instance = { matrix: bond_data.transform_matrix, color_start, color_end }
677
+ group.instances.push(instance)
372
678
  }
373
- return group.instances.length > 0 ? [group] : [];
374
- });
375
- let radius_by_site_idx = $derived.by(() => {
376
- const map = new SvelteMap();
679
+
680
+ return group.instances.length > 0 ? [group] : []
681
+ })
682
+
683
+ let radius_by_site_idx = $derived.by(() => {
684
+ const map = new SvelteMap<number, number>()
377
685
  for (const atom of atom_data) {
378
- if (!map.has(atom.site_idx))
379
- map.set(atom.site_idx, atom.radius);
686
+ if (!map.has(atom.site_idx)) map.set(atom.site_idx, atom.radius)
380
687
  }
381
- return map;
382
- });
383
- // Get radius for a site (for highlight fallback when site is hidden/filtered)
384
- // Checks site_radius_overrides first for consistency with visible atoms
385
- const get_site_radius = (site, site_idx) => {
688
+ return map
689
+ })
690
+
691
+ // Get radius for a site (for highlight fallback when site is hidden/filtered)
692
+ // Checks site_radius_overrides first for consistency with visible atoms
693
+ const get_site_radius = (site: Site, site_idx: number | null): number => {
386
694
  const override = site_idx !== null
387
- ? site_radius_overrides?.get(site_idx)
388
- : undefined;
389
- const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site);
390
- return base_radius * atom_radius;
391
- };
392
- let force_data = $derived.by(() => show_force_vectors && structure?.sites
393
- ? structure?.sites
394
- .map((site) => {
395
- if (!site.properties?.force || !Array.isArray(site.properties.force))
396
- return null;
397
- const majority_element = site.species.reduce((max, spec) => spec.occu > max.occu ? spec : max).element;
398
- return {
399
- position: site.xyz,
400
- vector: site.properties.force,
401
- scale: force_scale,
402
- color: colors.element?.[majority_element] || force_color,
403
- };
695
+ ? site_radius_overrides?.get(site_idx)
696
+ : undefined
697
+ const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site)
698
+ return base_radius * atom_radius
699
+ }
700
+
701
+ // Interpolate between spin-down (#3498db blue) and spin-up (#e74c3c red)
702
+ // based on the z-component direction of a magnetic vector
703
+ function spin_direction_color(vec: Vec3): string {
704
+ const mag = Math.hypot(...vec)
705
+ const z_frac = mag > 1e-10 ? (vec[2] / mag + 1) / 2 : 0.5 // 0=down, 1=up
706
+ const red = Math.round(52 + (231 - 52) * z_frac)
707
+ const grn = Math.round(152 + (76 - 152) * z_frac)
708
+ const blu = Math.round(219 + (60 - 219) * z_frac)
709
+ return `#${red.toString(16).padStart(2, `0`)}${
710
+ grn.toString(16).padStart(2, `0`)
711
+ }${blu.toString(16).padStart(2, `0`)}`
712
+ }
713
+
714
+ // Build one arrow layer per visible vector key. Auto-scales the longest
715
+ // vector to 1.8× char_atom_spacing (cube root of volume per atom).
716
+ // When vector_normalize is on, effective_max is 1 so all arrows get equal length.
717
+ // Single active key preserves legacy coloring (element for force,
718
+ // spin-direction for magmom/spin). Multiple keys use flat palette colors.
719
+ let vector_layers = $derived.by(() => {
720
+ if (!structure?.sites) return []
721
+ const keys = get_structure_vector_keys(structure)
722
+ const active_keys = keys.filter((key) => vector_configs[key]?.visible !== false)
723
+ if (active_keys.length === 0) return []
724
+
725
+ // Build per-site lookup; skip hidden sites so they don't contribute
726
+ // arrows or affect autoscaling. null entries = hidden site.
727
+ const active_set = new Set(active_keys)
728
+ let max_mag = 0
729
+ const site_vec_maps = structure.sites.map((site, site_idx) => {
730
+ if (!is_site_visible(site_idx)) return null
731
+ const map = new SvelteMap<string, Vec3>()
732
+ for (const { key, vec } of get_all_site_vectors(site)) {
733
+ map.set(key, vec)
734
+ if (active_set.has(key)) {
735
+ max_mag = Math.max(max_mag, Math.hypot(...vec))
736
+ }
737
+ }
738
+ return map
739
+ })
740
+
741
+ // When normalize is on, treat all magnitudes as 1 so arrows have equal length
742
+ const effective_max = vector_normalize ? 1 : max_mag
743
+ const auto_scale = effective_max > 1e-10
744
+ ? (char_atom_spacing * 1.8) / effective_max
745
+ : 1
746
+ const is_single = active_keys.length === 1
747
+ const effective_global_scale = auto_scale * vector_scale
748
+
749
+ // When vector_origin_gap > 0 and multiple vectors exist at a site,
750
+ // arrange arrow origins on a regular polygon centered on the atom, in a
751
+ // plane perpendicular to the mean vector direction. The gap is a fraction
752
+ // of the visual atom radius (0 = center, 0.5 = halfway to surface).
753
+ // get_site_radius() returns the uniform scale applied to SphereGeometry(0.5),
754
+ // so visual_radius = get_site_radius() * 0.5.
755
+ const site_offsets = (vector_origin_gap > 0 && !is_single)
756
+ ? structure.sites.map((site, site_idx) => {
757
+ const vec_map = site_vec_maps[site_idx]
758
+ if (!vec_map) return null
759
+ const site_keys = active_keys.filter((key) => vec_map.has(key))
760
+ const n_keys = site_keys.length
761
+ if (n_keys <= 1) return null
762
+ const visual_radius = get_site_radius(site, site_idx) * 0.5
763
+ const gap_abs = vector_origin_gap * visual_radius
764
+ let mean: Vec3 = [0, 0, 0]
765
+ for (const key of site_keys) {
766
+ const vec = vec_map.get(key)
767
+ if (vec) mean = math.add(mean, math.normalize_vec3(vec)) as Vec3
768
+ }
769
+ const mean_dir = math.normalize_vec3(mean, [0, 1, 0] as Vec3)
770
+ const [u_vec, v_vec] = math.compute_in_plane_basis(mean_dir)
771
+ const offsets = new SvelteMap<string, Vec3>()
772
+ for (const [idx, key] of site_keys.entries()) {
773
+ const angle = (2 * Math.PI * idx) / n_keys
774
+ const dx = math.scale(u_vec, gap_abs * Math.cos(angle)) as Vec3
775
+ const dy = math.scale(v_vec, gap_abs * Math.sin(angle)) as Vec3
776
+ offsets.set(key, math.add(dx, dy) as Vec3)
777
+ }
778
+ return offsets
779
+ })
780
+ : null
781
+
782
+ const mag_interpolator = get_d3_interpolator(vector_color_scale)
783
+
784
+ return active_keys.map((key, layer_idx) => {
785
+ const layer_cfg = vector_configs[key]
786
+ const layer_scale = effective_global_scale * (layer_cfg?.scale ?? 1.0)
787
+ const layer_color = layer_cfg?.color ??
788
+ VECTOR_PALETTE[layer_idx % VECTOR_PALETTE.length]
789
+
790
+ const arrows = structure.sites
791
+ .map((site, site_idx) => {
792
+ const vec_map = site_vec_maps[site_idx]
793
+ if (!vec_map) return null
794
+ const vec = vec_map.get(key)
795
+ if (!vec) return null
796
+
797
+ // Resolve color mode: explicit per-key color always wins,
798
+ // then multi-key uses palette, then mode-based coloring
799
+ let arrow_color: string
800
+ if (layer_cfg?.color) {
801
+ arrow_color = layer_cfg.color
802
+ } else if (!is_single) arrow_color = layer_color
803
+ else {
804
+ const effective_mode = vector_color_mode === `auto`
805
+ ? (key.startsWith(`magmom`) || key.startsWith(`spin`)
806
+ ? `spin_direction`
807
+ : `element`)
808
+ : vector_color_mode
809
+ if (effective_mode === `magnitude`) {
810
+ const mag = Math.hypot(...vec)
811
+ const norm = max_mag > 1e-10 ? mag / max_mag : 0
812
+ arrow_color = mag_interpolator(norm)
813
+ } else if (effective_mode === `spin_direction`) {
814
+ arrow_color = spin_direction_color(vec)
815
+ } else if (effective_mode === `uniform`) {
816
+ arrow_color = vector_color
817
+ } else {
818
+ const majority_element = site.species.length > 0
819
+ ? site.species.reduce((max, spec) =>
820
+ spec.occu > max.occu ? spec : max
821
+ ).element
822
+ : undefined
823
+ arrow_color =
824
+ (majority_element && colors.element?.[majority_element]) ||
825
+ vector_color
826
+ }
827
+ }
828
+
829
+ const offset = site_offsets?.[site_idx]?.get(key)
830
+ const position = offset ? math.add(site.xyz, offset) as Vec3 : site.xyz
831
+ const arrow_vec = vector_normalize ? math.normalize_vec3(vec) : vec
832
+
833
+ return {
834
+ site_idx,
835
+ position,
836
+ vector: arrow_vec,
837
+ scale: layer_scale,
838
+ color: arrow_color,
839
+ }
840
+ })
841
+ .filter((item): item is NonNullable<typeof item> => item !== null)
842
+
843
+ return { key, arrows }
404
844
  })
405
- .filter((item) => item !== null)
406
- : []);
407
- let instanced_atom_groups = $derived(Object.values(atom_data
408
- .filter((atom) => !atom.has_partial_occupancy)
409
- .reduce((groups, atom) => {
410
- const { element, radius, color, is_image_atom } = atom;
411
- // Separate image atoms into their own groups for distinct styling in edit-atoms mode
412
- const key = `${element}-${format_num(radius, `.3~`)}-${color}-${is_image_atom ? `img` : `base`}`;
413
- const bucket = groups[key] ||
414
- (groups[key] = { element, radius, color, is_image_atom, atoms: [] });
415
- bucket.atoms.push(atom);
416
- return groups;
417
- }, {})));
418
- let unique_instanced_atoms = $derived(Object.values(instanced_atom_groups
419
- .flatMap((group) => group.atoms)
420
- .reduce((acc, atom) => {
421
- acc[atom.site_idx] = atom;
422
- return acc;
423
- }, {})));
424
- let gizmo_props = $derived.by(() => {
425
- const axis_options = Object.fromEntries([...AXIS_COLORS, ...NEG_AXIS_COLORS].map(([axis, color, hover_color]) => [
845
+ })
846
+
847
+ let instanced_atom_groups = $derived(
848
+ Object.values(
849
+ atom_data
850
+ .filter((atom) => !atom.has_partial_occupancy)
851
+ .reduce(
852
+ (groups, atom) => {
853
+ const { element, radius, color, is_image_atom } = atom
854
+ // Separate image atoms into their own groups for distinct styling in edit-atoms mode
855
+ const key = `${element}-${format_num(radius, `.3~`)}-${color}-${
856
+ is_image_atom ? `img` : `base`
857
+ }`
858
+ const bucket = groups[key] ||
859
+ (groups[key] = { element, radius, color, is_image_atom, atoms: [] })
860
+ bucket.atoms.push(atom)
861
+ return groups
862
+ },
863
+ {} as Record<string, InstancedAtomGroup>,
864
+ ),
865
+ ),
866
+ )
867
+
868
+ let unique_instanced_atoms = $derived(
869
+ Object.values(
870
+ instanced_atom_groups
871
+ .flatMap((group) => group.atoms)
872
+ .reduce((acc, atom) => {
873
+ acc[atom.site_idx] = atom
874
+ return acc
875
+ }, {} as Record<number, (typeof atom_data)[number]>),
876
+ ),
877
+ )
878
+
879
+ let gizmo_props = $derived.by(() => {
880
+ const axis_options = Object.fromEntries(
881
+ [...AXIS_COLORS, ...NEG_AXIS_COLORS].map(([axis, color, hover_color]) => [
426
882
  axis,
427
883
  {
428
- color,
429
- labelColor: `#111`,
430
- opacity: axis.startsWith(`n`) ? 0.9 : 0.8,
431
- hover: {
432
- color: hover_color,
433
- labelColor: `#222222`,
434
- opacity: axis.startsWith(`n`) ? 1 : 0.9,
435
- },
884
+ color,
885
+ labelColor: `#111`,
886
+ opacity: axis.startsWith(`n`) ? 0.9 : 0.8,
887
+ hover: {
888
+ color: hover_color,
889
+ labelColor: `#222222`,
890
+ opacity: axis.startsWith(`n`) ? 1 : 0.9,
891
+ },
436
892
  },
437
- ]));
893
+ ]),
894
+ )
438
895
  return {
439
- background: { enabled: false },
440
- className: `responsive-gizmo`,
441
- ...axis_options,
442
- ...(typeof gizmo === `boolean` ? {} : gizmo),
443
- offset: { left: 5, bottom: 5 },
444
- };
445
- });
446
- let orbit_controls_props = $derived({
896
+ background: { enabled: false },
897
+ className: `responsive-gizmo`,
898
+ ...axis_options,
899
+ ...(typeof gizmo === `boolean` ? {} : gizmo),
900
+ offset: { left: 5, bottom: 5 },
901
+ }
902
+ })
903
+
904
+ let orbit_controls_props = $derived({
447
905
  position: [0, 0, 0],
448
906
  enableRotate: rotate_speed > 0,
449
907
  rotateSpeed: rotate_speed,
@@ -452,7 +910,7 @@ let orbit_controls_props = $derived({
452
910
  zoomToCursor: zoom_to_cursor,
453
911
  enablePan: pan_speed > 0,
454
912
  panSpeed: pan_speed,
455
- target: rotation_target,
913
+ target: camera_target ?? rotation_target,
456
914
  maxZoom: max_zoom,
457
915
  minZoom: min_zoom,
458
916
  autoRotate: Boolean(auto_rotate),
@@ -460,20 +918,20 @@ let orbit_controls_props = $derived({
460
918
  enableDamping: Boolean(rotation_damping),
461
919
  dampingFactor: rotation_damping,
462
920
  onstart: () => {
463
- camera_is_moving = true;
464
- hovered_idx = null;
921
+ camera_is_moving = true
922
+ hovered_idx = null
465
923
  },
466
924
  onend: () => {
467
- camera_is_moving = false;
925
+ camera_is_moving = false
468
926
  },
469
- });
470
- let measure_line_color = $derived.by(() => {
471
- if (typeof window === `undefined`)
472
- return;
473
- const root_styles = getComputedStyle(document.documentElement);
474
- const text_color = root_styles.getPropertyValue(`--text-color`).trim();
475
- return text_color || `#808080`;
476
- });
927
+ })
928
+
929
+ let measure_line_color = $derived.by(() => {
930
+ if (typeof window === `undefined`) return
931
+ const root_styles = getComputedStyle(document.documentElement)
932
+ const text_color = root_styles.getPropertyValue(`--text-color`).trim()
933
+ return text_color || `#808080`
934
+ })
477
935
  </script>
478
936
 
479
937
  {#snippet bond_instanced_mesh_snippet(
@@ -498,27 +956,17 @@ let measure_line_color = $derived.by(() => {
498
956
  style:padding="{site_label_padding}px"
499
957
  style:color={site_label_color}
500
958
  >
501
- {#if show_site_labels && show_site_indices}
502
- {#if site.species.length === 1}
503
- {site.species[0].element}-{site_idx + 1}
504
- {:else}
505
- {@html site.species.map((spec) =>
506
- `${spec.element}<sub>${
507
- format_num(spec.occu, `.3~`).replace(`0.`, `.`)
508
- }</sub>`
509
- ).join(``)}-{
510
- site_idx + 1
511
- }
512
- {/if}
513
- {:else if show_site_labels}
959
+ {#if show_site_labels}
514
960
  {#if site.species.length === 1}
515
- {site.species[0].element}
961
+ {site.species[0].element}{#if show_site_indices}-{site_idx + 1}{/if}
516
962
  {:else}
517
- {@html site.species.map((spec) =>
963
+ {@html sanitize_html(site.species.map((spec) =>
518
964
  `${spec.element}<sub>${
519
965
  format_num(spec.occu, `.3~`).replace(`0.`, `.`)
520
966
  }</sub>`
521
- ).join(``)}
967
+ ).join(``))}{#if show_site_indices}-{
968
+ site_idx + 1
969
+ }{/if}
522
970
  {/if}
523
971
  {:else if show_site_indices}
524
972
  {site_idx + 1}
@@ -570,6 +1018,7 @@ let measure_line_color = $derived.by(() => {
570
1018
  {@const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom}
571
1019
  <extras.InstancedMesh
572
1020
  key="{element}-{format_num(radius, `.3~`)}-{color}-{is_image_atom ? `img` : `base`}-{edit_mode_image}"
1021
+ limit={atoms.length}
573
1022
  range={atoms.length}
574
1023
  frustumCulled={false}
575
1024
  >
@@ -637,7 +1086,7 @@ let measure_line_color = $derived.by(() => {
637
1086
  sphere_segments,
638
1087
  sphere_segments,
639
1088
  atom.start_phi,
640
- 2 * Math.PI * atom.occupancy,
1089
+ atom.phi_length,
641
1090
  ]}
642
1091
  />
643
1092
  <T.MeshStandardMaterial
@@ -647,9 +1096,16 @@ let measure_line_color = $derived.by(() => {
647
1096
  />
648
1097
  </T.Mesh>
649
1098
 
650
- {#if atom.has_partial_occupancy}
1099
+ {#if atom.has_partial_occupancy && atom.render_start_cap}
651
1100
  <T.Mesh rotation={[0, atom.start_phi, 0]}>
652
- <T.CircleGeometry args={[0.5, sphere_segments]} />
1101
+ <T.CircleGeometry
1102
+ args={[
1103
+ 0.5,
1104
+ sphere_segments,
1105
+ PARTIAL_OCCUPANCY_CAP_ARC.start_cap_arc_start,
1106
+ PARTIAL_OCCUPANCY_CAP_ARC.arc_length,
1107
+ ]}
1108
+ />
653
1109
  <T.MeshStandardMaterial
654
1110
  color={partial_color}
655
1111
  side={2}
@@ -657,8 +1113,17 @@ let measure_line_color = $derived.by(() => {
657
1113
  transparent={partial_edit_image}
658
1114
  />
659
1115
  </T.Mesh>
1116
+ {/if}
1117
+ {#if atom.has_partial_occupancy && atom.render_end_cap}
660
1118
  <T.Mesh rotation={[0, atom.end_phi, 0]}>
661
- <T.CircleGeometry args={[0.5, sphere_segments]} />
1119
+ <T.CircleGeometry
1120
+ args={[
1121
+ 0.5,
1122
+ sphere_segments,
1123
+ PARTIAL_OCCUPANCY_CAP_ARC.end_cap_arc_start,
1124
+ PARTIAL_OCCUPANCY_CAP_ARC.arc_length,
1125
+ ]}
1126
+ />
662
1127
  <T.MeshStandardMaterial
663
1128
  color={partial_color}
664
1129
  side={2}
@@ -684,11 +1149,16 @@ let measure_line_color = $derived.by(() => {
684
1149
  {/if}
685
1150
  {/if}
686
1151
 
687
- {#if force_data.length > 0}
688
- {#each force_data as force (force.position.join(`,`) + force.vector.join(`,`))}
689
- <Arrow {...force} />
1152
+ {#each vector_layers as layer (layer.key)}
1153
+ {#each layer.arrows as arrow (`${layer.key}-${arrow.site_idx}`)}
1154
+ <Arrow
1155
+ {...arrow}
1156
+ shaft_radius={eff_shaft_radius}
1157
+ arrow_head_radius={eff_head_radius}
1158
+ arrow_head_length={eff_head_length}
1159
+ />
690
1160
  {/each}
691
- {/if}
1161
+ {/each}
692
1162
 
693
1163
  <!-- Instanced bond rendering with gradient colors -->
694
1164
  {#if instanced_bond_groups.length > 0}
@@ -806,8 +1276,12 @@ let measure_line_color = $derived.by(() => {
806
1276
 
807
1277
  <!-- hovered site tooltip -->
808
1278
  {#if hovered_site && !camera_is_moving && active_tooltip === `atom`}
809
- {@const abc = hovered_site.abc.map((x) => format_num(x, float_fmt)).join(`, `)}
810
- {@const xyz = hovered_site.xyz.map((x) => format_num(x, float_fmt)).join(`, `)}
1279
+ {@const abc = hovered_site.abc.map((val) => format_num(val, float_fmt)).join(
1280
+ `, `,
1281
+ )}
1282
+ {@const xyz = hovered_site.xyz.map((val) => format_num(val, float_fmt)).join(
1283
+ `, `,
1284
+ )}
811
1285
  {@const bond_neighbors = (() => {
812
1286
  if (hovered_idx == null || !structure?.sites) return []
813
1287
  return filtered_bond_pairs
@@ -853,7 +1327,7 @@ let measure_line_color = $derived.by(() => {
853
1327
  {#if occu !== 1}<span class="occupancy">{
854
1328
  format_num(occu, `.3~f`)
855
1329
  }</span>{/if}
856
- <strong>{element}{@html oxi_str}</strong>
1330
+ <strong>{element}{@html sanitize_html(oxi_str)}</strong>
857
1331
  {#if element_name}<span class="elem-name">{element_name}</span>{/if}
858
1332
  {/each}
859
1333
  </div>
@@ -874,7 +1348,7 @@ let measure_line_color = $derived.by(() => {
874
1348
  structure?.sites}
875
1349
  {@const selected_atoms = selected_sites
876
1350
  .map((idx) => structure?.sites?.[idx])
877
- .filter(Boolean) as Site[]}
1351
+ .filter((site): site is Site => site != null)}
878
1352
  {#if selected_atoms.length > 0}
879
1353
  {@const avg = (dim: number) =>
880
1354
  selected_atoms.reduce((sum, atom) => sum + atom.xyz[dim], 0) /
@@ -994,12 +1468,12 @@ let measure_line_color = $derived.by(() => {
994
1468
  {:else if measure_mode === `angle` && measured_sites.length >= 3}
995
1469
  {#each measured_sites as idx_center (idx_center)}
996
1470
  {@const center = structure.sites[idx_center]}
997
- {#each measured_sites.filter((x) => x !== idx_center) as
1471
+ {#each measured_sites.filter((idx) => idx !== idx_center) as
998
1472
  idx_a,
999
1473
  loop_idx
1000
1474
  (idx_center + `-` + idx_a)
1001
1475
  }
1002
- {#each measured_sites.filter((x) => x !== idx_center).slice(loop_idx + 1) as
1476
+ {#each measured_sites.filter((idx) => idx !== idx_center).slice(loop_idx + 1) as
1003
1477
  idx_b
1004
1478
  (idx_center + `-` + idx_a + `-` + idx_b)
1005
1479
  }