matterviz 0.4.1 → 0.4.2

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 (395) hide show
  1. package/dist/EmptyState.svelte +10 -4
  2. package/dist/FilePicker.svelte +12 -14
  3. package/dist/Icon.svelte +7 -4
  4. package/dist/MillerIndexInput.svelte +2 -2
  5. package/dist/app.css +53 -0
  6. package/dist/brillouin/BrillouinZone.svelte +75 -85
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +5 -7
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +18 -14
  10. package/dist/brillouin/BrillouinZoneInfoPane.svelte +6 -10
  11. package/dist/brillouin/BrillouinZoneScene.svelte +26 -39
  12. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +1 -1
  13. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  14. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +62 -74
  15. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +1 -1
  16. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +511 -547
  17. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +1 -1
  18. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  19. package/dist/chempot-diagram/controls-state.svelte.js +1 -1
  20. package/dist/colors/index.d.ts +0 -4
  21. package/dist/colors/index.js +19 -20
  22. package/dist/composition/BarChart.svelte +37 -31
  23. package/dist/composition/BubbleChart.svelte +17 -17
  24. package/dist/composition/Composition.svelte +7 -11
  25. package/dist/composition/Formula.svelte +18 -25
  26. package/dist/composition/FormulaFilter.svelte +92 -95
  27. package/dist/composition/PieChart.svelte +55 -41
  28. package/dist/composition/format.js +1 -1
  29. package/dist/composition/index.d.ts +2 -0
  30. package/dist/composition/index.js +14 -0
  31. package/dist/composition/parse.d.ts +2 -2
  32. package/dist/composition/parse.js +11 -10
  33. package/dist/constants.js +11 -9
  34. package/dist/convex-hull/ConvexHull.svelte +12 -15
  35. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -1
  36. package/dist/convex-hull/ConvexHull2D.svelte +93 -92
  37. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  38. package/dist/convex-hull/ConvexHull3D.svelte +109 -140
  39. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  40. package/dist/convex-hull/ConvexHull4D.svelte +75 -94
  41. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  42. package/dist/convex-hull/ConvexHullChrome.svelte +36 -40
  43. package/dist/convex-hull/ConvexHullControls.svelte +46 -48
  44. package/dist/convex-hull/ConvexHullStats.svelte +193 -195
  45. package/dist/convex-hull/ConvexHullTooltip.svelte +5 -12
  46. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  47. package/dist/convex-hull/StructurePopup.svelte +17 -21
  48. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  49. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  50. package/dist/convex-hull/canvas-interactions.svelte.js +2 -1
  51. package/dist/convex-hull/helpers.d.ts +8 -10
  52. package/dist/convex-hull/helpers.js +36 -36
  53. package/dist/convex-hull/hull-state.svelte.d.ts +1 -1
  54. package/dist/convex-hull/thermodynamics.js +18 -17
  55. package/dist/coordination/CoordinationBarPlot.svelte +38 -33
  56. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  57. package/dist/coordination/calc-coordination.d.ts +1 -1
  58. package/dist/coordination/calc-coordination.js +28 -25
  59. package/dist/element/BohrAtom.svelte +5 -8
  60. package/dist/element/ElementHeading.svelte +4 -1
  61. package/dist/element/ElementPhoto.svelte +11 -9
  62. package/dist/element/ElementStats.svelte +12 -10
  63. package/dist/element/ElementTile.svelte +24 -29
  64. package/dist/element/Nucleus.svelte +4 -2
  65. package/dist/element/data.d.ts +1 -2
  66. package/dist/element/helpers.d.ts +4 -0
  67. package/dist/element/helpers.js +18 -0
  68. package/dist/element/index.d.ts +1 -4
  69. package/dist/element/index.js +3 -18
  70. package/dist/feedback/DragOverlay.svelte +5 -5
  71. package/dist/feedback/DragOverlay.svelte.d.ts +2 -2
  72. package/dist/feedback/StatusMessage.svelte +7 -4
  73. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  74. package/dist/fermi-surface/FermiSurface.svelte +14 -22
  75. package/dist/fermi-surface/FermiSurface.svelte.d.ts +5 -7
  76. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  77. package/dist/fermi-surface/FermiSurfaceControls.svelte.d.ts +1 -1
  78. package/dist/fermi-surface/FermiSurfaceScene.svelte +37 -64
  79. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +1 -1
  80. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  81. package/dist/fermi-surface/compute.js +3 -3
  82. package/dist/fermi-surface/parse.js +22 -24
  83. package/dist/heatmap-matrix/HeatmapMatrix.svelte +109 -148
  84. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +2 -2
  85. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +9 -7
  86. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  87. package/dist/heatmap-matrix/index.d.ts +5 -0
  88. package/dist/icons.d.ts +4 -0
  89. package/dist/icons.js +4 -0
  90. package/dist/io/ExportPane.svelte +10 -6
  91. package/dist/io/decompress.d.ts +2 -1
  92. package/dist/io/decompress.js +22 -35
  93. package/dist/io/export.d.ts +4 -0
  94. package/dist/io/export.js +5 -1
  95. package/dist/io/fetch.d.ts +0 -3
  96. package/dist/io/fetch.js +0 -11
  97. package/dist/io/index.d.ts +0 -1
  98. package/dist/io/index.js +2 -9
  99. package/dist/io/is-binary.d.ts +9 -0
  100. package/dist/io/is-binary.js +45 -1
  101. package/dist/io/url-drop.js +13 -25
  102. package/dist/isosurface/Isosurface.svelte +17 -23
  103. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  104. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  105. package/dist/isosurface/parse-vaspwave.js +138 -0
  106. package/dist/isosurface/parse.js +6 -6
  107. package/dist/labels.d.ts +6 -2
  108. package/dist/labels.js +43 -13
  109. package/dist/layout/FullscreenButton.svelte +2 -1
  110. package/dist/layout/FullscreenToggle.svelte +10 -4
  111. package/dist/layout/InfoCard.svelte +7 -18
  112. package/dist/layout/InfoTag.svelte +1 -3
  113. package/dist/layout/NumberRangeInput.svelte +46 -0
  114. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  115. package/dist/layout/PropertyFilter.svelte +2 -6
  116. package/dist/layout/SettingsSection.svelte +21 -17
  117. package/dist/layout/SubpageGrid.svelte +3 -1
  118. package/dist/layout/ViewerChrome.svelte +9 -10
  119. package/dist/layout/ViewerChrome.svelte.d.ts +2 -1
  120. package/dist/layout/fullscreen.js +3 -5
  121. package/dist/layout/fullscreen.svelte.js +1 -2
  122. package/dist/layout/index.d.ts +1 -0
  123. package/dist/layout/index.js +1 -0
  124. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  125. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  126. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  127. package/dist/layout/json-tree/utils.js +14 -23
  128. package/dist/math.d.ts +2 -0
  129. package/dist/math.js +19 -0
  130. package/dist/overlays/ContextMenu.svelte +4 -2
  131. package/dist/overlays/CopyButton.svelte +11 -5
  132. package/dist/overlays/DragControlTab.svelte +6 -1
  133. package/dist/overlays/DraggablePane.svelte +81 -26
  134. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  135. package/dist/overlays/GlassChip.svelte +30 -0
  136. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  137. package/dist/overlays/InfoPaneCards.svelte +11 -23
  138. package/dist/overlays/index.d.ts +7 -0
  139. package/dist/overlays/index.js +26 -0
  140. package/dist/overlays/portal.d.ts +2 -0
  141. package/dist/overlays/portal.js +14 -0
  142. package/dist/periodic-table/PeriodicTable.svelte +113 -145
  143. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  144. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  145. package/dist/periodic-table/TableInset.svelte +9 -4
  146. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  147. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  148. package/dist/phase-diagram/PhaseDiagramControls.svelte +70 -94
  149. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +1 -1
  150. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +2 -1
  151. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +12 -12
  152. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  153. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  154. package/dist/phase-diagram/build-diagram.js +3 -3
  155. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  156. package/dist/phase-diagram/parse.js +15 -15
  157. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  158. package/dist/phase-diagram/utils.js +2 -3
  159. package/dist/plot/bar/BarPlot.svelte +430 -391
  160. package/dist/plot/bar/BarPlot.svelte.d.ts +2 -0
  161. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  162. package/dist/plot/bar/BarPlotControls.svelte.d.ts +4 -6
  163. package/dist/plot/bar/SpacegroupBarPlot.svelte +29 -36
  164. package/dist/plot/bar/data.d.ts +5 -4
  165. package/dist/plot/bar/data.js +2 -1
  166. package/dist/plot/box/BoxPlot.svelte +301 -209
  167. package/dist/plot/box/BoxPlot.svelte.d.ts +2 -0
  168. package/dist/plot/box/Violin.svelte.d.ts +1 -0
  169. package/dist/plot/box/box-plot.js +1 -1
  170. package/dist/plot/core/auto-place.js +1 -1
  171. package/dist/plot/core/axis-utils.d.ts +12 -7
  172. package/dist/plot/core/axis-utils.js +48 -48
  173. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  174. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  175. package/dist/plot/core/components/ColorBar.svelte +101 -106
  176. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  177. package/dist/plot/core/components/FillArea.svelte +10 -17
  178. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  179. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  180. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  181. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  182. package/dist/plot/core/components/Line.svelte +18 -9
  183. package/dist/plot/core/components/Line.svelte.d.ts +2 -0
  184. package/dist/plot/core/components/PlotAxis.svelte +40 -17
  185. package/dist/plot/core/components/PlotControls.svelte +65 -41
  186. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  187. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  188. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  189. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  190. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  191. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  192. package/dist/plot/core/components/ReferenceLine3D.svelte +54 -57
  193. package/dist/plot/core/components/ReferencePlane.svelte +7 -8
  194. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  195. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  196. package/dist/plot/core/components/index.d.ts +1 -0
  197. package/dist/plot/core/components/index.js +1 -0
  198. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  199. package/dist/plot/core/data-cleaning-signal.js +467 -0
  200. package/dist/plot/core/data-cleaning.d.ts +2 -42
  201. package/dist/plot/core/data-cleaning.js +27 -474
  202. package/dist/plot/core/data-transform.js +9 -5
  203. package/dist/plot/core/fill-utils.d.ts +3 -1
  204. package/dist/plot/core/fill-utils.js +28 -6
  205. package/dist/plot/core/index.d.ts +1 -0
  206. package/dist/plot/core/index.js +1 -0
  207. package/dist/plot/core/interactions.js +0 -2
  208. package/dist/plot/core/layout.d.ts +3 -1
  209. package/dist/plot/core/layout.js +22 -13
  210. package/dist/plot/core/marginals.d.ts +170 -0
  211. package/dist/plot/core/marginals.js +436 -0
  212. package/dist/plot/core/reference-line.js +1 -1
  213. package/dist/plot/core/scales.d.ts +23 -0
  214. package/dist/plot/core/scales.js +39 -13
  215. package/dist/plot/core/types/fills.d.ts +101 -0
  216. package/dist/plot/core/types/fills.js +15 -0
  217. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  218. package/dist/plot/core/types/plot-3d.js +4 -0
  219. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  220. package/dist/plot/core/types/reference-lines.js +9 -0
  221. package/dist/plot/core/types.d.ts +29 -324
  222. package/dist/plot/core/types.js +23 -20
  223. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  224. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  225. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  226. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  227. package/dist/plot/core/utils/label-placement.js +2 -2
  228. package/dist/plot/histogram/Histogram.svelte +304 -325
  229. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  230. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  231. package/dist/plot/histogram/histogram.d.ts +31 -0
  232. package/dist/plot/histogram/histogram.js +70 -0
  233. package/dist/plot/histogram/index.d.ts +1 -0
  234. package/dist/plot/histogram/index.js +1 -0
  235. package/dist/plot/index.d.ts +1 -0
  236. package/dist/plot/index.js +1 -0
  237. package/dist/plot/sankey/Sankey.svelte +78 -69
  238. package/dist/plot/sankey/Sankey.svelte.d.ts +2 -2
  239. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  240. package/dist/plot/scatter/BinnedScatterPlot.svelte +263 -109
  241. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +7 -0
  242. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  243. package/dist/plot/scatter/ScatterPlot.svelte +477 -385
  244. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +2 -0
  245. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  246. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  247. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  248. package/dist/plot/scatter/binned-scatter-types.d.ts +3 -2
  249. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  250. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  251. package/dist/plot/scatter/scatter-data.js +8 -7
  252. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +25 -34
  253. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +2 -2
  254. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +8 -5
  255. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +113 -116
  256. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +2 -2
  257. package/dist/plot/scatter-3d/Surface3D.svelte +4 -13
  258. package/dist/plot/sunburst/Sunburst.svelte +251 -329
  259. package/dist/plot/sunburst/Sunburst.svelte.d.ts +2 -2
  260. package/dist/plot/sunburst/index.d.ts +1 -1
  261. package/dist/plot/sunburst/index.js +2 -1
  262. package/dist/plot/sunburst/render.d.ts +2 -2
  263. package/dist/plot/sunburst/render.js +47 -22
  264. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  265. package/dist/plot/sunburst/sunburst.js +10 -3
  266. package/dist/plot/treemap/Treemap.svelte +907 -0
  267. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  268. package/dist/plot/treemap/index.d.ts +3 -0
  269. package/dist/plot/treemap/index.js +4 -0
  270. package/dist/plot/treemap/treemap.d.ts +26 -0
  271. package/dist/plot/treemap/treemap.js +99 -0
  272. package/dist/rdf/RdfPlot.svelte +22 -26
  273. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  274. package/dist/rdf/calc-rdf.js +4 -8
  275. package/dist/sanitize.js +1 -1
  276. package/dist/scene/bind-renderer.svelte.js +14 -6
  277. package/dist/settings.d.ts +19 -9
  278. package/dist/settings.js +77 -50
  279. package/dist/spectral/Bands.svelte +223 -230
  280. package/dist/spectral/BandsAndDos.svelte +11 -9
  281. package/dist/spectral/BrillouinBandsDos.svelte +19 -27
  282. package/dist/spectral/Dos.svelte +70 -73
  283. package/dist/spectral/helpers.d.ts +3 -3
  284. package/dist/spectral/helpers.js +25 -35
  285. package/dist/state.svelte.js +5 -18
  286. package/dist/structure/Arrow.svelte +5 -18
  287. package/dist/structure/ArrowInstances.svelte +160 -0
  288. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  289. package/dist/structure/AtomLegend.svelte +115 -75
  290. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  291. package/dist/structure/Bond.svelte +31 -23
  292. package/dist/structure/CanvasTooltip.svelte +15 -3
  293. package/dist/structure/CellSelect.svelte +11 -26
  294. package/dist/structure/Cylinder.svelte +24 -40
  295. package/dist/structure/InstancedAtoms.svelte +114 -0
  296. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  297. package/dist/structure/Lattice.svelte +85 -149
  298. package/dist/structure/SiteLabels.svelte +122 -0
  299. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  300. package/dist/structure/Structure.svelte +473 -419
  301. package/dist/structure/Structure.svelte.d.ts +4 -2
  302. package/dist/structure/StructureCarousel.svelte +630 -0
  303. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  304. package/dist/structure/StructureControls.svelte +157 -399
  305. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  306. package/dist/structure/StructureExportPane.svelte +17 -21
  307. package/dist/structure/StructureInfoPane.svelte +72 -66
  308. package/dist/structure/StructureInfoPane.svelte.d.ts +1 -1
  309. package/dist/structure/StructureScene.svelte +467 -476
  310. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  311. package/dist/structure/StructureViewport.svelte +386 -0
  312. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  313. package/dist/structure/atom-label-placement.d.ts +16 -12
  314. package/dist/structure/atom-label-placement.js +70 -37
  315. package/dist/structure/atom-properties.d.ts +2 -0
  316. package/dist/structure/atom-properties.js +104 -38
  317. package/dist/structure/bonding.d.ts +10 -3
  318. package/dist/structure/bonding.js +84 -28
  319. package/dist/structure/edit-history.d.ts +6 -0
  320. package/dist/structure/edit-history.js +27 -0
  321. package/dist/structure/export.d.ts +1 -0
  322. package/dist/structure/export.js +39 -31
  323. package/dist/structure/format-detect.d.ts +1 -0
  324. package/dist/structure/format-detect.js +22 -0
  325. package/dist/structure/geometry.d.ts +9 -0
  326. package/dist/structure/geometry.js +27 -0
  327. package/dist/structure/index.d.ts +16 -1
  328. package/dist/structure/index.js +13 -2
  329. package/dist/structure/measure.d.ts +1 -1
  330. package/dist/structure/measure.js +6 -12
  331. package/dist/structure/parse.d.ts +2 -1
  332. package/dist/structure/parse.js +243 -222
  333. package/dist/structure/partial-occupancy.js +4 -2
  334. package/dist/structure/pbc.js +78 -98
  335. package/dist/structure/polyhedra.d.ts +0 -1
  336. package/dist/structure/polyhedra.js +3 -18
  337. package/dist/structure/supercell.d.ts +1 -2
  338. package/dist/structure/supercell.js +7 -13
  339. package/dist/symmetry/SymmetryElements.svelte +11 -15
  340. package/dist/symmetry/SymmetryStats.svelte +56 -68
  341. package/dist/symmetry/WyckoffTable.svelte +5 -24
  342. package/dist/symmetry/cell-transform.js +1 -0
  343. package/dist/symmetry/index.d.ts +1 -0
  344. package/dist/symmetry/index.js +8 -2
  345. package/dist/symmetry/spacegroups.js +3 -2
  346. package/dist/table/HeatmapTable.svelte +1102 -365
  347. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  348. package/dist/table/ToggleMenu.svelte +5 -12
  349. package/dist/table/index.d.ts +21 -6
  350. package/dist/table/index.js +34 -21
  351. package/dist/theme/ThemeControl.svelte +16 -8
  352. package/dist/theme/index.d.ts +1 -5
  353. package/dist/theme/index.js +4 -10
  354. package/dist/theme/themes.mjs +14 -7
  355. package/dist/tooltip/KCoords.svelte +4 -1
  356. package/dist/tooltip/TooltipContent.svelte +2 -5
  357. package/dist/trajectory/Trajectory.svelte +515 -308
  358. package/dist/trajectory/Trajectory.svelte.d.ts +1 -1
  359. package/dist/trajectory/TrajectoryError.svelte +11 -8
  360. package/dist/trajectory/TrajectoryExportPane.svelte +14 -31
  361. package/dist/trajectory/TrajectoryInfoPane.svelte +85 -97
  362. package/dist/trajectory/extract.js +56 -34
  363. package/dist/trajectory/format-detect.js +11 -9
  364. package/dist/trajectory/frame-reader.d.ts +2 -0
  365. package/dist/trajectory/frame-reader.js +33 -29
  366. package/dist/trajectory/helpers.d.ts +2 -0
  367. package/dist/trajectory/helpers.js +18 -7
  368. package/dist/trajectory/index.d.ts +1 -0
  369. package/dist/trajectory/index.js +3 -0
  370. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  371. package/dist/trajectory/parse/h5-utils.js +109 -0
  372. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  373. package/dist/trajectory/parse/hdf5.js +116 -106
  374. package/dist/trajectory/parse/index.d.ts +2 -3
  375. package/dist/trajectory/parse/index.js +17 -20
  376. package/dist/trajectory/parse/lammps.js +8 -12
  377. package/dist/trajectory/parse/vasp.js +10 -7
  378. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  379. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  380. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  381. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  382. package/dist/trajectory/parse/xyz.js +11 -7
  383. package/dist/trajectory/plotting.d.ts +8 -9
  384. package/dist/trajectory/plotting.js +99 -27
  385. package/dist/utils.d.ts +3 -0
  386. package/dist/utils.js +7 -0
  387. package/dist/xrd/XrdPlot.svelte +47 -61
  388. package/dist/xrd/broadening.js +2 -1
  389. package/dist/xrd/calc-xrd.js +2 -4
  390. package/dist/xrd/parse.d.ts +0 -1
  391. package/dist/xrd/parse.js +56 -73
  392. package/package.json +61 -33
  393. package/dist/api/mp.d.ts +0 -6
  394. package/dist/api/mp.js +0 -22
  395. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
@@ -25,7 +25,6 @@
25
25
  StructureBond,
26
26
  } from './'
27
27
  import {
28
- Arrow,
29
28
  atomic_radii,
30
29
  Cylinder,
31
30
  get_all_site_vectors,
@@ -34,11 +33,11 @@
34
33
  Lattice,
35
34
  VECTOR_PALETTE,
36
35
  } from './'
36
+ import ArrowInstances from './ArrowInstances.svelte'
37
+ import InstancedAtoms from './InstancedAtoms.svelte'
38
+ import SiteLabels from './SiteLabels.svelte'
37
39
  import type { AtomColorConfig } from './atom-properties'
38
- import {
39
- get_orig_site_idx,
40
- get_property_colors,
41
- } from './atom-properties'
40
+ import { get_orig_site_idx, get_property_colors } from './atom-properties'
42
41
  import type { SymmetryElement } from '../symmetry'
43
42
  import { has_visible_symmetry_overlay } from '../symmetry/symmetry-elements'
44
43
  import SymmetryElements from '../symmetry/SymmetryElements.svelte'
@@ -59,9 +58,9 @@
59
58
  import type { BondEditResult, BondingStrategy, BondKeyTarget } from './bonding'
60
59
  import {
61
60
  add_or_restore_bond,
62
- BONDING_STRATEGIES,
63
61
  BOND_ORDER_OPTIONS,
64
62
  canonicalize_bond_target,
63
+ compute_bonds,
65
64
  delete_bond as apply_delete_bond,
66
65
  get_bond_key,
67
66
  get_bond_render_matrices,
@@ -70,37 +69,11 @@
70
69
  set_bond_order as apply_set_bond_order,
71
70
  structure_bond_to_bond_pair,
72
71
  } from './bonding'
73
- import {
74
- CanvasTooltip,
75
- compose_perceived_bonds,
76
- perceive_bond_orders,
77
- } from './index'
78
- import {
79
- choose_site_label_offset,
80
- LABEL_OFFSET_EPS,
81
- make_label_position_calculator,
82
- } from './atom-label-placement'
72
+ import { CanvasTooltip, compose_perceived_bonds, perceive_bond_orders } from './index'
73
+ import { choose_site_label_offset, LABEL_OFFSET_EPS } from './atom-label-placement'
83
74
  import type { PolyhedraColorMode, Polyhedron } from './polyhedra'
84
75
  import { compute_polyhedra, merge_polyhedra_buffers } from './polyhedra'
85
76
 
86
- type InstancedAtomGroup = {
87
- element: string
88
- radius: number
89
- color: string
90
- is_image_atom: boolean
91
- atoms: (typeof atom_data)[number][]
92
- }
93
-
94
- function instanced_atom_group_key(
95
- { element, radius, color, is_image_atom, atoms }: InstancedAtomGroup,
96
- measure_mode: MeasureMode,
97
- ): string {
98
- const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom
99
- return `${element}-${format_num(radius, `.3~`)}-${color}-${
100
- is_image_atom ? `img` : `base`
101
- }-${edit_mode_image}-${atoms.length}`
102
- }
103
-
104
77
  type EditableAtomHitTarget = {
105
78
  site_idx: number
106
79
  position: Vec3
@@ -133,6 +106,7 @@
133
106
  same_size_atoms = false,
134
107
  camera_position = DEFAULTS.structure.camera_position,
135
108
  camera_target = undefined,
109
+ camera_direction = undefined,
136
110
  camera_projection = DEFAULTS.structure.camera_projection,
137
111
  rotation_damping = DEFAULTS.structure.rotation_damping,
138
112
  max_zoom = DEFAULTS.structure.max_zoom,
@@ -237,6 +211,7 @@
237
211
  dragging_atoms = $bindable(false),
238
212
  volumetric_data = undefined,
239
213
  isosurface_settings = DEFAULT_ISOSURFACE_SETTINGS,
214
+ interactive = true,
240
215
  }: SceneControlProps & {
241
216
  structure?: AnyStructure
242
217
  base_structure?: AnyStructure // The original structure without image atoms, used for property color calculation
@@ -245,6 +220,9 @@
245
220
  // determined by the atomic radius of the element
246
221
  camera_position?: [x: number, y: number, z: number] // initial camera position from which to render the scene
247
222
  camera_target?: Vec3 // external orbit-controls target for pan synchronization
223
+ // When set (and camera_position is unset/zero), auto-place the camera along this
224
+ // direction from the structure center (used by the multi-side view for fixed angles)
225
+ camera_direction?: Vec3
248
226
  show_atoms?: boolean
249
227
  show_bonds?: ShowBonds
250
228
  show_site_labels?: boolean
@@ -287,7 +265,10 @@
287
265
  // Fractional coords must refer to the SAME cell as the rendered lattice (moyo
288
266
  // operations are in the input-cell frame, i.e. the original untransformed cell).
289
267
  symmetry_elements?: SymmetryElement[]
290
- symmetry_elements_props?: Omit<ComponentProps<typeof SymmetryElements>, `elements` | `lattice`>
268
+ symmetry_elements_props?: Omit<
269
+ ComponentProps<typeof SymmetryElements>,
270
+ `elements` | `lattice`
271
+ >
291
272
  // Auto-reduce visual clutter while a symmetry-element overlay is visible: hides
292
273
  // coordination polyhedra and calculated bonds, and shrinks atoms so axes/planes/
293
274
  // centers stay readable. Purely derived — toggling the overlay off restores the
@@ -337,6 +318,9 @@
337
318
  dragging_atoms?: boolean // true while TransformControls drag is active (skips expensive recalculations)
338
319
  volumetric_data?: VolumetricData // Active volumetric data for isosurface rendering
339
320
  isosurface_settings?: IsosurfaceSettings // Isosurface rendering settings
321
+ // When false, render the scene without hover/edit raycast helpers. Used by multi-side
322
+ // view so inactive panes skip interaction-only work while the active pane stays editable.
323
+ interactive?: boolean
340
324
  } = $props()
341
325
 
342
326
  const pulse = create_pulse_animation(
@@ -364,7 +348,6 @@
364
348
  initial_computed_zoom = stored_initial_zoom
365
349
  })
366
350
 
367
- let bond_pairs: BondPair[] = $state([])
368
351
  let atom_tooltip_active = $state(false)
369
352
  let hovered_bond_key = $state<string | null>(null)
370
353
  const ATOM_HOVER_CLEAR_DELAY_MS = 200
@@ -392,20 +375,18 @@
392
375
  }, ATOM_HOVER_CLEAR_DELAY_MS)
393
376
  }
394
377
 
395
- const atom_hover_props = (site_idx: number | null, disabled = false) => ({
396
- onpointerenter: () => {
397
- if (!disabled && site_idx != null) set_atom_hover(site_idx)
398
- },
399
- onpointermove: () => {
400
- if (!disabled && site_idx != null) set_atom_hover(site_idx)
401
- },
402
- onpointerleave: () => {
403
- if (!disabled && site_idx != null) schedule_atom_hover_clear(site_idx)
404
- },
405
- })
378
+ const atom_hover_props = (site_idx: number | null) =>
379
+ !interactive || site_idx == null
380
+ ? {}
381
+ : {
382
+ onpointerenter: () => set_atom_hover(site_idx),
383
+ onpointermove: () => set_atom_hover(site_idx),
384
+ onpointerleave: () => schedule_atom_hover_clear(site_idx),
385
+ }
406
386
 
407
387
  // Cursor style for the canvas, derived from mode and hover state
408
388
  let canvas_cursor = $derived.by(() => {
389
+ if (!interactive) return `default`
409
390
  if (measure_mode === `edit-atoms` && add_atom_mode) return `crosshair`
410
391
  if (measure_mode === `edit-bonds` && hovered_bond_key != null) {
411
392
  return bond_edits_enabled ? `pointer` : `not-allowed`
@@ -419,7 +400,6 @@
419
400
  if (measure_mode === `edit-atoms`) {
420
401
  const site = structure?.sites?.[hovered_idx]
421
402
  if (site?.properties?.orig_site_idx != null) return `not-allowed`
422
- return `pointer`
423
403
  }
424
404
  return `pointer`
425
405
  }
@@ -474,8 +454,9 @@
474
454
  ): BondKeyTarget {
475
455
  const rendered_target = { site_idx_1, site_idx_2, cell_shift }
476
456
  const rendered_key = rendered_bond_key_for(rendered_target)
477
- return find_added_bond_by_rendered_key(rendered_key) ??
478
- canonical_bond_target(rendered_target)
457
+ return (
458
+ find_added_bond_by_rendered_key(rendered_key) ?? canonical_bond_target(rendered_target)
459
+ )
479
460
  }
480
461
 
481
462
  const is_image_bond_site = (site_idx: number): boolean =>
@@ -486,9 +467,11 @@
486
467
 
487
468
  const can_edit_bond = (bond: BondKeyTarget): boolean => {
488
469
  const target = canonical_bond_target(bond)
489
- return bond_edits_enabled &&
470
+ return (
471
+ bond_edits_enabled &&
490
472
  !is_image_bond_site(target.site_idx_1) &&
491
473
  !is_image_bond_site(target.site_idx_2)
474
+ )
492
475
  }
493
476
 
494
477
  const format_bond_order = (order: BondOrder | undefined): string =>
@@ -500,10 +483,12 @@
500
483
  cell_shift?: Vec3,
501
484
  ): BondOrder | undefined {
502
485
  const key = get_bond_key(site_idx_1, site_idx_2, cell_shift)
503
- return find_added_bond_by_rendered_key(key)?.order ??
486
+ return (
487
+ find_added_bond_by_rendered_key(key)?.order ??
504
488
  bond_order_overrides.find((bond) => matches_bond_key(bond, key))?.order ??
505
489
  added_bonds.find((bond) => matches_bond_key(bond, key))?.order ??
506
490
  filtered_bond_pairs.find((bond) => matches_bond_key(bond, key))?.bond_order
491
+ )
507
492
  }
508
493
 
509
494
  const midpoint = (pos_1: Vec3, pos_2: Vec3): Vec3 => [
@@ -553,9 +538,10 @@
553
538
  return site_idx
554
539
  }
555
540
 
556
- const image_site_idx = structure.sites.findIndex((candidate_site) =>
557
- candidate_site.properties?.orig_site_idx === site_idx &&
558
- matches_world_position(candidate_site)
541
+ const image_site_idx = structure.sites.findIndex(
542
+ (candidate_site) =>
543
+ candidate_site.properties?.orig_site_idx === site_idx &&
544
+ matches_world_position(candidate_site),
559
545
  )
560
546
  return image_site_idx === -1 ? site_idx : image_site_idx
561
547
  }
@@ -590,10 +576,7 @@
590
576
 
591
577
  let label_screen_margin = $derived(site_label_size * 10 + site_label_padding)
592
578
 
593
- function get_bond_context_menu_position(
594
- bond: BondPair,
595
- event?: BondContextMenuEvent,
596
- ): Vec3 {
579
+ function get_bond_context_menu_position(bond: BondPair, event?: BondContextMenuEvent): Vec3 {
597
580
  const parent = event?.object?.parent
598
581
  if (!event?.point || !parent) return midpoint(bond.pos_1, bond.pos_2)
599
582
 
@@ -634,8 +617,10 @@
634
617
  ): BondPair | undefined => {
635
618
  const rendered_key = rendered_bond_key_for(target)
636
619
  const canonical_key = bond_key_for(canonical_target)
637
- return filtered_bond_pairs.find((bond) => rendered_bond_key_for(bond) === rendered_key) ??
620
+ return (
621
+ filtered_bond_pairs.find((bond) => rendered_bond_key_for(bond) === rendered_key) ??
638
622
  filtered_bond_pairs.find((bond) => bond_key_for(bond) === canonical_key)
623
+ )
639
624
  }
640
625
 
641
626
  function open_bond_order_menu_for_target(
@@ -647,6 +632,7 @@
647
632
  }
648
633
 
649
634
  function add_or_restore_pair(site_idx_1: number, site_idx_2: number) {
635
+ if (!interactive) return // inactive panes must not mutate shared bond state
650
636
  const rendered_target = { site_idx_1, site_idx_2 }
651
637
  if (!can_edit_bond(rendered_target)) return
652
638
  const edit_state = current_bond_edit_state()
@@ -704,11 +690,7 @@
704
690
  const target = resolve_bond_edit_target(site_idx_1, site_idx_2, cell_shift)
705
691
  if (!can_edit_bond(target)) return
706
692
  apply_bond_edit_result(
707
- apply_delete_bond(
708
- current_bond_edit_state(),
709
- target,
710
- editable_perceived_bond_pairs,
711
- ),
693
+ apply_delete_bond(current_bond_edit_state(), target, editable_perceived_bond_pairs),
712
694
  )
713
695
  }
714
696
 
@@ -722,12 +704,10 @@
722
704
  // intercept the same native click, only the first intersection should fire.
723
705
  // All threlte intersection events from one click share the same nativeEvent ref.
724
706
  let last_native_event: Event | null = null
725
- // extras.Instance does not always emit pointerdown, so edit-bonds also falls
726
- // back to click. When pointerdown did fire, skip the matching click once.
707
+ // Instanced-atom raycasts do not always emit pointerdown, so edit-bonds also
708
+ // falls back to click. When pointerdown did fire, skip the matching click once.
727
709
  let last_edit_bonds_pointerdown_site_idx: number | null = null
728
- let clear_edit_bonds_pointerdown_site_timeout:
729
- | ReturnType<typeof setTimeout>
730
- | null = null
710
+ let clear_edit_bonds_pointerdown_site_timeout: ReturnType<typeof setTimeout> | null = null
731
711
 
732
712
  function remember_edit_bonds_pointerdown_site(site_idx: number) {
733
713
  last_edit_bonds_pointerdown_site_idx = site_idx
@@ -756,30 +736,62 @@
756
736
  return true
757
737
  }
758
738
 
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
- }
739
+ // Selection handlers shared by instanced atom meshes and per-site hit targets
740
+ // so the edit-bonds click semantics can't drift between the two paths
741
+ const handle_atom_pointerdown = (site_idx: number, event: PointerEvent) => {
742
+ if (measure_mode !== `edit-bonds` || bond_edit_mode !== `add`) return
743
+ select_edit_bonds_site(site_idx, event)
744
+ }
745
+ const handle_atom_click = (site_idx: number, event: MouseEvent) => {
746
+ if (measure_mode === `edit-bonds`) {
747
+ if (bond_edit_mode !== `add`) return
748
+ if (skip_duplicate_edit_bonds_click(site_idx)) {
749
+ event.stopPropagation()
750
+ return
775
751
  }
776
- toggle_selection(site_idx, event)
777
- },
778
- })
752
+ }
753
+ toggle_selection(site_idx, event)
754
+ }
755
+
756
+ // Pointer handlers for an instanced atom mesh: intersection events carry the
757
+ // hit `instanceId`, which indexes into the mesh's `atoms` array. One handler
758
+ // set per mesh instead of one per atom. Inactive grid panes render without
759
+ // raycast handlers; ghosted edit-mode image atoms are non-interactive.
760
+ type InstanceEvent = { instanceId?: number }
761
+ const atom_instance_events = (
762
+ instance_atoms: { site_idx: number }[],
763
+ is_edit_image: boolean,
764
+ ) => {
765
+ if (!interactive || is_edit_image) return {}
766
+ const wrap =
767
+ <Event_ extends InstanceEvent>(handler: (site_idx: number, event: Event_) => void) =>
768
+ (event: Event_) => {
769
+ const site_idx = instance_atoms[event.instanceId ?? -1]?.site_idx
770
+ if (site_idx != null) handler(site_idx, event)
771
+ }
772
+ return {
773
+ onpointerenter: wrap(set_atom_hover),
774
+ onpointermove: wrap(set_atom_hover),
775
+ onpointerleave: wrap(schedule_atom_hover_clear),
776
+ onpointerdown: wrap<PointerEvent & InstanceEvent>(handle_atom_pointerdown),
777
+ onclick: wrap<MouseEvent & InstanceEvent>(handle_atom_click),
778
+ }
779
+ }
780
+
781
+ // Pointer props (hover + select) for per-site hit-target meshes (partial-occupancy
782
+ // sites), with the same interactivity gating as atom_instance_events
783
+ const atom_pointer_props = (site_idx: number, is_edit_image: boolean) =>
784
+ !interactive || is_edit_image
785
+ ? {}
786
+ : {
787
+ ...atom_hover_props(site_idx),
788
+ onpointerdown: (event: PointerEvent) => handle_atom_pointerdown(site_idx, event),
789
+ onclick: (event: MouseEvent) => handle_atom_click(site_idx, event),
790
+ }
779
791
 
780
792
  function toggle_selection(site_index: number, evt?: Event) {
781
793
  evt?.stopPropagation?.()
782
- const event_with_native = evt as Event & { nativeEvent?: unknown } | undefined
794
+ const event_with_native = evt as (Event & { nativeEvent?: unknown }) | undefined
783
795
  const native_event = event_with_native?.nativeEvent ?? evt
784
796
  if (native_event instanceof Event) {
785
797
  if (native_event === last_native_event) return
@@ -787,6 +799,9 @@
787
799
  }
788
800
 
789
801
  if (measure_mode === `edit-bonds`) {
802
+ // Only the active pane edits: inactive panes keep selection/hover overlays visible
803
+ // but must not change selection or add/restore bonds in shared state.
804
+ if (!interactive) return
790
805
  if (bond_edit_mode === `delete`) {
791
806
  measured_sites = []
792
807
  selected_sites = []
@@ -811,12 +826,16 @@
811
826
  }
812
827
 
813
828
  if (measure_mode === `edit-atoms`) {
829
+ // Inactive panes don't drive edit-atoms selection (gizmo/add-plane are interactive-gated)
830
+ if (!interactive) return
814
831
  // Block image atoms (detected by orig_site_idx property from PBC)
815
832
  const site = structure?.sites?.[site_index]
816
833
  if (site?.properties?.orig_site_idx != null) return
817
834
 
818
835
  const is_selected = selected_sites.includes(site_index)
819
- const is_shift = evt instanceof MouseEvent && evt.shiftKey
836
+ // threlte dispatches plain objects wrapping the DOM event, so read shift
837
+ // from the extracted native event (evt itself is never a MouseEvent for 3D hits)
838
+ const is_shift = native_event instanceof MouseEvent && native_event.shiftKey
820
839
 
821
840
  // In edit-atoms mode, selected_sites and measured_sites always stay in sync
822
841
  let new_sites: number[]
@@ -866,11 +885,15 @@
866
885
  $effect(() => {
867
886
  const count = structure?.sites?.length ?? 0
868
887
  if (count <= 0) {
869
- measured_sites = []
888
+ if (untrack(() => measured_sites.length) > 0) measured_sites = []
870
889
  return
871
890
  }
872
891
  untrack(() => {
873
- measured_sites = measured_sites.filter((idx) => idx >= 0 && idx < count)
892
+ // Only reassign when out-of-range indices were dropped: a fresh (equal)
893
+ // array identity on every structure change would ripple through all
894
+ // measured_sites bindings and their dependents each frame of a trajectory.
895
+ const filtered = measured_sites.filter((idx) => idx >= 0 && idx < count)
896
+ if (filtered.length !== measured_sites.length) measured_sites = filtered
874
897
  })
875
898
  })
876
899
 
@@ -882,9 +905,7 @@
882
905
  $effect.pre(() => {
883
906
  hovered_site = structure?.sites?.[hovered_idx ?? -1] ?? null
884
907
  })
885
- let lattice = $derived(
886
- structure && `lattice` in structure ? structure.lattice : null,
887
- )
908
+ let lattice = $derived(structure && `lattice` in structure ? structure.lattice : null)
888
909
 
889
910
  let visual_lattice = $derived(
890
911
  base_structure && `lattice` in base_structure ? base_structure.lattice : lattice,
@@ -894,9 +915,11 @@
894
915
  lattice
895
916
  ? math.scale(math.add(...lattice.matrix), 0.5)
896
917
  : structure
897
- ? get_center_of_mass(structure)
898
- : [0, 0, 0] as Vec3,
918
+ ? get_center_of_mass(structure)
919
+ : ([0, 0, 0] as Vec3),
899
920
  )
921
+ // Negated target for the inner un-translate group (recomputed only on target change)
922
+ let neg_rotation_target = $derived(math.scale(rotation_target, -1) as Vec3)
900
923
 
901
924
  let structure_size = $derived.by(() => {
902
925
  if (lattice) return (lattice.a + lattice.b + lattice.c) / 2
@@ -913,8 +936,8 @@
913
936
  // Excludes PBC image atoms (orig_site_idx) so toggling image atoms doesn't affect arrow sizing.
914
937
  let char_atom_spacing = $derived.by(() => {
915
938
  if (!lattice || !structure?.sites?.length) return structure_size
916
- const n_real = structure.sites.filter((site) =>
917
- site.properties?.orig_site_idx == null
939
+ const n_real = structure.sites.filter(
940
+ (site) => site.properties?.orig_site_idx == null,
918
941
  ).length
919
942
  return n_real > 0 ? Math.cbrt(lattice.volume / n_real) : structure_size
920
943
  })
@@ -945,9 +968,19 @@
945
968
 
946
969
  // Using $state because this is mutated in an effect based on viewport/structure size
947
970
  let computed_zoom = $state(untrack(() => initial_zoom))
971
+ // structure_size is read untracked so structure changes don't re-zoom the user's view;
972
+ // zoom only re-frames on a genuine viewport resize. Skip same-value width/height
973
+ // re-fires (a wrapping component can transiently re-emit clientWidth/Height during a
974
+ // structure swap) so they don't sneak in a re-zoom with the new structure_size.
975
+ let last_zoom_dims: [number, number] = [0, 0]
948
976
  $effect(() => {
949
977
  if (!(width > 0) || !(height > 0)) return
950
- const structure_max_dim = Math.max(1, untrack(() => structure_size))
978
+ if (width === last_zoom_dims[0] && height === last_zoom_dims[1]) return
979
+ last_zoom_dims = [width, height]
980
+ const structure_max_dim = Math.max(
981
+ 1,
982
+ untrack(() => structure_size),
983
+ )
951
984
  const viewer_min_dim = Math.min(width, height)
952
985
  const scale_factor = viewer_min_dim / (structure_max_dim * 50) // 50px per unit
953
986
  let new_zoom = initial_zoom * scale_factor
@@ -956,11 +989,23 @@
956
989
  computed_zoom = new_zoom
957
990
  })
958
991
 
959
- $effect.pre(() => { // Simple initial camera auto-position: proportional to structure size and fov
992
+ $effect.pre(() => {
993
+ // Simple initial camera auto-position: proportional to structure size and fov
960
994
  if (camera_position.every((val) => val === 0) && structure) {
961
995
  stored_initial_zoom = undefined
962
996
  const distance = Math.max(1, structure_size) * (60 / fov)
963
- camera_position = [distance, distance * 0.3, distance * 0.8]
997
+ // When a view direction is given (multi-side view), place the camera
998
+ // target-relative along it; otherwise use the default angled position.
999
+ // normalize_vec returns [0,0,0] for an absent or zero-length direction (arrays are
1000
+ // truthy, so a plain `camera_direction ?` check would miss [0,0,0]); treat that as
1001
+ // "no direction" and fall back to the default, since placing the camera on the
1002
+ // rotation target would be a degenerate zero-length view.
1003
+ const view_dir: Vec3 = camera_direction
1004
+ ? math.normalize_vec(camera_direction, [0, 0, 0])
1005
+ : [0, 0, 0]
1006
+ camera_position = view_dir.some((val) => val !== 0)
1007
+ ? math.add(rotation_target, math.scale(view_dir, distance))
1008
+ : [distance, distance * 0.3, distance * 0.8]
964
1009
  }
965
1010
  })
966
1011
  // Whether a never|always|crystals|molecules setting applies to the current structure
@@ -984,23 +1029,29 @@
984
1029
  )
985
1030
  // Calculated bonds are hidden in declutter mode (only their cylinders — bond_pairs
986
1031
  // stay computed so tooltips and manually added bonds keep working)
987
- const effective_show_bonds: ShowBonds = $derived(
988
- declutter_active ? `never` : show_bonds,
989
- )
1032
+ const effective_show_bonds: ShowBonds = $derived(declutter_active ? `never` : show_bonds)
990
1033
  const effective_atom_radius = $derived(declutter_active ? atom_radius * 0.6 : atom_radius)
991
1034
 
992
- $effect(() => {
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.
1035
+ // Derived (not effect + state) so downstream consumers (filtering, polyhedra,
1036
+ // instanced bond buffers) recompute exactly once per structure change instead
1037
+ // of once with stale bonds and again after an effect flush. The mutable cache
1038
+ // lets atom drags freeze the last computed value (recompute happens on release).
1039
+ // Bonds are computed when either bond rendering or polyhedra need them. The
1040
+ // raw/effective mix is deliberate: RAW show_bonds keeps bond_pairs available
1041
+ // during symmetry declutter (cylinders hide via effective_show_bonds in
1042
+ // bonds_to_render, but tooltips + manually added bonds still need the data),
1043
+ // while EFFECTIVE show_polyhedra skips computing bonds whose only consumer —
1044
+ // the polyhedra $derived below, gated on the same effective value — won't run.
1045
+ let last_bond_pairs: BondPair[] = []
1046
+ let bond_pairs: BondPair[] = $derived.by(() => {
1047
+ if (dragging_atoms) return last_bond_pairs
999
1048
  const want_bonds = applies_to_structure(show_bonds)
1000
1049
  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)
1003
- } else bond_pairs = []
1050
+ last_bond_pairs =
1051
+ structure && (want_bonds || want_polyhedra)
1052
+ ? compute_bonds(structure, bonding_strategy, bonding_options)
1053
+ : []
1054
+ return last_bond_pairs
1004
1055
  })
1005
1056
 
1006
1057
  // Compute property-based colors when not using element coloring
@@ -1053,21 +1104,22 @@
1053
1104
  site.properties?.completion_image &&
1054
1105
  !applies_to_structure(effective_show_bonds) &&
1055
1106
  !applies_to_structure(effective_show_polyhedra)
1056
- ) return []
1107
+ )
1108
+ return []
1057
1109
 
1058
1110
  // Calculate radius: same_size > site override > element override > default
1059
1111
  // All radii scale uniformly with atom_radius for consistent slider behavior
1060
1112
  const base_radius = same_size_atoms
1061
1113
  ? 1
1062
- : site_radius_overrides?.get(site_idx) ?? calc_weighted_radius(site)
1114
+ : (site_radius_overrides?.get(site_idx) ?? calc_weighted_radius(site))
1063
1115
  const radius = base_radius * effective_atom_radius
1064
1116
 
1065
1117
  // Use property color if available (e.g. coordination number, Wyckoff position)
1066
1118
  // Otherwise, each species gets its own element color (important for disordered sites)
1067
1119
  const site_property_color = property_colors?.colors[orig_idx]
1068
1120
 
1069
- const visible_species = site.species.filter(({ element }) =>
1070
- !hidden_elements.has(element)
1121
+ const visible_species = site.species.filter(
1122
+ ({ element }) => !hidden_elements.has(element),
1071
1123
  )
1072
1124
  const slice_geometry = compute_slice_geometry(visible_species)
1073
1125
  return slice_geometry.map((slice_data) => {
@@ -1095,13 +1147,12 @@
1095
1147
  const is_site_visible = (site_idx: number): boolean => {
1096
1148
  if (!structure?.sites) return false
1097
1149
  const site = structure.sites[site_idx]
1098
- const has_visible_element = site?.species.some(({ element }) =>
1099
- !hidden_elements.has(element)
1150
+ const has_visible_element = site?.species.some(
1151
+ ({ element }) => !hidden_elements.has(element),
1100
1152
  )
1101
1153
  const orig_idx = get_orig_site_idx(site, site_idx)
1102
1154
  const prop_val = property_colors?.values[orig_idx]
1103
- const prop_visible = prop_val === undefined ||
1104
- !hidden_prop_vals.has(prop_val)
1155
+ const prop_visible = prop_val === undefined || !hidden_prop_vals.has(prop_val)
1105
1156
  return has_visible_element && prop_visible
1106
1157
  }
1107
1158
 
@@ -1127,19 +1178,17 @@
1127
1178
  })
1128
1179
 
1129
1180
  let editable_perceived_bond_pairs = $derived(
1130
- perceived_bond_pairs.map((bond) => ({ ...bond, ...canonical_bond_target(bond) })),
1181
+ interactive && bond_edits_enabled
1182
+ ? perceived_bond_pairs.map((bond) => ({ ...bond, ...canonical_bond_target(bond) }))
1183
+ : [],
1131
1184
  )
1132
1185
 
1133
1186
  let filtered_bond_pairs = $derived.by(() => {
1134
1187
  if (!structure?.sites) return perceived_bond_pairs
1135
1188
 
1136
1189
  // Build set of removed bond keys for efficient lookup
1137
- const removed_keys = new Set(
1138
- removed_bonds.map(bond_key_for),
1139
- )
1140
- const added_keys = new Set(
1141
- added_bonds.map(bond_key_for),
1142
- )
1190
+ const removed_keys = new Set(removed_bonds.map(bond_key_for))
1191
+ const added_keys = new Set(added_bonds.map(bond_key_for))
1143
1192
  const order_overrides = new Map(
1144
1193
  bond_order_overrides.map((bond) => [bond_key_for(bond), bond.order]),
1145
1194
  )
@@ -1177,7 +1226,7 @@
1177
1226
  })
1178
1227
 
1179
1228
  let editable_bond_pairs = $derived(
1180
- bond_edits_enabled ? bonds_to_render.filter(can_edit_bond) : [],
1229
+ interactive && bond_edits_enabled ? bonds_to_render.filter(can_edit_bond) : [],
1181
1230
  )
1182
1231
 
1183
1232
  // Coordination polyhedra around cation-like centers, derived from the same
@@ -1186,10 +1235,12 @@
1186
1235
  // never recompute the hull geometry.
1187
1236
  let polyhedra: Polyhedron[] = $derived.by(() => {
1188
1237
  if (
1189
- !structure?.sites || dragging_atoms ||
1238
+ !structure?.sites ||
1239
+ dragging_atoms ||
1190
1240
  !applies_to_structure(effective_show_polyhedra) ||
1191
1241
  filtered_bond_pairs.length === 0
1192
- ) return []
1242
+ )
1243
+ return []
1193
1244
  return compute_polyhedra(structure, filtered_bond_pairs, {
1194
1245
  min_neighbors: polyhedra_min_neighbors,
1195
1246
  max_neighbors: polyhedra_max_neighbors,
@@ -1203,8 +1254,9 @@
1203
1254
  const site = structure?.sites[site_idx]
1204
1255
  const orig_idx = get_orig_site_idx(site, site_idx)
1205
1256
  const element = get_majority_element(site)
1206
- return property_colors?.colors[orig_idx] ??
1207
- (element && colors.element?.[element]) ?? `#808080`
1257
+ return (
1258
+ property_colors?.colors[orig_idx] ?? (element && colors.element?.[element]) ?? `#808080`
1259
+ )
1208
1260
  }
1209
1261
 
1210
1262
  // Separate derived so material-only changes (opacity, edge color) don't rebuild
@@ -1229,6 +1281,7 @@
1229
1281
  // Publish which elements currently anchor polyhedra (consumed by controls so
1230
1282
  // per-element toggles reflect the actual render state incl. spectator hiding)
1231
1283
  $effect(() => {
1284
+ if (!interactive) return
1232
1285
  const elems = [...new Set(polyhedra.map((poly) => poly.center_element))].sort()
1233
1286
  if (elems.join(`,`) !== polyhedra_rendered_elements.join(`,`)) {
1234
1287
  polyhedra_rendered_elements = elems
@@ -1257,23 +1310,23 @@
1257
1310
 
1258
1311
  let polyhedra_edge_geometry: BufferGeometry | null = $state(null)
1259
1312
  $effect(() => {
1260
- const geo = polyhedra_show_edges && polyhedra_buffers && polyhedra_buffers.edge_count > 0
1261
- ? buffer_geometry({ position: polyhedra_buffers.edge_positions })
1262
- : null
1313
+ const geo =
1314
+ polyhedra_show_edges && polyhedra_buffers && polyhedra_buffers.edge_count > 0
1315
+ ? buffer_geometry({ position: polyhedra_buffers.edge_positions })
1316
+ : null
1263
1317
  polyhedra_edge_geometry = geo
1264
1318
  return () => geo?.dispose()
1265
1319
  })
1266
1320
 
1267
1321
  let smart_site_label_offsets = $derived.by(() => {
1268
- const offsets = new SvelteMap<number, Vec3>()
1322
+ // Plain Maps: built and consumed inside deriveds, so per-entry reactivity
1323
+ // (SvelteMap) would only add signal overhead for potentially thousands of sites
1324
+ const offsets = new Map<number, Vec3>()
1269
1325
  if (bonds_to_render.length === 0) return offsets
1270
1326
 
1271
- const bond_directions_by_site = new SvelteMap<number, Vec3[]>()
1327
+ const bond_directions_by_site = new Map<number, Vec3[]>()
1272
1328
  const add_bond_direction = (site_idx: number, pos_1: Vec3, pos_2: Vec3) => {
1273
- const direction = math.normalize_vec(
1274
- math.subtract(pos_2, pos_1),
1275
- [0, 0, 0],
1276
- )
1329
+ const direction = math.normalize_vec(math.subtract(pos_2, pos_1), [0, 0, 0])
1277
1330
  if (Math.hypot(...direction) < LABEL_OFFSET_EPS) return
1278
1331
  bond_directions_by_site.set(site_idx, [
1279
1332
  ...(bond_directions_by_site.get(site_idx) ?? []),
@@ -1312,7 +1365,7 @@
1312
1365
  const get_majority_color = (site: typeof site_a) => {
1313
1366
  if (!site?.species || site.species.length === 0) return bond_color
1314
1367
  const majority_species = site.species.reduce((max, spec) =>
1315
- spec.occu > max.occu ? spec : max
1368
+ spec.occu > max.occu ? spec : max,
1316
1369
  )
1317
1370
  return colors.element?.[majority_species.element] || bond_color
1318
1371
  }
@@ -1328,7 +1381,7 @@
1328
1381
  })
1329
1382
 
1330
1383
  let radius_by_site_idx = $derived.by(() => {
1331
- const map = new SvelteMap<number, number>()
1384
+ const map = new Map<number, number>()
1332
1385
  for (const atom of atom_data) {
1333
1386
  if (!map.has(atom.site_idx)) map.set(atom.site_idx, atom.radius)
1334
1387
  }
@@ -1340,7 +1393,8 @@
1340
1393
  // angles. Give each such site one invisible full-sphere hit target so it's as
1341
1394
  // reliably hoverable as an ordered atom (single solid sphere). One per site.
1342
1395
  let partial_hit_targets = $derived.by(() => {
1343
- const targets = new SvelteMap<number, EditableAtomHitTarget & { is_image_atom: boolean }>()
1396
+ if (!interactive) return []
1397
+ const targets = new Map<number, EditableAtomHitTarget & { is_image_atom: boolean }>()
1344
1398
  for (const atom of atom_data) {
1345
1399
  if (!atom.has_partial_occupancy || targets.has(atom.site_idx)) continue
1346
1400
  targets.set(atom.site_idx, {
@@ -1355,6 +1409,7 @@
1355
1409
 
1356
1410
  let editable_atom_hit_targets = $derived.by(() => {
1357
1411
  if (
1412
+ !interactive ||
1358
1413
  measure_mode !== `edit-bonds` ||
1359
1414
  bond_edit_mode !== `add` ||
1360
1415
  !bond_edits_enabled
@@ -1362,7 +1417,7 @@
1362
1417
  return []
1363
1418
  }
1364
1419
 
1365
- const targets = new SvelteMap<number, EditableAtomHitTarget>()
1420
+ const targets = new Map<number, EditableAtomHitTarget>()
1366
1421
  for (const atom of atom_data) {
1367
1422
  if (!can_select_bond_site(atom.site_idx)) continue
1368
1423
  if (targets.has(atom.site_idx)) continue
@@ -1378,13 +1433,47 @@
1378
1433
  // Get radius for a site (for highlight fallback when site is hidden/filtered)
1379
1434
  // Checks site_radius_overrides first for consistency with visible atoms
1380
1435
  const get_site_radius = (site: Site, site_idx: number | null): number => {
1381
- const override = site_idx !== null
1382
- ? site_radius_overrides?.get(site_idx)
1383
- : undefined
1384
- const base_radius = same_size_atoms ? 1 : override ?? calc_weighted_radius(site)
1436
+ const override = site_idx !== null ? site_radius_overrides?.get(site_idx) : undefined
1437
+ const base_radius = same_size_atoms ? 1 : (override ?? calc_weighted_radius(site))
1385
1438
  return base_radius * effective_atom_radius
1386
1439
  }
1387
1440
 
1441
+ // Sites to outline with a translucent sphere: hovered + all selected/active sites. Kept
1442
+ // independent of the pulse animation so this list (with its per-site radius lookups) only
1443
+ // rebuilds when highlighted sites change; pulsing opacity is applied per-frame in the
1444
+ // template instead, avoiding a rebuild every frame in every multi-view pane.
1445
+ type HighlightTarget = {
1446
+ kind: `hover` | `selected` | `active`
1447
+ site: Site
1448
+ site_idx: number | null
1449
+ color: string
1450
+ radius: number
1451
+ }
1452
+ let highlight_targets: HighlightTarget[] = $derived.by(() => {
1453
+ const targets: HighlightTarget[] = []
1454
+ const add = (
1455
+ kind: HighlightTarget[`kind`],
1456
+ site: Site | null,
1457
+ site_idx: number | null,
1458
+ color: string,
1459
+ ) => {
1460
+ if (!site) return
1461
+ const radius =
1462
+ site_idx !== null
1463
+ ? (radius_by_site_idx.get(site_idx) ?? get_site_radius(site, site_idx))
1464
+ : get_site_radius(site, site_idx)
1465
+ targets.push({ kind, site, site_idx, color, radius })
1466
+ }
1467
+ add(`hover`, hovered_site, hovered_idx, `white`)
1468
+ for (const idx of selected_sites ?? []) {
1469
+ add(`selected`, structure?.sites?.[idx] ?? null, idx, selection_highlight_color)
1470
+ }
1471
+ for (const idx of active_sites ?? []) {
1472
+ add(`active`, structure?.sites?.[idx] ?? null, idx, active_highlight_color)
1473
+ }
1474
+ return targets
1475
+ })
1476
+
1388
1477
  // Interpolate between spin-down (#3498db blue) and spin-up (#e74c3c red)
1389
1478
  // based on the z-component direction of a magnetic vector
1390
1479
  function spin_direction_color(vec: Vec3): string {
@@ -1393,9 +1482,9 @@
1393
1482
  const red = Math.round(52 + (231 - 52) * z_frac)
1394
1483
  const grn = Math.round(152 + (76 - 152) * z_frac)
1395
1484
  const blu = Math.round(219 + (60 - 219) * z_frac)
1396
- return `#${red.toString(16).padStart(2, `0`)}${
1397
- grn.toString(16).padStart(2, `0`)
1398
- }${blu.toString(16).padStart(2, `0`)}`
1485
+ return `#${red.toString(16).padStart(2, `0`)}${grn
1486
+ .toString(16)
1487
+ .padStart(2, `0`)}${blu.toString(16).padStart(2, `0`)}`
1399
1488
  }
1400
1489
 
1401
1490
  // Build one arrow layer per visible vector key. Auto-scales the longest
@@ -1415,7 +1504,7 @@
1415
1504
  let max_mag = 0
1416
1505
  const site_vec_maps = structure.sites.map((site, site_idx) => {
1417
1506
  if (!is_site_visible(site_idx)) return null
1418
- const map = new SvelteMap<string, Vec3>()
1507
+ const map = new Map<string, Vec3>()
1419
1508
  for (const { key, vec } of get_all_site_vectors(site)) {
1420
1509
  map.set(key, vec)
1421
1510
  if (active_set.has(key)) {
@@ -1427,9 +1516,7 @@
1427
1516
 
1428
1517
  // When normalize is on, treat all magnitudes as 1 so arrows have equal length
1429
1518
  const effective_max = vector_normalize ? 1 : max_mag
1430
- const auto_scale = effective_max > 1e-10
1431
- ? (char_atom_spacing * 1.8) / effective_max
1432
- : 1
1519
+ const auto_scale = effective_max > 1e-10 ? (char_atom_spacing * 1.8) / effective_max : 1
1433
1520
  const is_single = active_keys.length === 1
1434
1521
  const effective_global_scale = auto_scale * vector_scale
1435
1522
 
@@ -1439,40 +1526,40 @@
1439
1526
  // of the visual atom radius (0 = center, 0.5 = halfway to surface).
1440
1527
  // get_site_radius() returns the uniform scale applied to SphereGeometry(0.5),
1441
1528
  // so visual_radius = get_site_radius() * 0.5.
1442
- const site_offsets = (vector_origin_gap > 0 && !is_single)
1443
- ? structure.sites.map((site, site_idx) => {
1444
- const vec_map = site_vec_maps[site_idx]
1445
- if (!vec_map) return null
1446
- const site_keys = active_keys.filter((key) => vec_map.has(key))
1447
- const n_keys = site_keys.length
1448
- if (n_keys <= 1) return null
1449
- const visual_radius = get_site_radius(site, site_idx) * 0.5
1450
- const gap_abs = vector_origin_gap * visual_radius
1451
- let mean: Vec3 = [0, 0, 0]
1452
- for (const key of site_keys) {
1453
- const vec = vec_map.get(key)
1454
- if (vec) mean = math.add(mean, math.normalize_vec(vec)) as Vec3
1455
- }
1456
- const mean_dir = math.normalize_vec(mean, [0, 1, 0] as Vec3)
1457
- const [u_vec, v_vec] = math.compute_in_plane_basis(mean_dir)
1458
- const offsets = new SvelteMap<string, Vec3>()
1459
- for (const [idx, key] of site_keys.entries()) {
1460
- const angle = (2 * Math.PI * idx) / n_keys
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))
1464
- }
1465
- return offsets
1466
- })
1467
- : null
1529
+ const site_offsets =
1530
+ vector_origin_gap > 0 && !is_single
1531
+ ? structure.sites.map((site, site_idx) => {
1532
+ const vec_map = site_vec_maps[site_idx]
1533
+ if (!vec_map) return null
1534
+ const site_keys = active_keys.filter((key) => vec_map.has(key))
1535
+ const n_keys = site_keys.length
1536
+ if (n_keys <= 1) return null
1537
+ const visual_radius = get_site_radius(site, site_idx) * 0.5
1538
+ const gap_abs = vector_origin_gap * visual_radius
1539
+ let mean: Vec3 = [0, 0, 0]
1540
+ for (const key of site_keys) {
1541
+ const vec = vec_map.get(key)
1542
+ if (vec) mean = math.add(mean, math.normalize_vec(vec)) as Vec3
1543
+ }
1544
+ const mean_dir = math.normalize_vec(mean, [0, 1, 0] as Vec3)
1545
+ const [u_vec, v_vec] = math.compute_in_plane_basis(mean_dir)
1546
+ const offsets = new Map<string, Vec3>()
1547
+ for (const [idx, key] of site_keys.entries()) {
1548
+ const angle = (2 * Math.PI * idx) / n_keys
1549
+ const dx = math.scale(u_vec, gap_abs * Math.cos(angle))
1550
+ const dy = math.scale(v_vec, gap_abs * Math.sin(angle))
1551
+ offsets.set(key, math.add(dx, dy))
1552
+ }
1553
+ return offsets
1554
+ })
1555
+ : null
1468
1556
 
1469
1557
  const mag_interpolator = get_d3_interpolator(vector_color_scale)
1470
1558
 
1471
1559
  return active_keys.map((key, layer_idx) => {
1472
1560
  const layer_cfg = vector_configs[key]
1473
1561
  const layer_scale = effective_global_scale * (layer_cfg?.scale ?? 1.0)
1474
- const layer_color = layer_cfg?.color ??
1475
- VECTOR_PALETTE[layer_idx % VECTOR_PALETTE.length]
1562
+ const layer_color = layer_cfg?.color ?? VECTOR_PALETTE[layer_idx % VECTOR_PALETTE.length]
1476
1563
 
1477
1564
  const arrows = structure.sites
1478
1565
  .map((site, site_idx) => {
@@ -1488,11 +1575,12 @@
1488
1575
  arrow_color = layer_cfg.color
1489
1576
  } else if (!is_single) arrow_color = layer_color
1490
1577
  else {
1491
- const effective_mode = vector_color_mode === `auto`
1492
- ? (key.startsWith(`magmom`) || key.startsWith(`spin`)
1493
- ? `spin_direction`
1494
- : `element`)
1495
- : vector_color_mode
1578
+ const effective_mode =
1579
+ vector_color_mode === `auto`
1580
+ ? key.startsWith(`magmom`) || key.startsWith(`spin`)
1581
+ ? `spin_direction`
1582
+ : `element`
1583
+ : vector_color_mode
1496
1584
  if (effective_mode === `magnitude`) {
1497
1585
  const mag = Math.hypot(...vec)
1498
1586
  const norm = max_mag > 1e-10 ? mag / max_mag : 0
@@ -1502,14 +1590,13 @@
1502
1590
  } else if (effective_mode === `uniform`) {
1503
1591
  arrow_color = vector_color
1504
1592
  } else {
1505
- const majority_element = site.species.length > 0
1506
- ? site.species.reduce((max, spec) =>
1507
- spec.occu > max.occu ? spec : max
1508
- ).element
1509
- : undefined
1593
+ const majority_element =
1594
+ site.species.length > 0
1595
+ ? site.species.reduce((max, spec) => (spec.occu > max.occu ? spec : max))
1596
+ .element
1597
+ : undefined
1510
1598
  arrow_color =
1511
- (majority_element && colors.element?.[majority_element]) ||
1512
- vector_color
1599
+ (majority_element && colors.element?.[majority_element]) || vector_color
1513
1600
  }
1514
1601
  }
1515
1602
 
@@ -1531,61 +1618,68 @@
1531
1618
  })
1532
1619
  })
1533
1620
 
1534
- let instanced_atom_groups = $derived(
1535
- Object.values(
1536
- atom_data
1537
- .filter((atom) => !atom.has_partial_occupancy)
1538
- .reduce(
1539
- (groups, atom) => {
1540
- const { element, radius, color, is_image_atom } = atom
1541
- // Separate image atoms into their own groups for distinct styling in edit-atoms mode
1542
- const key = `${element}-${format_num(radius, `.3~`)}-${color}-${
1543
- is_image_atom ? `img` : `base`
1544
- }`
1545
- const bucket = groups[key] ||
1546
- (groups[key] = { element, radius, color, is_image_atom, atoms: [] })
1547
- bucket.atoms.push(atom)
1548
- return groups
1549
- },
1550
- {} as Record<string, InstancedAtomGroup>,
1551
- ),
1552
- ),
1553
- )
1621
+ // Full-occupancy atoms split into base and PBC-image sets. Each set renders
1622
+ // as ONE InstancedMesh (per-atom color/radius live in instance buffers), so
1623
+ // no per-element grouping is needed. Image atoms get their own mesh because
1624
+ // they ghost (desaturate + translucent) and lose interactivity in edit-atoms mode.
1625
+ let instanced_atom_sets = $derived.by(() => {
1626
+ const base: typeof atom_data = []
1627
+ const image: typeof atom_data = []
1628
+ for (const atom of atom_data) {
1629
+ if (!atom.has_partial_occupancy) (atom.is_image_atom ? image : base).push(atom)
1630
+ }
1631
+ return { base, image }
1632
+ })
1554
1633
 
1555
- let unique_instanced_atoms = $derived(
1556
- Object.values(
1557
- instanced_atom_groups
1558
- .flatMap((group) => group.atoms)
1559
- .reduce((acc, atom) => {
1560
- acc[atom.site_idx] = atom
1561
- return acc
1562
- }, {} as Record<number, (typeof atom_data)[number]>),
1563
- ),
1634
+ // One label anchor per visible site (sites can contribute several atom_data
1635
+ // entries, e.g. partial-occupancy wedges — label each site once)
1636
+ let label_entries = $derived.by(() => {
1637
+ if (!show_site_labels && !show_site_indices) return []
1638
+ const seen = new Set<number>()
1639
+ const entries: { site_idx: number; position: Vec3; radius: number }[] = []
1640
+ for (const atom of atom_data) {
1641
+ if (seen.has(atom.site_idx)) continue
1642
+ seen.add(atom.site_idx)
1643
+ entries.push({
1644
+ site_idx: atom.site_idx,
1645
+ position: atom.position,
1646
+ radius: atom.radius,
1647
+ })
1648
+ }
1649
+ return entries
1650
+ })
1651
+
1652
+ // Partial-occupancy atoms render as separate wedge meshes (see template below).
1653
+ // Derived so the filter isn't re-run inline on every render of the atoms group.
1654
+ let partial_occupancy_atoms = $derived(
1655
+ atom_data.filter((atom) => atom.has_partial_occupancy),
1564
1656
  )
1565
1657
 
1566
- let orbit_controls_props = $derived(build_orbit_props({
1567
- camera_projection,
1568
- target: camera_target ?? rotation_target,
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: () => {
1583
- cancel_atom_hover_clear()
1584
- hovered_idx = null
1585
- hovered_bond_key = null
1586
- bond_context_menu = null
1587
- },
1588
- }))
1658
+ let orbit_controls_props = $derived(
1659
+ build_orbit_props({
1660
+ camera_projection,
1661
+ target: camera_target ?? rotation_target,
1662
+ rotate_speed,
1663
+ zoom_speed,
1664
+ zoom_to_cursor,
1665
+ pan_speed,
1666
+ max_zoom,
1667
+ min_zoom,
1668
+ auto_rotate,
1669
+ rotation_damping,
1670
+ set_camera_is_moving: (moving) => (camera_is_moving = moving),
1671
+ // Close hover tooltips + bond context menu while the camera moves. Only hide the
1672
+ // VISIBLE menu (not bond_context_target): clicking a menu button fires this
1673
+ // orbit-controls start handler before the button's own handler runs, which still
1674
+ // needs the target bond to apply the edit (see bond_context_target comment).
1675
+ onstart_extra: () => {
1676
+ cancel_atom_hover_clear()
1677
+ hovered_idx = null
1678
+ hovered_bond_key = null
1679
+ bond_context_menu = null
1680
+ },
1681
+ }),
1682
+ )
1589
1683
 
1590
1684
  let measure_line_color = $derived.by(() => {
1591
1685
  if (typeof window === `undefined`) return
@@ -1595,27 +1689,15 @@
1595
1689
  })
1596
1690
  </script>
1597
1691
 
1598
- {#snippet bond_instanced_mesh_snippet(
1599
- group: ComponentProps<typeof Bond>[`group`],
1600
- )}
1692
+ {#snippet bond_instanced_mesh_snippet(group: ComponentProps<typeof Bond>[`group`])}
1601
1693
  {#key group.instances.length}
1602
1694
  <Bond {group} />
1603
1695
  {/key}
1604
1696
  {/snippet}
1605
1697
 
1606
- {#snippet site_label_snippet(position: Vec3, site_idx: number)}
1698
+ {#snippet site_label_snippet(site_idx: number)}
1607
1699
  {@const site = structure!.sites[site_idx]}
1608
- {@const visual_radius = (radius_by_site_idx.get(site_idx) ?? 1) * 0.5}
1609
- <extras.HTML
1610
- center
1611
- position={position}
1612
- calculatePosition={make_label_position_calculator(
1613
- position,
1614
- () => smart_site_label_offsets.get(site_idx) ?? site_label_offset,
1615
- visual_radius,
1616
- label_screen_margin,
1617
- )}
1618
- >
1700
+ {#if site}
1619
1701
  {#if atom_label}
1620
1702
  {@render atom_label({ site, site_idx })}
1621
1703
  {:else}
@@ -1646,10 +1728,7 @@
1646
1728
  {#if site.species.length === 1}
1647
1729
  {site.species[0].element}{#if show_site_indices}-{site_idx + 1}{/if}
1648
1730
  {:else}
1649
- {#each site.species as
1650
- { element, occu, oxidation_state }
1651
- (`${element}-${occu}-${oxidation_state}`)
1652
- }
1731
+ {#each site.species as { element, occu, oxidation_state } (`${element}-${occu}-${oxidation_state}`)}
1653
1732
  {element}<sub>{format_num(occu, `.3~`).replace(`0.`, `.`)}</sub>
1654
1733
  {/each}
1655
1734
  {#if show_site_indices}-{site_idx + 1}{/if}
@@ -1659,7 +1738,7 @@
1659
1738
  {/if}
1660
1739
  </button>
1661
1740
  {/if}
1662
- </extras.HTML>
1741
+ {/if}
1663
1742
  {/snippet}
1664
1743
 
1665
1744
  <SceneCamera
@@ -1680,56 +1759,39 @@
1680
1759
  <!-- Apply manual rotation around center: translate to origin, rotate, translate back -->
1681
1760
  <T.Group position={rotation_target}>
1682
1761
  <T.Group {rotation}>
1683
- <T.Group position={math.scale(rotation_target, -1)}>
1762
+ <T.Group position={neg_rotation_target}>
1684
1763
  {#if show_atoms}
1685
- <!-- Instanced rendering for full occupancy atoms -->
1686
- {#each instanced_atom_groups as atom_group (instanced_atom_group_key(atom_group, measure_mode))}
1687
- {@const { element, radius, color, is_image_atom, atoms } = atom_group}
1688
- {@const edit_mode_image = measure_mode === `edit-atoms` && is_image_atom}
1689
- <extras.InstancedMesh
1690
- key={instanced_atom_group_key(atom_group, measure_mode)}
1691
- limit={atoms.length}
1692
- range={atoms.length}
1693
- frustumCulled={false}
1694
- >
1695
- <T.SphereGeometry args={[0.5, sphere_segments, sphere_segments]} />
1696
- <T.MeshStandardMaterial
1697
- color={edit_mode_image ? desaturate(color) : color}
1698
- opacity={edit_mode_image ? 0.5 : 1}
1699
- transparent={edit_mode_image}
1700
- />
1701
- {#each atoms as atom (atom.site_idx)}
1702
- <extras.Instance
1703
- position={atom.position}
1704
- scale={atom.radius}
1705
- {...atom_pointer_props(atom.site_idx, edit_mode_image)}
1706
- />
1707
- {/each}
1708
- </extras.InstancedMesh>
1709
- {/each}
1764
+ <!-- Instanced rendering for full-occupancy atoms: one InstancedMesh for
1765
+ base atoms and one for PBC image atoms (which ghost + lose interaction
1766
+ in edit-atoms mode). Pointer events are resolved via raycast instanceId. -->
1767
+ {#if instanced_atom_sets.base.length > 0}
1768
+ <InstancedAtoms
1769
+ atoms={instanced_atom_sets.base}
1770
+ {sphere_segments}
1771
+ {...atom_instance_events(instanced_atom_sets.base, false)}
1772
+ />
1773
+ {/if}
1774
+ {#if instanced_atom_sets.image.length > 0}
1775
+ {@const edit_mode_image = measure_mode === `edit-atoms`}
1776
+ <InstancedAtoms
1777
+ atoms={instanced_atom_sets.image}
1778
+ {sphere_segments}
1779
+ ghost={edit_mode_image}
1780
+ {...atom_instance_events(instanced_atom_sets.image, edit_mode_image)}
1781
+ />
1782
+ {/if}
1710
1783
 
1711
1784
  <!-- Regular rendering for partial occupancy atoms -->
1712
- {#each atom_data.filter((atom) => atom.has_partial_occupancy) as
1713
- atom
1714
- (atom.site_idx + atom.element + atom.occupancy)
1715
- }
1785
+ {#each partial_occupancy_atoms as atom (atom.site_idx + atom.element + atom.occupancy)}
1716
1786
  {@const partial_edit_image = measure_mode === `edit-atoms` && atom.is_image_atom}
1717
1787
  {@const ghost_opacity = partial_edit_image ? 0.5 : 1}
1718
1788
  <!-- Visual only: pointer interaction handled by the invisible full-sphere
1719
1789
  hit targets below (wedge meshes leave gaps at the poles). -->
1720
1790
  <T.Group position={atom.position} scale={atom.radius}>
1721
- {@const partial_color = partial_edit_image
1722
- ? desaturate(atom.color)
1723
- : atom.color}
1791
+ {@const partial_color = partial_edit_image ? desaturate(atom.color) : atom.color}
1724
1792
  <T.Mesh>
1725
1793
  <T.SphereGeometry
1726
- args={[
1727
- 0.5,
1728
- sphere_segments,
1729
- sphere_segments,
1730
- atom.start_phi,
1731
- atom.phi_length,
1732
- ]}
1794
+ args={[0.5, sphere_segments, sphere_segments, atom.start_phi, atom.phi_length]}
1733
1795
  />
1734
1796
  <T.MeshStandardMaterial
1735
1797
  color={partial_color}
@@ -1775,12 +1837,6 @@
1775
1837
  </T.Mesh>
1776
1838
  {/if}
1777
1839
  </T.Group>
1778
-
1779
- <!-- Render label only for the first species of this site to avoid duplicates -->
1780
- {#if (show_site_labels || show_site_indices) &&
1781
- atom.element === structure!.sites[atom.site_idx].species[0].element}
1782
- {@render site_label_snippet(atom.position, atom.site_idx)}
1783
- {/if}
1784
1840
  {/each}
1785
1841
 
1786
1842
  <!-- Invisible full-sphere hit targets for partial-occupancy sites so the
@@ -1797,23 +1853,31 @@
1797
1853
  </T.Mesh>
1798
1854
  {/each}
1799
1855
 
1800
- <!-- Site labels/indices for instanced atoms -->
1801
- {#if show_site_labels || show_site_indices}
1802
- {#each unique_instanced_atoms as atom (atom.site_idx)}
1803
- {@render site_label_snippet(atom.position, atom.site_idx)}
1804
- {/each}
1856
+ <!-- Site labels/indices: single overlay for all labels (one DOM container
1857
+ + one per-frame position pass instead of one threlte <HTML> per label) -->
1858
+ {#if label_entries.length > 0}
1859
+ <SiteLabels
1860
+ entries={label_entries}
1861
+ get_offset={(site_idx) =>
1862
+ smart_site_label_offsets.get(site_idx) ?? site_label_offset}
1863
+ screen_margin={label_screen_margin}
1864
+ >
1865
+ {#snippet label({ site_idx })}
1866
+ {@render site_label_snippet(site_idx)}
1867
+ {/snippet}
1868
+ </SiteLabels>
1805
1869
  {/if}
1806
1870
  {/if}
1807
1871
 
1872
+ <!-- Per-site vector arrows (forces, magmoms, ...) as instanced meshes:
1873
+ 2 draw calls per layer instead of 2 meshes per site -->
1808
1874
  {#each vector_layers as layer (layer.key)}
1809
- {#each layer.arrows as arrow (`${layer.key}-${arrow.site_idx}`)}
1810
- <Arrow
1811
- {...arrow}
1812
- shaft_radius={eff_shaft_radius}
1813
- arrow_head_radius={eff_head_radius}
1814
- arrow_head_length={eff_head_length}
1815
- />
1816
- {/each}
1875
+ <ArrowInstances
1876
+ arrows={layer.arrows}
1877
+ shaft_radius={eff_shaft_radius}
1878
+ arrow_head_radius={eff_head_radius}
1879
+ arrow_head_length={eff_head_length}
1880
+ />
1817
1881
  {/each}
1818
1882
 
1819
1883
  <!-- Instanced bond rendering with gradient colors -->
@@ -1850,16 +1914,12 @@
1850
1914
  {/if}
1851
1915
 
1852
1916
  <!-- Clickable bond hit-test cylinders in edit-bonds mode -->
1853
- {#if measure_mode === `edit-bonds` && editable_bond_pairs.length > 0}
1854
- {#each editable_bond_pairs as
1855
- bond
1856
- (`bond-hit-${bond_edit_mode}-${rendered_bond_key_for(bond)}`)
1857
- }
1917
+ {#if interactive && measure_mode === `edit-bonds` && editable_bond_pairs.length > 0}
1918
+ {#each editable_bond_pairs as bond (`bond-hit-${bond_edit_mode}-${rendered_bond_key_for(bond)}`)}
1858
1919
  {@const bond_key = rendered_bond_key_for(bond)}
1859
1920
  {@const is_hovered = hovered_bond_key === bond_key}
1860
1921
  {@const is_delete_mode = bond_edit_mode === `delete`}
1861
- {@const bond_hit_radius =
1862
- bond_thickness * (is_delete_mode ? 5 : 1.25)}
1922
+ {@const bond_hit_radius = bond_thickness * (is_delete_mode ? 5 : 1.25)}
1863
1923
  {@const bond_hover_radius = bond_thickness * 1.1}
1864
1924
  <T.Mesh
1865
1925
  matrixAutoUpdate={false}
@@ -1888,19 +1948,8 @@
1888
1948
  onpointermove={() => (hovered_bond_key = bond_key)}
1889
1949
  onpointerleave={() => (hovered_bond_key = null)}
1890
1950
  >
1891
- <T.CylinderGeometry
1892
- args={[
1893
- bond_hit_radius,
1894
- bond_hit_radius,
1895
- 1,
1896
- 6,
1897
- ]}
1898
- />
1899
- <T.MeshBasicMaterial
1900
- transparent
1901
- opacity={0}
1902
- depthWrite={false}
1903
- />
1951
+ <T.CylinderGeometry args={[bond_hit_radius, bond_hit_radius, 1, 6]} />
1952
+ <T.MeshBasicMaterial transparent opacity={0} depthWrite={false} />
1904
1953
  </T.Mesh>
1905
1954
  {#if is_hovered}
1906
1955
  <T.Mesh
@@ -1919,7 +1968,7 @@
1919
1968
  {/each}
1920
1969
  {/if}
1921
1970
 
1922
- {#if editable_atom_hit_targets.length > 0}
1971
+ {#if interactive && editable_atom_hit_targets.length > 0}
1923
1972
  {#each editable_atom_hit_targets as atom_hit (atom_hit.site_idx)}
1924
1973
  <T.Mesh
1925
1974
  position={atom_hit.position}
@@ -1930,16 +1979,12 @@
1930
1979
  }}
1931
1980
  >
1932
1981
  <T.SphereGeometry args={[0.5, 12, 12]} />
1933
- <T.MeshBasicMaterial
1934
- transparent
1935
- opacity={0}
1936
- depthWrite={false}
1937
- />
1982
+ <T.MeshBasicMaterial transparent opacity={0} depthWrite={false} />
1938
1983
  </T.Mesh>
1939
1984
  {/each}
1940
1985
  {/if}
1941
1986
 
1942
- {#if measure_mode === `edit-bonds` && bond_context_menu}
1987
+ {#if interactive && measure_mode === `edit-bonds` && bond_context_menu}
1943
1988
  {@const current_order = get_current_bond_order(
1944
1989
  bond_context_menu.site_idx_1,
1945
1990
  bond_context_menu.site_idx_2,
@@ -2003,62 +2048,31 @@
2003
2048
  </extras.HTML>
2004
2049
  {/if}
2005
2050
 
2006
- <!-- highlight hovered, active and selected sites -->
2007
- {#each [
2008
- {
2009
- kind: `hover`,
2010
- site: hovered_site,
2011
- opacity: 0.28,
2012
- color: `white`,
2013
- site_idx: hovered_idx,
2014
- },
2015
- ...((selected_sites ?? []).map((idx) => ({
2016
- kind: `selected`,
2017
- site: structure?.sites?.[idx] ?? null,
2018
- site_idx: idx,
2019
- opacity: pulse_opacity,
2020
- color: selection_highlight_color,
2021
- }))),
2022
- ...((active_sites ?? []).map((idx) => ({
2023
- kind: `active`,
2024
- site: structure?.sites?.[idx] ?? null,
2025
- site_idx: idx,
2026
- opacity: pulse_opacity, // Let it pulse freely
2027
- color: active_highlight_color,
2028
- }))),
2029
- ] as
2030
- entry
2031
- (`${entry.kind}-${entry.site_idx}`)
2032
- }
2033
- {@const { site, opacity, color, kind, site_idx } = entry}
2034
- {#if site}
2035
- {@const xyz = site.xyz}
2036
- {@const highlight_radius = site_idx !== null
2037
- ? radius_by_site_idx.get(site_idx) ?? get_site_radius(site, site_idx)
2038
- : get_site_radius(site, site_idx)}
2039
- <T.Mesh
2040
- position={xyz}
2041
- scale={1.2 * highlight_radius}
2042
- oncreate={disable_raycast}
2043
- >
2044
- <T.SphereGeometry args={[0.5, 22, 22]} />
2045
- <T.MeshStandardMaterial
2046
- {color}
2047
- transparent
2048
- {opacity}
2049
- emissive={color}
2050
- emissiveIntensity={kind === `selected` || kind === `active` ? 0.7 : 0.2}
2051
- depthTest={false}
2052
- depthWrite={false}
2053
- />
2054
- </T.Mesh>
2055
- {/if}
2051
+ <!-- highlight hovered, active and selected sites. The target list is a pulse-
2052
+ independent $derived; only the opacity reads the per-frame pulse value so the
2053
+ array isn't rebuilt every animation frame. -->
2054
+ {#each highlight_targets as entry (`${entry.kind}-${entry.site_idx}`)}
2055
+ {@const is_pulsing = entry.kind !== `hover`}
2056
+ <T.Mesh
2057
+ position={entry.site.xyz}
2058
+ scale={1.2 * entry.radius}
2059
+ oncreate={disable_raycast}
2060
+ >
2061
+ <T.SphereGeometry args={[0.5, 22, 22]} />
2062
+ <T.MeshStandardMaterial
2063
+ color={entry.color}
2064
+ transparent
2065
+ opacity={is_pulsing ? pulse_opacity : 0.28}
2066
+ emissive={entry.color}
2067
+ emissiveIntensity={is_pulsing ? 0.7 : 0.2}
2068
+ depthTest={false}
2069
+ depthWrite={false}
2070
+ />
2071
+ </T.Mesh>
2056
2072
  {/each}
2057
2073
 
2058
2074
  <!-- selection order labels (1, 2, 3, ...) for measurements and bond editing -->
2059
- {#if structure?.sites && (measured_sites?.length ?? 0) > 0 &&
2060
- (measure_mode === `distance` || measure_mode === `angle` ||
2061
- measure_mode === `edit-bonds`)}
2075
+ {#if structure?.sites && (measured_sites?.length ?? 0) > 0 && (measure_mode === `distance` || measure_mode === `angle` || measure_mode === `edit-bonds`)}
2062
2076
  {#each measured_sites as site_index, loop_idx (site_index)}
2063
2077
  {@const site = structure.sites[site_index]}
2064
2078
  {#if site}
@@ -2073,24 +2087,18 @@
2073
2087
  {/if}
2074
2088
 
2075
2089
  <!-- hovered site tooltip -->
2076
- {#if hovered_site && !camera_is_moving &&
2077
- (atom_tooltip_active || active_sites.includes(hovered_idx ?? -1))}
2078
- {@const abc = hovered_site.abc.map((val) => format_num(val, float_fmt)).join(
2079
- `, `,
2080
- )}
2081
- {@const xyz = hovered_site.xyz.map((val) => format_num(val, float_fmt)).join(
2082
- `, `,
2083
- )}
2090
+ {#if hovered_site && !camera_is_moving && (atom_tooltip_active || active_sites.includes(hovered_idx ?? -1))}
2091
+ {@const abc = hovered_site.abc.map((val) => format_num(val, float_fmt)).join(`, `)}
2092
+ {@const xyz = hovered_site.xyz.map((val) => format_num(val, float_fmt)).join(`, `)}
2084
2093
  {@const bond_neighbors = (() => {
2085
2094
  if (hovered_idx == null || !structure?.sites) return []
2086
2095
  return filtered_bond_pairs
2087
- .filter((bond) =>
2088
- bond.site_idx_1 === hovered_idx || bond.site_idx_2 === hovered_idx
2096
+ .filter(
2097
+ (bond) => bond.site_idx_1 === hovered_idx || bond.site_idx_2 === hovered_idx,
2089
2098
  )
2090
2099
  .map((bond) => {
2091
- const neighbor_idx = bond.site_idx_1 === hovered_idx
2092
- ? bond.site_idx_2
2093
- : bond.site_idx_1
2100
+ const neighbor_idx =
2101
+ bond.site_idx_1 === hovered_idx ? bond.site_idx_2 : bond.site_idx_1
2094
2102
  return structure.sites[neighbor_idx]?.species[0]?.element ?? `?`
2095
2103
  })
2096
2104
  })()}
@@ -2107,27 +2115,20 @@
2107
2115
  })()}
2108
2116
  {@const tooltip_species =
2109
2117
  render_sites.find((rs) => rs.site_idx === hovered_idx)?.site.species ??
2110
- hovered_site.species ?? []}
2118
+ hovered_site.species ??
2119
+ []}
2111
2120
  <CanvasTooltip position={hovered_site.xyz}>
2112
2121
  <!-- Element symbols with occupancies for disordered sites -->
2113
2122
  <div class="elements">
2114
- {#each tooltip_species as
2115
- { element, occu, oxidation_state: oxi_state },
2116
- idx
2117
- (`${element ?? ``}-${occu ?? ``}-${oxi_state ?? ``}-${idx}`)
2118
- }
2119
- {@const element_name = element_data.find((elem) =>
2120
- elem.symbol === element
2121
- )?.name ??
2122
- ``}
2123
+ {#each tooltip_species as { element, occu, oxidation_state: oxi_state }, idx (`${element ?? ``}-${occu ?? ``}-${oxi_state ?? ``}-${idx}`)}
2124
+ {@const element_name =
2125
+ element_data.find((elem) => elem.symbol === element)?.name ?? ``}
2123
2126
  <span class="species">
2124
- {#if occu !== 1}<span class="occupancy">{
2125
- format_num(occu, `.3~f`)
2126
- }</span>{/if}
2127
+ {#if occu !== 1}<span class="occupancy">{format_num(occu, `.3~f`)}</span>{/if}
2127
2128
  <strong>
2128
- {element}{#if oxi_state != null && oxi_state !== 0}<sup>{Math.abs(
2129
- oxi_state,
2130
- )}{oxi_state > 0 ? `+` : `−`}</sup>{/if}
2129
+ {element}{#if oxi_state != null && oxi_state !== 0}<sup
2130
+ >{Math.abs(oxi_state)}{oxi_state > 0 ? `+` : `−`}</sup
2131
+ >{/if}
2131
2132
  </strong>
2132
2133
  {#if element_name}<span class="elem-name">{element_name}</span>{/if}
2133
2134
  </span>
@@ -2153,23 +2154,19 @@
2153
2154
  {/if}
2154
2155
 
2155
2156
  <!-- TransformControls for editing atoms in edit-atoms mode -->
2156
- {#if measure_mode === `edit-atoms` && selected_sites.length > 0 &&
2157
- structure?.sites}
2157
+ {#if interactive && measure_mode === `edit-atoms` && selected_sites.length > 0 && structure?.sites}
2158
2158
  {@const selected_atoms = selected_sites
2159
2159
  .map((idx) => structure?.sites?.[idx])
2160
2160
  .filter((site): site is Site => site != null)}
2161
2161
  {#if selected_atoms.length > 0}
2162
2162
  {@const avg = (dim: number) =>
2163
- selected_atoms.reduce((sum, atom) => sum + atom.xyz[dim], 0) /
2164
- selected_atoms.length}
2163
+ selected_atoms.reduce((sum, atom) => sum + atom.xyz[dim], 0) /
2164
+ selected_atoms.length}
2165
2165
  {@const centroid = [avg(0), avg(1), avg(2)] as Vec3}
2166
2166
  <!-- Invisible mesh at centroid for TransformControls to manipulate.
2167
2167
  During drag, use frozen_centroid so Svelte doesn't override TransformControls
2168
2168
  with the wrapped centroid (which jumps on PBC boundary crossings). -->
2169
- <T.Mesh
2170
- position={frozen_centroid ?? centroid}
2171
- bind:ref={transform_object}
2172
- >
2169
+ <T.Mesh position={frozen_centroid ?? centroid} bind:ref={transform_object}>
2173
2170
  <T.SphereGeometry args={[0.01, 4, 4]} />
2174
2171
  <T.MeshBasicMaterial transparent opacity={0} />
2175
2172
  </T.Mesh>
@@ -2208,7 +2205,7 @@
2208
2205
 
2209
2206
  <!-- Invisible plane for click-to-place atom in add-atom mode -->
2210
2207
  <!-- Uses onBeforeRender to orient normal toward camera so raycasts always hit -->
2211
- {#if measure_mode === `edit-atoms` && add_atom_mode}
2208
+ {#if interactive && measure_mode === `edit-atoms` && add_atom_mode}
2212
2209
  {@const center = rotation_target ?? [0, 0, 0]}
2213
2210
  <T.Mesh
2214
2211
  position={center}
@@ -2223,10 +2220,7 @@
2223
2220
  }}
2224
2221
  >
2225
2222
  <T.PlaneGeometry
2226
- args={[
2227
- Math.max(200, structure_size * 4),
2228
- Math.max(200, structure_size * 4),
2229
- ]}
2223
+ args={[Math.max(200, structure_size * 4), Math.max(200, structure_size * 4)]}
2230
2224
  />
2231
2225
  <T.MeshBasicMaterial transparent opacity={0} side={2} depthWrite={false} />
2232
2226
  </T.Mesh>
@@ -2246,27 +2240,23 @@
2246
2240
  {@const site_j = structure.sites[idx_j]}
2247
2241
  {@const pos_i = site_i.xyz}
2248
2242
  {@const pos_j = site_j.xyz}
2249
- <Cylinder
2250
- from={pos_i}
2251
- to={pos_j}
2252
- thickness={0.12}
2253
- color={measure_line_color}
2254
- />
2243
+ <Cylinder from={pos_i} to={pos_j} thickness={0.12} color={measure_line_color} />
2255
2244
  {@const midpoint = [
2256
- (pos_i[0] + pos_j[0]) / 2,
2257
- (pos_i[1] + pos_j[1]) / 2,
2258
- (pos_i[2] + pos_j[2]) / 2,
2259
- ] as Vec3}
2245
+ (pos_i[0] + pos_j[0]) / 2,
2246
+ (pos_i[1] + pos_j[1]) / 2,
2247
+ (pos_i[2] + pos_j[2]) / 2,
2248
+ ] as Vec3}
2260
2249
  {@const direct = math.euclidean_dist(pos_i, pos_j)}
2261
2250
  {@const pbc = lattice
2262
- ? measure.distance_pbc(pos_i, pos_j, lattice.matrix, undefined, lattice.pbc)
2263
- : direct}
2251
+ ? measure.distance_pbc(pos_i, pos_j, lattice.matrix, undefined, lattice.pbc)
2252
+ : direct}
2264
2253
  {@const differ = lattice ? Math.abs(pbc - direct) > 1e-6 : false}
2265
2254
  <extras.HTML center position={midpoint}>
2266
2255
  <span class="measure-label">
2267
2256
  {#if differ}
2268
2257
  PBC: {format_num(pbc, float_fmt)} Å<br /><small>
2269
- Direct: {format_num(direct, float_fmt)} Å</small>
2258
+ Direct: {format_num(direct, float_fmt)} Å</small
2259
+ >
2270
2260
  {:else}
2271
2261
  {format_num(pbc, float_fmt)} Å
2272
2262
  {/if}
@@ -2277,19 +2267,20 @@
2277
2267
  {:else if measure_mode === `angle` && measured_sites.length >= 3}
2278
2268
  {#each measured_sites as idx_center (idx_center)}
2279
2269
  {@const center = structure.sites[idx_center]}
2280
- {#each measured_sites.filter((idx) => idx !== idx_center) as
2281
- idx_a,
2282
- loop_idx
2283
- (idx_center + `-` + idx_a)
2284
- }
2285
- {#each measured_sites.filter((idx) => idx !== idx_center).slice(loop_idx + 1) as
2286
- idx_b
2287
- (idx_center + `-` + idx_a + `-` + idx_b)
2288
- }
2270
+ {#each measured_sites.filter((idx) => idx !== idx_center) as idx_a, loop_idx (idx_center + `-` + idx_a)}
2271
+ {#each measured_sites
2272
+ .filter((idx) => idx !== idx_center)
2273
+ .slice(loop_idx + 1) as idx_b (idx_center + `-` + idx_a + `-` + idx_b)}
2289
2274
  {@const site_a = structure.sites[idx_a]}
2290
2275
  {@const site_b = structure.sites[idx_b]}
2291
2276
  {@const disp = (to: Vec3) =>
2292
- measure.displacement_pbc(center.xyz, to, lattice?.matrix, undefined, lattice?.pbc)}
2277
+ measure.displacement_pbc(
2278
+ center.xyz,
2279
+ to,
2280
+ lattice?.matrix,
2281
+ undefined,
2282
+ lattice?.pbc,
2283
+ )}
2293
2284
  {@const v1 = disp(site_a.xyz)}
2294
2285
  {@const v2 = disp(site_b.xyz)}
2295
2286
  {@const n1 = Math.hypot(v1[0], v1[1], v1[2])}