matterviz 0.4.0 → 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 (527) 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 +132 -219
  7. package/dist/brillouin/BrillouinZone.svelte.d.ts +9 -20
  8. package/dist/brillouin/BrillouinZoneControls.svelte +3 -6
  9. package/dist/brillouin/BrillouinZoneExportPane.svelte +47 -96
  10. package/dist/brillouin/BrillouinZoneExportPane.svelte.d.ts +1 -1
  11. package/dist/brillouin/BrillouinZoneInfoPane.svelte +15 -42
  12. package/dist/brillouin/BrillouinZoneInfoPane.svelte.d.ts +2 -3
  13. package/dist/brillouin/BrillouinZoneScene.svelte +63 -230
  14. package/dist/brillouin/BrillouinZoneScene.svelte.d.ts +3 -23
  15. package/dist/brillouin/ReciprocalVectors.svelte +39 -0
  16. package/dist/brillouin/ReciprocalVectors.svelte.d.ts +9 -0
  17. package/dist/brillouin/compute.d.ts +2 -0
  18. package/dist/brillouin/compute.js +80 -77
  19. package/dist/brillouin/geometry.d.ts +8 -0
  20. package/dist/brillouin/geometry.js +57 -0
  21. package/dist/brillouin/index.d.ts +2 -0
  22. package/dist/brillouin/index.js +2 -0
  23. package/dist/brillouin/types.d.ts +2 -2
  24. package/dist/chempot-diagram/ChemPotDiagram.svelte +33 -38
  25. package/dist/chempot-diagram/ChemPotDiagram.svelte.d.ts +1 -1
  26. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +143 -246
  27. package/dist/chempot-diagram/ChemPotDiagram2D.svelte.d.ts +4 -1
  28. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +652 -976
  29. package/dist/chempot-diagram/ChemPotDiagram3D.svelte.d.ts +7 -1
  30. package/dist/chempot-diagram/async-compute.svelte.js +1 -1
  31. package/dist/chempot-diagram/color.d.ts +3 -6
  32. package/dist/chempot-diagram/color.js +5 -5
  33. package/dist/chempot-diagram/compute.d.ts +3 -3
  34. package/dist/chempot-diagram/compute.js +3 -1
  35. package/dist/chempot-diagram/controls-state.svelte.d.ts +10 -0
  36. package/dist/chempot-diagram/controls-state.svelte.js +42 -0
  37. package/dist/chempot-diagram/export.d.ts +47 -0
  38. package/dist/chempot-diagram/export.js +133 -0
  39. package/dist/chempot-diagram/index.d.ts +1 -0
  40. package/dist/chempot-diagram/index.js +1 -0
  41. package/dist/chempot-diagram/pointer.d.ts +0 -10
  42. package/dist/chempot-diagram/pointer.js +4 -4
  43. package/dist/chempot-diagram/types.d.ts +3 -3
  44. package/dist/colors/index.d.ts +0 -4
  45. package/dist/colors/index.js +21 -22
  46. package/dist/composition/BarChart.svelte +37 -31
  47. package/dist/composition/BubbleChart.svelte +17 -17
  48. package/dist/composition/Composition.svelte +7 -11
  49. package/dist/composition/Formula.svelte +18 -25
  50. package/dist/composition/FormulaFilter.svelte +96 -98
  51. package/dist/composition/PieChart.svelte +60 -46
  52. package/dist/composition/chem-sys.js +3 -2
  53. package/dist/composition/format.js +4 -3
  54. package/dist/composition/index.d.ts +2 -0
  55. package/dist/composition/index.js +14 -0
  56. package/dist/composition/parse.d.ts +2 -3
  57. package/dist/composition/parse.js +27 -28
  58. package/dist/constants.js +11 -9
  59. package/dist/controls.d.ts +1 -0
  60. package/dist/controls.js +0 -1
  61. package/dist/convex-hull/ConvexHull.svelte +20 -25
  62. package/dist/convex-hull/ConvexHull.svelte.d.ts +1 -4
  63. package/dist/convex-hull/ConvexHull2D.svelte +176 -256
  64. package/dist/convex-hull/ConvexHull2D.svelte.d.ts +1 -1
  65. package/dist/convex-hull/ConvexHull3D.svelte +269 -804
  66. package/dist/convex-hull/ConvexHull3D.svelte.d.ts +1 -1
  67. package/dist/convex-hull/ConvexHull4D.svelte +236 -755
  68. package/dist/convex-hull/ConvexHull4D.svelte.d.ts +1 -1
  69. package/dist/convex-hull/ConvexHullChrome.svelte +264 -0
  70. package/dist/convex-hull/ConvexHullChrome.svelte.d.ts +30 -0
  71. package/dist/convex-hull/ConvexHullControls.svelte +128 -49
  72. package/dist/convex-hull/ConvexHullControls.svelte.d.ts +7 -6
  73. package/dist/convex-hull/ConvexHullInfoPane.svelte +18 -5
  74. package/dist/convex-hull/ConvexHullInfoPane.svelte.d.ts +6 -5
  75. package/dist/convex-hull/ConvexHullStats.svelte +210 -351
  76. package/dist/convex-hull/ConvexHullStats.svelte.d.ts +3 -1
  77. package/dist/convex-hull/ConvexHullTooltip.svelte +16 -14
  78. package/dist/convex-hull/ConvexHullTooltip.svelte.d.ts +2 -1
  79. package/dist/convex-hull/GasPressureControls.svelte +5 -12
  80. package/dist/convex-hull/StructurePopup.svelte +17 -21
  81. package/dist/convex-hull/StructurePopup.svelte.d.ts +2 -0
  82. package/dist/convex-hull/TemperatureSlider.svelte +4 -10
  83. package/dist/convex-hull/barycentric-coords.d.ts +2 -4
  84. package/dist/convex-hull/barycentric-coords.js +6 -33
  85. package/dist/convex-hull/canvas-interactions.svelte.d.ts +79 -0
  86. package/dist/convex-hull/canvas-interactions.svelte.js +279 -0
  87. package/dist/convex-hull/helpers.d.ts +47 -17
  88. package/dist/convex-hull/helpers.js +186 -101
  89. package/dist/convex-hull/hull-state.svelte.d.ts +44 -0
  90. package/dist/convex-hull/hull-state.svelte.js +124 -0
  91. package/dist/convex-hull/index.d.ts +9 -7
  92. package/dist/convex-hull/index.js +7 -2
  93. package/dist/convex-hull/thermodynamics.js +109 -937
  94. package/dist/convex-hull/types.d.ts +12 -4
  95. package/dist/convex-hull/types.js +12 -0
  96. package/dist/coordination/CoordinationBarPlot.svelte +41 -43
  97. package/dist/coordination/CoordinationBarPlot.svelte.d.ts +1 -1
  98. package/dist/coordination/calc-coordination.d.ts +1 -1
  99. package/dist/coordination/calc-coordination.js +28 -25
  100. package/dist/element/BohrAtom.svelte +6 -8
  101. package/dist/element/ElementHeading.svelte +4 -1
  102. package/dist/element/ElementPhoto.svelte +11 -9
  103. package/dist/element/ElementStats.svelte +12 -10
  104. package/dist/element/ElementTile.svelte +24 -29
  105. package/dist/element/ElementTile.svelte.d.ts +1 -1
  106. package/dist/element/Nucleus.svelte +4 -2
  107. package/dist/element/data.d.ts +1 -2
  108. package/dist/element/helpers.d.ts +4 -0
  109. package/dist/element/helpers.js +18 -0
  110. package/dist/element/index.d.ts +1 -0
  111. package/dist/element/index.js +3 -0
  112. package/dist/feedback/DragOverlay.svelte +5 -3
  113. package/dist/feedback/DragOverlay.svelte.d.ts +2 -1
  114. package/dist/feedback/StatusMessage.svelte +20 -7
  115. package/dist/fermi-surface/FermiSlice.svelte +21 -18
  116. package/dist/fermi-surface/FermiSurface.svelte +78 -165
  117. package/dist/fermi-surface/FermiSurface.svelte.d.ts +9 -20
  118. package/dist/fermi-surface/FermiSurfaceControls.svelte +6 -20
  119. package/dist/fermi-surface/FermiSurfaceScene.svelte +93 -272
  120. package/dist/fermi-surface/FermiSurfaceScene.svelte.d.ts +3 -23
  121. package/dist/fermi-surface/FermiSurfaceTooltip.svelte +9 -6
  122. package/dist/fermi-surface/compute.js +14 -13
  123. package/dist/fermi-surface/export.js +4 -15
  124. package/dist/fermi-surface/index.d.ts +0 -1
  125. package/dist/fermi-surface/index.js +0 -1
  126. package/dist/fermi-surface/parse.d.ts +1 -1
  127. package/dist/fermi-surface/parse.js +84 -97
  128. package/dist/fermi-surface/types.d.ts +2 -2
  129. package/dist/heatmap-matrix/HeatmapMatrix.svelte +161 -185
  130. package/dist/heatmap-matrix/HeatmapMatrix.svelte.d.ts +6 -5
  131. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte +12 -9
  132. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +5 -5
  133. package/dist/heatmap-matrix/index.d.ts +8 -2
  134. package/dist/icons.d.ts +4 -0
  135. package/dist/icons.js +4 -0
  136. package/dist/index.d.ts +1 -0
  137. package/dist/index.js +1 -0
  138. package/dist/io/ExportPane.svelte +170 -0
  139. package/dist/io/ExportPane.svelte.d.ts +17 -0
  140. package/dist/io/decompress.d.ts +2 -1
  141. package/dist/io/decompress.js +22 -36
  142. package/dist/io/export.d.ts +9 -1
  143. package/dist/io/export.js +37 -29
  144. package/dist/io/fetch.d.ts +2 -4
  145. package/dist/io/fetch.js +0 -11
  146. package/dist/io/file-drop.d.ts +7 -0
  147. package/dist/io/file-drop.js +13 -0
  148. package/dist/io/index.d.ts +1 -0
  149. package/dist/io/index.js +3 -0
  150. package/dist/io/is-binary.d.ts +9 -0
  151. package/dist/io/is-binary.js +45 -1
  152. package/dist/io/types.d.ts +13 -0
  153. package/dist/io/url-drop.js +13 -25
  154. package/dist/isosurface/Isosurface.svelte +17 -23
  155. package/dist/isosurface/IsosurfaceControls.svelte +37 -42
  156. package/dist/isosurface/parse-vaspwave.d.ts +3 -0
  157. package/dist/isosurface/parse-vaspwave.js +138 -0
  158. package/dist/isosurface/parse.js +50 -48
  159. package/dist/labels.d.ts +6 -2
  160. package/dist/labels.js +44 -14
  161. package/dist/layout/FullscreenButton.svelte +34 -0
  162. package/dist/layout/FullscreenButton.svelte.d.ts +10 -0
  163. package/dist/layout/FullscreenToggle.svelte +12 -12
  164. package/dist/layout/InfoCard.svelte +7 -18
  165. package/dist/layout/InfoTag.svelte +1 -3
  166. package/dist/layout/NumberRangeInput.svelte +46 -0
  167. package/dist/layout/NumberRangeInput.svelte.d.ts +12 -0
  168. package/dist/layout/PropertyFilter.svelte +2 -6
  169. package/dist/layout/SettingsSection.svelte +21 -17
  170. package/dist/layout/SubpageGrid.svelte +3 -1
  171. package/dist/layout/ViewerChrome.svelte +115 -0
  172. package/dist/layout/ViewerChrome.svelte.d.ts +18 -0
  173. package/dist/layout/fullscreen.d.ts +4 -0
  174. package/dist/layout/fullscreen.js +3 -5
  175. package/dist/layout/fullscreen.svelte.d.ts +8 -0
  176. package/dist/layout/fullscreen.svelte.js +36 -0
  177. package/dist/layout/index.d.ts +4 -0
  178. package/dist/layout/index.js +4 -0
  179. package/dist/layout/json-tree/JsonNode.svelte +8 -11
  180. package/dist/layout/json-tree/JsonTree.svelte +53 -77
  181. package/dist/layout/json-tree/JsonValue.svelte +7 -15
  182. package/dist/layout/json-tree/utils.js +14 -23
  183. package/dist/math.d.ts +9 -3
  184. package/dist/math.js +37 -21
  185. package/dist/overlays/ContextMenu.svelte +4 -2
  186. package/dist/overlays/CopyButton.svelte +11 -5
  187. package/dist/overlays/DragControlTab.svelte +6 -1
  188. package/dist/overlays/DraggablePane.svelte +81 -26
  189. package/dist/overlays/DraggablePane.svelte.d.ts +1 -0
  190. package/dist/overlays/GlassChip.svelte +30 -0
  191. package/dist/overlays/GlassChip.svelte.d.ts +8 -0
  192. package/dist/overlays/InfoPaneCards.svelte +11 -23
  193. package/dist/overlays/index.d.ts +11 -0
  194. package/dist/overlays/index.js +26 -0
  195. package/dist/overlays/portal.d.ts +2 -0
  196. package/dist/overlays/portal.js +14 -0
  197. package/dist/periodic-table/PeriodicTable.svelte +115 -146
  198. package/dist/periodic-table/PeriodicTable.svelte.d.ts +5 -3
  199. package/dist/periodic-table/PeriodicTableControls.svelte +11 -63
  200. package/dist/periodic-table/TableInset.svelte +9 -4
  201. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +50 -71
  202. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte.d.ts +6 -8
  203. package/dist/phase-diagram/PhaseDiagramControls.svelte +73 -96
  204. package/dist/phase-diagram/PhaseDiagramControls.svelte.d.ts +4 -3
  205. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte +4 -2
  206. package/dist/phase-diagram/PhaseDiagramEditorPane.svelte.d.ts +2 -3
  207. package/dist/phase-diagram/PhaseDiagramExportPane.svelte +49 -134
  208. package/dist/phase-diagram/PhaseDiagramExportPane.svelte.d.ts +3 -4
  209. package/dist/phase-diagram/PhaseDiagramTooltip.svelte +54 -58
  210. package/dist/phase-diagram/TdbInfoPanel.svelte +21 -15
  211. package/dist/phase-diagram/build-diagram.js +3 -3
  212. package/dist/phase-diagram/colors.js +1 -1
  213. package/dist/phase-diagram/diagram-input.d.ts +1 -2
  214. package/dist/phase-diagram/parse.d.ts +2 -1
  215. package/dist/phase-diagram/parse.js +15 -15
  216. package/dist/phase-diagram/svg-to-diagram.js +21 -15
  217. package/dist/phase-diagram/utils.js +2 -3
  218. package/dist/plot/bar/BarPlot.svelte +494 -692
  219. package/dist/plot/bar/BarPlot.svelte.d.ts +9 -15
  220. package/dist/plot/bar/BarPlotControls.svelte +1 -3
  221. package/dist/plot/bar/BarPlotControls.svelte.d.ts +5 -7
  222. package/dist/plot/bar/SpacegroupBarPlot.svelte +31 -37
  223. package/dist/plot/bar/data.d.ts +5 -4
  224. package/dist/plot/bar/data.js +2 -1
  225. package/dist/plot/box/BoxPlot.svelte +361 -439
  226. package/dist/plot/box/BoxPlot.svelte.d.ts +6 -3
  227. package/dist/plot/box/BoxPlotControls.svelte.d.ts +1 -1
  228. package/dist/plot/box/Violin.svelte.d.ts +2 -1
  229. package/dist/plot/box/box-plot.d.ts +3 -2
  230. package/dist/plot/box/box-plot.js +6 -3
  231. package/dist/plot/box/kde.d.ts +2 -1
  232. package/dist/plot/box/kde.js +4 -4
  233. package/dist/plot/core/auto-place.d.ts +1 -1
  234. package/dist/plot/core/auto-place.js +5 -2
  235. package/dist/plot/core/axis-utils.d.ts +12 -7
  236. package/dist/plot/core/axis-utils.js +48 -48
  237. package/dist/plot/core/components/AxisLabel.svelte +82 -20
  238. package/dist/plot/core/components/AxisLabel.svelte.d.ts +1 -0
  239. package/dist/plot/core/components/ColorBar.svelte +106 -111
  240. package/dist/plot/core/components/ColorBar.svelte.d.ts +5 -4
  241. package/dist/plot/core/components/ColorScaleSelect.svelte +1 -1
  242. package/dist/plot/core/components/FillArea.svelte +10 -17
  243. package/dist/plot/core/components/HierarchyControls.svelte +249 -0
  244. package/dist/plot/{sunburst/SunburstControls.svelte.d.ts → core/components/HierarchyControls.svelte.d.ts} +12 -8
  245. package/dist/plot/core/components/InteractiveAxisLabel.svelte +1 -1
  246. package/dist/plot/core/components/InteractiveAxisLabel.svelte.d.ts +1 -1
  247. package/dist/plot/core/components/Line.svelte +21 -11
  248. package/dist/plot/core/components/Line.svelte.d.ts +5 -2
  249. package/dist/plot/core/components/PlotAxis.svelte +42 -18
  250. package/dist/plot/core/components/PlotAxis.svelte.d.ts +2 -1
  251. package/dist/plot/core/components/PlotControls.svelte +65 -41
  252. package/dist/plot/core/components/PlotControls.svelte.d.ts +1 -1
  253. package/dist/plot/core/components/PlotLegend.svelte +31 -35
  254. package/dist/plot/core/components/PlotMarginals.svelte +616 -0
  255. package/dist/plot/core/components/PlotMarginals.svelte.d.ts +15 -0
  256. package/dist/plot/core/components/PlotTooltip.svelte +1 -1
  257. package/dist/plot/core/components/PortalSelect.svelte +4 -8
  258. package/dist/plot/core/components/ReferenceLine.svelte +27 -24
  259. package/dist/plot/core/components/ReferenceLine3D.svelte +56 -59
  260. package/dist/plot/core/components/ReferenceLine3D.svelte.d.ts +4 -4
  261. package/dist/plot/core/components/ReferencePlane.svelte +9 -10
  262. package/dist/plot/core/components/ReferencePlane.svelte.d.ts +4 -4
  263. package/dist/plot/core/components/ZeroLines.svelte +4 -8
  264. package/dist/plot/core/components/ZoomRect.svelte +1 -2
  265. package/dist/plot/core/components/index.d.ts +1 -0
  266. package/dist/plot/core/components/index.js +1 -0
  267. package/dist/plot/core/data-cleaning-signal.d.ts +46 -0
  268. package/dist/plot/core/data-cleaning-signal.js +467 -0
  269. package/dist/plot/core/data-cleaning.d.ts +2 -42
  270. package/dist/plot/core/data-cleaning.js +29 -476
  271. package/dist/plot/core/data-transform.js +9 -5
  272. package/dist/plot/core/fill-utils.d.ts +7 -4
  273. package/dist/plot/core/fill-utils.js +34 -9
  274. package/dist/plot/core/index.d.ts +1 -0
  275. package/dist/plot/core/index.js +1 -0
  276. package/dist/plot/core/interactions.d.ts +5 -1
  277. package/dist/plot/core/interactions.js +14 -2
  278. package/dist/plot/core/layout.d.ts +3 -1
  279. package/dist/plot/core/layout.js +22 -13
  280. package/dist/plot/core/marginals.d.ts +170 -0
  281. package/dist/plot/core/marginals.js +436 -0
  282. package/dist/plot/core/pan-zoom.svelte.d.ts +35 -0
  283. package/dist/plot/core/pan-zoom.svelte.js +221 -0
  284. package/dist/plot/core/placed-tween.svelte.d.ts +21 -0
  285. package/dist/plot/core/placed-tween.svelte.js +68 -0
  286. package/dist/plot/core/reference-line.d.ts +10 -10
  287. package/dist/plot/core/reference-line.js +7 -7
  288. package/dist/plot/core/scales.d.ts +40 -25
  289. package/dist/plot/core/scales.js +49 -21
  290. package/dist/plot/core/svg.d.ts +2 -1
  291. package/dist/plot/core/types/fills.d.ts +101 -0
  292. package/dist/plot/core/types/fills.js +15 -0
  293. package/dist/plot/core/types/plot-3d.d.ts +87 -0
  294. package/dist/plot/core/types/plot-3d.js +4 -0
  295. package/dist/plot/core/types/reference-lines.d.ts +140 -0
  296. package/dist/plot/core/types/reference-lines.js +9 -0
  297. package/dist/plot/core/types.d.ts +47 -331
  298. package/dist/plot/core/types.js +23 -20
  299. package/dist/plot/core/utils/hierarchy-chart.d.ts +51 -0
  300. package/dist/plot/core/utils/hierarchy-chart.js +179 -0
  301. package/dist/plot/core/utils/hierarchy-labels.d.ts +20 -0
  302. package/dist/plot/core/utils/hierarchy-labels.js +99 -0
  303. package/dist/plot/core/utils/label-placement.d.ts +1 -1
  304. package/dist/plot/core/utils/label-placement.js +5 -5
  305. package/dist/plot/core/utils.d.ts +2 -1
  306. package/dist/plot/histogram/Histogram.svelte +373 -631
  307. package/dist/plot/histogram/Histogram.svelte.d.ts +7 -7
  308. package/dist/plot/histogram/HistogramControls.svelte +30 -28
  309. package/dist/plot/histogram/HistogramControls.svelte.d.ts +1 -1
  310. package/dist/plot/histogram/histogram.d.ts +31 -0
  311. package/dist/plot/histogram/histogram.js +70 -0
  312. package/dist/plot/histogram/index.d.ts +1 -0
  313. package/dist/plot/histogram/index.js +1 -0
  314. package/dist/plot/index.d.ts +1 -0
  315. package/dist/plot/index.js +1 -0
  316. package/dist/plot/sankey/Sankey.svelte +79 -73
  317. package/dist/plot/sankey/Sankey.svelte.d.ts +3 -3
  318. package/dist/plot/sankey/SankeyControls.svelte +22 -16
  319. package/dist/plot/sankey/sankey.js +3 -1
  320. package/dist/plot/scatter/BinnedScatterPlot.svelte +264 -112
  321. package/dist/plot/scatter/BinnedScatterPlot.svelte.d.ts +11 -4
  322. package/dist/plot/scatter/ElementScatter.svelte +4 -1
  323. package/dist/plot/scatter/ScatterPlot.svelte +623 -821
  324. package/dist/plot/scatter/ScatterPlot.svelte.d.ts +9 -15
  325. package/dist/plot/scatter/ScatterPlotControls.svelte +18 -19
  326. package/dist/plot/scatter/ScatterPlotControls.svelte.d.ts +1 -1
  327. package/dist/plot/scatter/ScatterPoint.svelte +16 -9
  328. package/dist/plot/scatter/binned-scatter-types.d.ts +6 -12
  329. package/dist/plot/scatter/binned-scatter-types.js +6 -1
  330. package/dist/plot/scatter/index.d.ts +1 -1
  331. package/dist/plot/scatter/scatter-data.d.ts +1 -1
  332. package/dist/plot/scatter/scatter-data.js +8 -7
  333. package/dist/plot/scatter-3d/ScatterPlot3D.svelte +40 -60
  334. package/dist/plot/scatter-3d/ScatterPlot3D.svelte.d.ts +6 -14
  335. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte +17 -15
  336. package/dist/plot/scatter-3d/ScatterPlot3DControls.svelte.d.ts +5 -5
  337. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte +232 -234
  338. package/dist/plot/scatter-3d/ScatterPlot3DScene.svelte.d.ts +5 -14
  339. package/dist/plot/scatter-3d/Surface3D.svelte +9 -17
  340. package/dist/plot/scatter-3d/Surface3D.svelte.d.ts +4 -3
  341. package/dist/plot/sunburst/Sunburst.svelte +256 -338
  342. package/dist/plot/sunburst/Sunburst.svelte.d.ts +4 -3
  343. package/dist/plot/sunburst/index.d.ts +1 -1
  344. package/dist/plot/sunburst/index.js +2 -1
  345. package/dist/plot/sunburst/render.d.ts +2 -2
  346. package/dist/plot/sunburst/render.js +47 -22
  347. package/dist/plot/sunburst/sunburst.d.ts +5 -1
  348. package/dist/plot/sunburst/sunburst.js +14 -4
  349. package/dist/plot/treemap/Treemap.svelte +907 -0
  350. package/dist/plot/treemap/Treemap.svelte.d.ts +84 -0
  351. package/dist/plot/treemap/index.d.ts +3 -0
  352. package/dist/plot/treemap/index.js +4 -0
  353. package/dist/plot/treemap/treemap.d.ts +26 -0
  354. package/dist/plot/treemap/treemap.js +99 -0
  355. package/dist/rdf/RdfPlot.svelte +22 -26
  356. package/dist/rdf/RdfPlot.svelte.d.ts +1 -1
  357. package/dist/rdf/calc-rdf.js +4 -8
  358. package/dist/sanitize.js +14 -3
  359. package/dist/scene/SceneCamera.svelte +62 -0
  360. package/dist/scene/SceneCamera.svelte.d.ts +19 -0
  361. package/dist/scene/bind-renderer.svelte.d.ts +2 -0
  362. package/dist/scene/bind-renderer.svelte.js +22 -0
  363. package/dist/scene/index.d.ts +4 -0
  364. package/dist/scene/index.js +5 -0
  365. package/dist/scene/props.js +52 -0
  366. package/dist/scene/types.d.ts +26 -0
  367. package/dist/scene/types.js +1 -0
  368. package/dist/settings.d.ts +33 -11
  369. package/dist/settings.js +136 -51
  370. package/dist/spectral/Bands.svelte +231 -237
  371. package/dist/spectral/Bands.svelte.d.ts +3 -2
  372. package/dist/spectral/BandsAndDos.svelte +29 -29
  373. package/dist/spectral/BrillouinBandsDos.svelte +21 -29
  374. package/dist/spectral/Dos.svelte +75 -77
  375. package/dist/spectral/Dos.svelte.d.ts +2 -1
  376. package/dist/spectral/helpers.d.ts +9 -9
  377. package/dist/spectral/helpers.js +66 -70
  378. package/dist/state.svelte.d.ts +0 -7
  379. package/dist/state.svelte.js +5 -24
  380. package/dist/structure/Arrow.svelte +6 -21
  381. package/dist/structure/ArrowInstances.svelte +160 -0
  382. package/dist/structure/ArrowInstances.svelte.d.ts +16 -0
  383. package/dist/structure/AtomLegend.svelte +115 -75
  384. package/dist/structure/AtomLegend.svelte.d.ts +1 -1
  385. package/dist/structure/Bond.svelte +31 -23
  386. package/dist/structure/CanvasTooltip.svelte +16 -3
  387. package/dist/structure/CellSelect.svelte +22 -29
  388. package/dist/structure/CellSelect.svelte.d.ts +2 -1
  389. package/dist/structure/Cylinder.svelte +24 -40
  390. package/dist/structure/InstancedAtoms.svelte +114 -0
  391. package/dist/structure/InstancedAtoms.svelte.d.ts +15 -0
  392. package/dist/structure/Lattice.svelte +85 -149
  393. package/dist/structure/SiteLabels.svelte +122 -0
  394. package/dist/structure/SiteLabels.svelte.d.ts +18 -0
  395. package/dist/structure/Structure.svelte +725 -735
  396. package/dist/structure/Structure.svelte.d.ts +8 -16
  397. package/dist/structure/StructureCarousel.svelte +630 -0
  398. package/dist/structure/StructureCarousel.svelte.d.ts +19 -0
  399. package/dist/structure/StructureControls.svelte +298 -325
  400. package/dist/structure/StructureControls.svelte.d.ts +5 -3
  401. package/dist/structure/StructureExportPane.svelte +62 -168
  402. package/dist/structure/StructureExportPane.svelte.d.ts +4 -5
  403. package/dist/structure/StructureInfoPane.svelte +77 -69
  404. package/dist/structure/StructureInfoPane.svelte.d.ts +6 -6
  405. package/dist/structure/StructureScene.svelte +758 -600
  406. package/dist/structure/StructureScene.svelte.d.ts +24 -20
  407. package/dist/structure/StructureViewport.svelte +386 -0
  408. package/dist/structure/StructureViewport.svelte.d.ts +63 -0
  409. package/dist/structure/atom-label-placement.d.ts +18 -0
  410. package/dist/structure/atom-label-placement.js +115 -0
  411. package/dist/structure/atom-properties.d.ts +3 -1
  412. package/dist/structure/atom-properties.js +115 -54
  413. package/dist/structure/bond-order-perception.js +2 -4
  414. package/dist/structure/bonding.d.ts +12 -2
  415. package/dist/structure/bonding.js +172 -73
  416. package/dist/structure/edit-history.d.ts +6 -0
  417. package/dist/structure/edit-history.js +27 -0
  418. package/dist/structure/export.d.ts +25 -4
  419. package/dist/structure/export.js +103 -153
  420. package/dist/structure/format-detect.d.ts +1 -0
  421. package/dist/structure/format-detect.js +22 -0
  422. package/dist/structure/geometry.d.ts +9 -0
  423. package/dist/structure/geometry.js +27 -0
  424. package/dist/structure/index.d.ts +18 -1
  425. package/dist/structure/index.js +15 -2
  426. package/dist/structure/measure.d.ts +1 -1
  427. package/dist/structure/measure.js +6 -12
  428. package/dist/structure/parse.d.ts +5 -3
  429. package/dist/structure/parse.js +551 -567
  430. package/dist/structure/partial-occupancy.d.ts +0 -1
  431. package/dist/structure/partial-occupancy.js +5 -3
  432. package/dist/structure/pbc.d.ts +1 -1
  433. package/dist/structure/pbc.js +204 -51
  434. package/dist/structure/polyhedra.d.ts +40 -0
  435. package/dist/structure/polyhedra.js +587 -0
  436. package/dist/structure/site.d.ts +4 -0
  437. package/dist/structure/site.js +1 -0
  438. package/dist/structure/supercell.d.ts +1 -2
  439. package/dist/structure/supercell.js +10 -15
  440. package/dist/structure/validation.js +5 -6
  441. package/dist/symmetry/SymmetryElementControls.svelte +69 -0
  442. package/dist/symmetry/SymmetryElementControls.svelte.d.ts +9 -0
  443. package/dist/symmetry/SymmetryElements.svelte +350 -0
  444. package/dist/symmetry/SymmetryElements.svelte.d.ts +24 -0
  445. package/dist/symmetry/SymmetryStats.svelte +142 -49
  446. package/dist/symmetry/WyckoffTable.svelte +58 -16
  447. package/dist/symmetry/WyckoffTable.svelte.d.ts +3 -0
  448. package/dist/symmetry/cell-transform.js +8 -14
  449. package/dist/symmetry/index.d.ts +15 -4
  450. package/dist/symmetry/index.js +308 -81
  451. package/dist/symmetry/spacegroups.d.ts +5 -1
  452. package/dist/symmetry/spacegroups.js +18 -3
  453. package/dist/symmetry/symmetry-elements.d.ts +33 -0
  454. package/dist/symmetry/symmetry-elements.js +521 -0
  455. package/dist/symmetry/wyckoff-db.d.ts +9 -0
  456. package/dist/symmetry/wyckoff-db.js +87 -0
  457. package/dist/table/HeatmapTable.svelte +1104 -378
  458. package/dist/table/HeatmapTable.svelte.d.ts +12 -2
  459. package/dist/table/ToggleMenu.svelte +5 -12
  460. package/dist/table/index.d.ts +21 -6
  461. package/dist/table/index.js +34 -21
  462. package/dist/theme/ThemeControl.svelte +16 -8
  463. package/dist/theme/index.d.ts +1 -5
  464. package/dist/theme/index.js +4 -10
  465. package/dist/theme/themes.mjs +14 -7
  466. package/dist/tooltip/KCoords.svelte +4 -1
  467. package/dist/tooltip/TooltipContent.svelte +2 -5
  468. package/dist/trajectory/Trajectory.svelte +556 -352
  469. package/dist/trajectory/Trajectory.svelte.d.ts +10 -22
  470. package/dist/trajectory/TrajectoryError.svelte +11 -8
  471. package/dist/trajectory/TrajectoryExportPane.svelte +28 -54
  472. package/dist/trajectory/TrajectoryExportPane.svelte.d.ts +4 -5
  473. package/dist/trajectory/TrajectoryInfoPane.svelte +88 -99
  474. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +3 -2
  475. package/dist/trajectory/constants.js +6 -2
  476. package/dist/trajectory/extract.js +73 -71
  477. package/dist/trajectory/format-detect.d.ts +0 -1
  478. package/dist/trajectory/format-detect.js +13 -17
  479. package/dist/trajectory/frame-reader.d.ts +2 -1
  480. package/dist/trajectory/frame-reader.js +74 -136
  481. package/dist/trajectory/helpers.d.ts +12 -2
  482. package/dist/trajectory/helpers.js +68 -37
  483. package/dist/trajectory/index.d.ts +1 -0
  484. package/dist/trajectory/index.js +3 -0
  485. package/dist/trajectory/parse/ase.d.ts +9 -1
  486. package/dist/trajectory/parse/ase.js +47 -32
  487. package/dist/trajectory/parse/diagnostics.d.ts +3 -0
  488. package/dist/trajectory/parse/diagnostics.js +14 -0
  489. package/dist/trajectory/parse/h5-utils.d.ts +13 -0
  490. package/dist/trajectory/parse/h5-utils.js +109 -0
  491. package/dist/trajectory/parse/hdf5.d.ts +1 -1
  492. package/dist/trajectory/parse/hdf5.js +116 -106
  493. package/dist/trajectory/parse/index.d.ts +3 -4
  494. package/dist/trajectory/parse/index.js +68 -119
  495. package/dist/trajectory/parse/lammps.d.ts +0 -2
  496. package/dist/trajectory/parse/lammps.js +14 -16
  497. package/dist/trajectory/parse/pymatgen.d.ts +2 -0
  498. package/dist/trajectory/parse/pymatgen.js +74 -0
  499. package/dist/trajectory/parse/vasp.js +13 -9
  500. package/dist/trajectory/parse/vaspout-electronic.d.ts +13 -0
  501. package/dist/trajectory/parse/vaspout-electronic.js +207 -0
  502. package/dist/trajectory/parse/vaspout-h5.d.ts +5 -0
  503. package/dist/trajectory/parse/vaspout-h5.js +284 -0
  504. package/dist/trajectory/parse/xyz.d.ts +9 -21
  505. package/dist/trajectory/parse/xyz.js +38 -39
  506. package/dist/trajectory/plotting.d.ts +8 -10
  507. package/dist/trajectory/plotting.js +102 -127
  508. package/dist/utils.d.ts +4 -0
  509. package/dist/utils.js +8 -1
  510. package/dist/xrd/XrdPlot.svelte +56 -85
  511. package/dist/xrd/broadening.d.ts +2 -1
  512. package/dist/xrd/broadening.js +2 -1
  513. package/dist/xrd/calc-xrd.js +8 -15
  514. package/dist/xrd/index.d.ts +2 -2
  515. package/dist/xrd/parse.d.ts +0 -1
  516. package/dist/xrd/parse.js +56 -73
  517. package/package.json +77 -36
  518. package/dist/api/mp.d.ts +0 -6
  519. package/dist/api/mp.js +0 -22
  520. package/dist/element/data.json +0 -11864
  521. package/dist/fermi-surface/marching-cubes.d.ts +0 -2
  522. package/dist/fermi-surface/marching-cubes.js +0 -2
  523. package/dist/plot/core/hover-lock.svelte.d.ts +0 -14
  524. package/dist/plot/core/hover-lock.svelte.js +0 -45
  525. package/dist/plot/sunburst/SunburstControls.svelte +0 -200
  526. package/dist/structure/label-placement.d.ts +0 -14
  527. package/dist/structure/label-placement.js +0 -72
@@ -1,13 +1,12 @@
1
1
  <script lang="ts">
2
2
  import type { ColorSchemeName } from '../colors'
3
3
  import { ELEMENT_COLOR_SCHEMES } from '../colors'
4
- import type { ShowControlsProp } from '../controls'
5
- import { normalize_show_controls } from '../controls'
6
- import type { ElementSymbol } from '../element'
4
+ import { normalize_show_controls, type ShowControlsProp } from '../controls'
5
+ import { coerce_elem_symbol, type ElementSymbol } from '../element'
7
6
  import { StatusMessage } from '../feedback'
8
7
  import Spinner from '../feedback/Spinner.svelte'
9
8
  import Icon from '../Icon.svelte'
10
- import { create_file_drop_handler, load_from_url } from '../io'
9
+ import { create_file_drop_handler, drag_over_handlers, load_from_url } from '../io'
11
10
  import { forward_window_keydown, handle_and_prevent } from '../keyboard'
12
11
  import { parse_volumetric_file } from '../isosurface/parse'
13
12
  import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
@@ -16,12 +15,13 @@
16
15
  DEFAULT_ISOSURFACE_SETTINGS,
17
16
  tile_volumetric_data,
18
17
  } from '../isosurface/types'
19
- import { ELEM_SYMBOLS } from '../labels'
20
- import { set_fullscreen_bg, toggle_fullscreen } from '../layout'
18
+ import { type FullscreenToggleProp, toggle_fullscreen, ViewerChrome } from '../layout'
19
+ import { sync_fullscreen } from '../layout/fullscreen.svelte'
21
20
  import type { Vec3 } from '../math'
22
21
  import { create_cart_to_frac, create_frac_to_cart } from '../math'
23
22
  import { DEFAULTS } from '../settings'
24
23
  import { colors } from '../state.svelte'
24
+ import StructureViewport from './StructureViewport.svelte'
25
25
  import type {
26
26
  AnyStructure,
27
27
  BondEditMode,
@@ -29,13 +29,16 @@
29
29
  Crystal,
30
30
  MeasureMode,
31
31
  StructureBond,
32
+ StructureView,
32
33
  } from './'
33
34
  import {
35
+ DEFAULT_STRUCTURE_VIEWS,
34
36
  default_vector_configs,
35
37
  get_element_counts,
36
38
  get_pbc_image_sites,
37
39
  get_structure_vector_keys,
38
40
  } from './'
41
+ import { push_edit, step_history } from './edit-history'
39
42
  import { wrap_to_unit_cell } from './pbc'
40
43
  import {
41
44
  is_valid_supercell_input,
@@ -46,13 +49,12 @@
46
49
  import * as symmetry from '../symmetry'
47
50
  import { transform_cell } from '../symmetry'
48
51
  import type { MoyoDataset } from '@spglib/moyo-wasm'
49
- import { Canvas } from '@threlte/core'
50
52
  import type { ComponentProps, Snippet } from 'svelte'
51
53
  import { untrack } from 'svelte'
52
54
  import { click_outside, tooltip } from 'svelte-multiselect/attachments'
53
55
  import type { HTMLAttributes } from 'svelte/elements'
54
56
  import { SvelteMap, SvelteSet } from 'svelte/reactivity'
55
- import type { Camera, OrthographicCamera, Scene } from 'three'
57
+ import type { Camera, Scene } from 'three'
56
58
  import type { AtomColorConfig } from './atom-properties'
57
59
  import { get_property_colors } from './atom-properties'
58
60
  import AtomLegend from './AtomLegend.svelte'
@@ -60,7 +62,11 @@
60
62
  import { BOND_ORDER_OPTIONS, merge_bond_edits, remap_bonds_after_deletion } from './bonding'
61
63
  import type { StructureHandlerData } from './index'
62
64
  import { MAX_SELECTED_SITES } from './measure'
63
- import { normalize_fractional_coords, parse_any_structure } from './parse'
65
+ import {
66
+ ensure_lattice_params,
67
+ normalize_fractional_coords,
68
+ parse_any_structure,
69
+ } from './parse'
64
70
  import StructureControls from './StructureControls.svelte'
65
71
  import StructureExportPane from './StructureExportPane.svelte'
66
72
  import StructureInfoPane from './StructureInfoPane.svelte'
@@ -108,6 +114,8 @@
108
114
  lattice_props: lattice_props_in = $bindable(),
109
115
  controls_open = $bindable(false),
110
116
  info_pane_open = $bindable(false),
117
+ multi_view = $bindable(false),
118
+ views = DEFAULT_STRUCTURE_VIEWS,
111
119
  enable_measure_mode = $bindable(true),
112
120
  measure_mode = $bindable<MeasureMode>(`distance`),
113
121
  bond_edit_mode = $bindable<BondEditMode>(`add`),
@@ -184,97 +192,101 @@
184
192
  on_camera_reset,
185
193
  on_bonds_change,
186
194
  ...rest
187
- }:
188
- & {
189
- structure?: AnyStructure
190
- bonds?: StructureBond[]
191
- scene_props?: ComponentProps<typeof StructureScene>
192
- /**
193
- * Controls visibility configuration.
194
- * - 'always': controls always visible
195
- * - 'hover': controls visible on component hover (default)
196
- * - 'never': controls never visible
197
- * - object: { mode, hidden, style } for fine-grained control
198
- *
199
- * Control names: 'reset-camera', 'fullscreen', 'measure-mode', 'info-pane', 'export-pane', 'controls'
200
- */
201
- show_controls?: ShowControlsProp
202
- fullscreen?: boolean
203
- // bindable width of the canvas
204
- width?: number
205
- // bindable height of the canvas
206
- height?: number
207
- // Canvas wrapper element (for export pane)
208
- wrapper?: HTMLDivElement
209
- // PNG export DPI setting
210
- png_dpi?: number
211
- reset_text?: string
212
- hovered?: boolean
213
- dragover?: boolean
214
- allow_file_drop?: boolean
215
- enable_info_pane?: boolean
216
- enable_measure_mode?: boolean
217
- measure_mode?: MeasureMode
218
- bond_edit_mode?: BondEditMode
219
- bond_edit_order?: BondOrder
220
- info_pane_open?: boolean
221
- fullscreen_toggle?: Snippet<[{ fullscreen: boolean }]> | boolean
222
- bottom_left?: Snippet<[{ structure?: AnyStructure }]>
223
- top_right_controls?: Snippet // Additional controls to render at the end of the control buttons row
224
- data_url?: string // URL to load structure from (alternative to providing structure directly)
225
- // Generic callback for when files are dropped - receives raw content and filename
226
- on_file_drop?: (content: string | ArrayBuffer, filename: string) => void
227
- // spinner props (passed to Spinner component)
228
- spinner_props?: ComponentProps<typeof Spinner>
229
- loading?: boolean
230
- error_msg?: string
231
- // Performance mode: 'quality' (default) or 'speed' for large structures
232
- performance_mode?: `quality` | `speed`
233
- // allow parent components to control highlighted/selected site indices
234
- selected_sites?: number[]
235
- highlighted_sites?: number[]
236
- hovered_site_idx?: number | null
237
- // explicit measured sites for distance/angle overlays
238
- measured_sites?: number[]
239
- // expose the displayed structure (with image atoms and/or supercell) for external use
240
- displayed_structure?: AnyStructure
241
- // Track which elements are hidden (bindable across frames in trajectories)
242
- hidden_elements?: Set<ElementSymbol>
243
- // Track which property values are hidden (e.g. Wyckoff positions, coordination numbers)
244
- hidden_prop_vals?: Set<number | string>
245
- // Per-element radius overrides (absolute values in Angstroms)
246
- element_radius_overrides?: Partial<Record<ElementSymbol, number>>
247
- // Per-site radius overrides (absolute values in Angstroms)
248
- // Accepts Map or SvelteMap for flexibility with external callers
249
- site_radius_overrides?: Map<number, number> | SvelteMap<number, number>
250
- // Symmetry analysis data (bindable for external access)
251
- sym_data?: MoyoDataset | null
252
- // Symmetry analysis settings (bindable for external control)
253
- symmetry_settings?: Partial<SymmetrySettings>
254
- // Map element symbols to different elements (e.g. {'H': 'Na', 'He': 'Cl'})
255
- element_mapping?: Partial<Record<ElementSymbol, ElementSymbol>>
256
- // Cell type: original, conventional, or primitive (requires symmetry analysis)
257
- cell_type?: CellType
258
- // Volumetric data for isosurface rendering (parsed from CHGCAR or .cube files)
259
- volumetric_data?: VolumetricData[]
260
- // Isosurface rendering settings
261
- isosurface_settings?: IsosurfaceSettings
262
- // Active volume index when multiple volumes are present
263
- active_volume_idx?: number
264
- // structure content as string (alternative to providing structure directly or via data_url)
265
- structure_string?: string
266
- // Atom coloring configuration
267
- atom_color_config?: Partial<AtomColorConfig>
268
- children?: Snippet<[{ structure?: AnyStructure; fullscreen: boolean }]>
269
- on_file_load?: EventHandler
270
- on_error?: EventHandler
271
- on_fullscreen_change?: EventHandler
272
- on_camera_move?: EventHandler
273
- on_camera_reset?: EventHandler
274
- on_bonds_change?: (bonds: StructureBond[] | undefined) => void
275
- }
276
- & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`>
277
- & Omit<HTMLAttributes<HTMLDivElement>, `children`> = $props()
195
+ }: {
196
+ structure?: AnyStructure
197
+ bonds?: StructureBond[]
198
+ scene_props?: ComponentProps<typeof StructureScene>
199
+ // Controls visibility configuration.
200
+ // - 'always': controls always visible
201
+ // - 'hover': controls visible on component hover (default)
202
+ // - 'never': controls never visible
203
+ // - object: { mode, hidden, style } for fine-grained control
204
+ //
205
+ // Control names: 'reset-camera', 'fullscreen', 'measure-mode', 'info-pane', 'export-pane', 'controls'
206
+ show_controls?: ShowControlsProp
207
+ fullscreen?: boolean
208
+ // bindable width of the canvas
209
+ width?: number
210
+ // bindable height of the canvas
211
+ height?: number
212
+ // Canvas wrapper element (for export pane)
213
+ wrapper?: HTMLDivElement
214
+ // PNG export DPI setting
215
+ png_dpi?: number
216
+ reset_text?: string
217
+ hovered?: boolean
218
+ dragover?: boolean
219
+ allow_file_drop?: boolean
220
+ enable_info_pane?: boolean
221
+ enable_measure_mode?: boolean
222
+ measure_mode?: MeasureMode
223
+ bond_edit_mode?: BondEditMode
224
+ bond_edit_order?: BondOrder
225
+ info_pane_open?: boolean
226
+ // When true, split the canvas into a 2x2 grid showing the structure from
227
+ // different angles (Ovito-style). Each pane has independent orbit controls.
228
+ multi_view?: boolean
229
+ // The 4 (or more) view definitions used by multi_view. Defaults to an
230
+ // Ovito-like set: one perspective + three orthographic axis views.
231
+ views?: StructureView[]
232
+ fullscreen_toggle?: FullscreenToggleProp
233
+ bottom_left?: Snippet<[{ structure?: AnyStructure }]>
234
+ top_right_controls?: Snippet // Additional controls to render at the end of the control buttons row
235
+ data_url?: string // URL to load structure from (alternative to providing structure directly)
236
+ // Generic callback for when files are dropped - receives raw content and filename
237
+ on_file_drop?: (content: string | ArrayBuffer, filename: string) => void
238
+ // spinner props (passed to Spinner component)
239
+ spinner_props?: ComponentProps<typeof Spinner>
240
+ loading?: boolean
241
+ error_msg?: string
242
+ // Performance mode: 'quality' (default) or 'speed' for large structures
243
+ performance_mode?: `quality` | `speed`
244
+ // allow parent components to control highlighted/selected site indices
245
+ selected_sites?: number[]
246
+ highlighted_sites?: number[]
247
+ hovered_site_idx?: number | null
248
+ // explicit measured sites for distance/angle overlays
249
+ measured_sites?: number[]
250
+ // expose the displayed structure (with image atoms and/or supercell) for external
251
+ // use. Output-only: the scene renders from an internal raw copy for performance,
252
+ // so external writes to this prop are ignored (overwritten on the next pipeline run)
253
+ displayed_structure?: AnyStructure
254
+ // Track which elements are hidden (bindable across frames in trajectories)
255
+ hidden_elements?: Set<ElementSymbol>
256
+ // Track which property values are hidden (e.g. Wyckoff positions, coordination numbers)
257
+ hidden_prop_vals?: Set<number | string>
258
+ // Per-element radius overrides (absolute values in Angstroms)
259
+ element_radius_overrides?: Partial<Record<ElementSymbol, number>>
260
+ // Per-site radius overrides (absolute values in Angstroms)
261
+ // Accepts Map or SvelteMap for flexibility with external callers
262
+ site_radius_overrides?: Map<number, number> | SvelteMap<number, number>
263
+ // Symmetry analysis data (bindable for external access)
264
+ sym_data?: MoyoDataset | null
265
+ // Symmetry analysis settings (bindable for external control)
266
+ symmetry_settings?: Partial<SymmetrySettings>
267
+ // Map element symbols to different elements (e.g. {'H': 'Na', 'He': 'Cl'})
268
+ element_mapping?: Partial<Record<ElementSymbol, ElementSymbol>>
269
+ // Cell type: original, conventional, or primitive (requires symmetry analysis)
270
+ cell_type?: CellType
271
+ // Volumetric data for isosurface rendering (parsed from CHGCAR or .cube files)
272
+ volumetric_data?: VolumetricData[]
273
+ // Isosurface rendering settings
274
+ isosurface_settings?: IsosurfaceSettings
275
+ // Active volume index when multiple volumes are present
276
+ active_volume_idx?: number
277
+ // structure content as string (alternative to providing structure directly or via data_url)
278
+ structure_string?: string
279
+ // Atom coloring configuration
280
+ atom_color_config?: Partial<AtomColorConfig>
281
+ children?: Snippet<[{ structure?: AnyStructure; fullscreen: boolean }]>
282
+ on_file_load?: EventHandler
283
+ on_error?: EventHandler
284
+ on_fullscreen_change?: EventHandler
285
+ on_camera_move?: EventHandler
286
+ on_camera_reset?: EventHandler
287
+ on_bonds_change?: (bonds: StructureBond[] | undefined) => void
288
+ } & Omit<ComponentProps<typeof StructureControls>, `children` | `onclose`> &
289
+ Omit<HTMLAttributes<HTMLDivElement>, `children`> = $props()
278
290
 
279
291
  // Initialize models from incoming props; mutations come from UI controls; we mirror into local dicts (NOTE only doing shallow merge)
280
292
  $effect.pre(() => {
@@ -286,41 +298,54 @@
286
298
  }
287
299
  })
288
300
 
289
- // Load structure from URL when data_url is provided
301
+ // Load structure from URL when data_url is provided. A monotonic load_id ignores stale
302
+ // completions so a newer data_url (or an externally-supplied structure, via the cleanup)
303
+ // can't be clobbered by a slow earlier fetch.
304
+ let data_url_load_id = 0
290
305
  $effect(() => {
291
- if (data_url && !structure) {
292
- loading = true
293
- error_msg = undefined
294
-
295
- load_from_url(data_url, (content, filename) => {
296
- if (on_file_drop) on_file_drop(content, filename)
297
- else {
298
- // Parse structure internally when no handler provided
299
- try {
300
- const text_content = content instanceof ArrayBuffer
301
- ? new TextDecoder().decode(content)
302
- : content
303
- const parsed = parse_file_content(text_content, filename)
304
- emit_file_load_event(parsed, filename, content)
305
- } catch (error) {
306
- error_msg = `Failed to parse structure: ${
307
- to_error(error).message
308
- }`
309
- on_error?.({ error_msg, filename })
310
- }
306
+ if (!data_url || structure) return
307
+ const load_id = ++data_url_load_id
308
+ const is_current = () => load_id === data_url_load_id
309
+ loading = true
310
+ error_msg = undefined
311
+
312
+ load_from_url(data_url, (content, filename) => {
313
+ if (!is_current()) return // stale response
314
+ if (on_file_drop) on_file_drop(content, filename)
315
+ else {
316
+ // Parse structure internally when no handler provided
317
+ try {
318
+ const text_content =
319
+ content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
320
+ const parsed = parse_file_content(text_content, filename)
321
+ emit_file_load_event(parsed, filename, content)
322
+ } catch (error) {
323
+ error_msg = `Failed to parse structure: ${to_error(error).message}`
324
+ on_error?.({ error_msg, filename })
311
325
  }
326
+ }
327
+ })
328
+ .catch((error: Error) => {
329
+ if (!is_current()) return
330
+ console.error(`Failed to load structure from URL:`, error)
331
+ error_msg = `Failed to load structure: ${error.message}`
332
+ on_error?.({ error_msg, filename: data_url })
333
+ })
334
+ .finally(() => {
335
+ if (is_current()) loading = false
312
336
  })
313
- .then(() => loading = false)
314
- .catch((error: Error) => {
315
- console.error(`Failed to load structure from URL:`, error)
316
- error_msg = `Failed to load structure: ${error.message}`
317
- loading = false
318
- on_error?.({ error_msg, filename: data_url })
319
- })
337
+
338
+ return () => {
339
+ // invalidate in-flight load on data_url change / structure arrival / unmount
340
+ if (is_current()) {
341
+ data_url_load_id += 1
342
+ loading = false
343
+ }
320
344
  }
321
345
  })
322
346
 
323
- $effect(() => { // Parse structure from string when structure_string is provided
347
+ $effect(() => {
348
+ // Parse structure from string when structure_string is provided
324
349
  if (!structure_string || data_url) return
325
350
  loading = true
326
351
  error_msg = undefined
@@ -334,9 +359,7 @@
334
359
  throw new Error(`Failed to parse structure from string`)
335
360
  }
336
361
  } catch (err) {
337
- error_msg = `Failed to parse structure from string: ${
338
- to_error(err).message
339
- }`
362
+ error_msg = `Failed to parse structure from string: ${to_error(err).message}`
340
363
  untrack(() => on_error?.({ error_msg, filename: `string` }))
341
364
  } finally {
342
365
  loading = false
@@ -392,12 +415,7 @@
392
415
 
393
416
  // Compute property-based colors for legend display
394
417
  let property_colors = $derived(
395
- get_property_colors(
396
- structure,
397
- atom_color_config,
398
- scene_props.bonding_strategy,
399
- sym_data,
400
- ),
418
+ get_property_colors(structure, atom_color_config, scene_props.bonding_strategy, sym_data),
401
419
  )
402
420
 
403
421
  let symmetry_run_id = 0
@@ -429,7 +447,7 @@
429
447
  } else {
430
448
  // Keep previous symmetry data while recomputing so bound consumers
431
449
  // (e.g. SymmetryStats inputs) do not unmount and lose focus.
432
- untrack(() => symmetry_error = undefined)
450
+ untrack(() => (symmetry_error = undefined))
433
451
  }
434
452
  const run_id = ++symmetry_run_id
435
453
  // Destructure symmetry_settings to ensure Svelte tracks changes to symprec and algo
@@ -439,20 +457,21 @@
439
457
  // Skip symmetry auto-analysis in unit tests; happy-dom can't fetch WASM assets
440
458
  if (typeof process !== `undefined` && process.env?.VITEST) return
441
459
 
442
- symmetry.ensure_moyo_wasm_ready()
460
+ symmetry
461
+ .ensure_moyo_wasm_ready()
443
462
  .then(() =>
444
463
  run_id === symmetry_run_id
445
464
  ? symmetry.analyze_structure_symmetry(current_structure, current_settings)
446
- : null
465
+ : null,
447
466
  )
448
467
  .then((data) => {
449
468
  if (data && run_id === symmetry_run_id) {
450
- untrack(() => sym_data = data)
469
+ untrack(() => (sym_data = data))
451
470
  }
452
471
  })
453
472
  .catch((err) => {
454
473
  if (run_id === symmetry_run_id) {
455
- untrack(() => sym_data = null)
474
+ untrack(() => (sym_data = null))
456
475
  symmetry_error = `Symmetry analysis failed: ${err?.message || err}`
457
476
  console.error(`Symmetry analysis failed:`, err)
458
477
  }
@@ -477,8 +496,7 @@
477
496
  let last_emitted_bond_signature = $state<string>()
478
497
  let bond_edit_snapshot = $state.raw<BondEditSnapshot>()
479
498
  let has_bond_edits = $derived(
480
- added_bonds.length > 0 || removed_bonds.length > 0 ||
481
- bond_order_overrides.length > 0,
499
+ added_bonds.length > 0 || removed_bonds.length > 0 || bond_order_overrides.length > 0,
482
500
  )
483
501
 
484
502
  const clone_bonds = (edit_bonds: StructureBond[]): StructureBond[] =>
@@ -559,10 +577,10 @@
559
577
  const current_source_bond_signature = (): string => {
560
578
  const raw_signature = bond_signature(current_source_bonds())
561
579
  if (raw_signature !== last_emitted_bond_signature) return raw_signature
562
- return bond_history_context?.source_bond_signature ??
563
- (bond_edit_snapshot
564
- ? bond_signature(bond_edit_snapshot.bonds)
565
- : raw_signature)
580
+ return (
581
+ bond_history_context?.source_bond_signature ??
582
+ (bond_edit_snapshot ? bond_signature(bond_edit_snapshot.bonds) : raw_signature)
583
+ )
566
584
  }
567
585
 
568
586
  const current_bond_edit_context = (): BondEditContext => ({
@@ -643,10 +661,21 @@
643
661
  emit_bonds(edited_bonds)
644
662
  })
645
663
 
664
+ // Elements currently anchoring polyhedra (written by StructureScene, read by
665
+ // StructureControls so per-element toggles reflect actual render state)
666
+ let polyhedra_rendered_elements = $state<string[]>([])
667
+
646
668
  // === Edit-atoms mode state ===
647
669
  let dragging_atoms = $state(false)
648
- let undo_stack = $state<AnyStructure[]>([])
649
- let redo_stack = $state<AnyStructure[]>([])
670
+ // History entries snapshot the bindable `bonds` prop alongside the structure:
671
+ // atom deletion remaps `bonds` to the shifted site indices, so restoring the
672
+ // structure alone would leave explicit bonds pointing at wrong sites.
673
+ type EditSnapshot = { structure: AnyStructure; bonds: StructureBond[] | undefined }
674
+ // $state.raw: stacks hold full structure snapshots — deep-proxying them would make
675
+ // every restored structure (and all downstream site reads) go through proxy traps.
676
+ // Mutations happen via reassignment so .length stays reactive in the template.
677
+ let undo_stack = $state.raw<EditSnapshot[]>([])
678
+ let redo_stack = $state.raw<EditSnapshot[]>([])
650
679
  const MAX_HISTORY = 20
651
680
  // Flag set before internal edits (undo/redo/delete/add/move) to distinguish
652
681
  // them from external structure changes (file load, trajectory step, etc.)
@@ -654,7 +683,6 @@
654
683
  // Add-atom sub-mode state (bound to StructureScene)
655
684
  let add_atom_mode = $state(false)
656
685
  let add_element = $state<ElementSymbol>(`C` as ElementSymbol)
657
- let canvas_cursor = $state(`default`)
658
686
  let is_measure_selection_mode = $derived(
659
687
  measure_mode === `distance` || measure_mode === `angle`,
660
688
  )
@@ -682,9 +710,8 @@
682
710
 
683
711
  // Normalize and validate element symbol (e.g. "fe" → "Fe", "Xx" → null)
684
712
  function normalize_element(input: string): ElementSymbol | null {
685
- const normalized = (input.charAt(0).toUpperCase() +
686
- input.slice(1).toLowerCase()) as ElementSymbol
687
- return ELEM_SYMBOLS.includes(normalized) ? normalized : null
713
+ const normalized = input.charAt(0).toUpperCase() + input.slice(1).toLowerCase()
714
+ return coerce_elem_symbol(normalized) ?? null
688
715
  }
689
716
 
690
717
  function clear_selection() {
@@ -693,28 +720,40 @@
693
720
  dragging_atoms = false
694
721
  }
695
722
 
723
+ const snapshot_edit_state = (current: AnyStructure): EditSnapshot => ({
724
+ structure: $state.snapshot(current) as AnyStructure,
725
+ bonds: bonds && ($state.snapshot(bonds) as StructureBond[]),
726
+ })
727
+
696
728
  function push_undo() {
697
729
  if (!structure) return
698
- if (undo_stack.length >= MAX_HISTORY) {
699
- undo_stack.splice(0, undo_stack.length - MAX_HISTORY + 1)
700
- }
701
- undo_stack.push($state.snapshot(structure) as AnyStructure)
702
- redo_stack.length = 0
730
+ ;[undo_stack, redo_stack] = push_edit(
731
+ [undo_stack, redo_stack],
732
+ snapshot_edit_state(structure),
733
+ MAX_HISTORY,
734
+ )
703
735
  }
704
736
 
705
- // Shared undo/redo: pop from `source`, push current state onto `target`
706
- function apply_history(source: AnyStructure[], target: AnyStructure[]) {
707
- if (source.length === 0 || !structure) return
708
- const restored = source.pop()
709
- if (!restored) return
737
+ // Shared undo/redo: restore the top of that direction's stack, pushing the
738
+ // current state onto the opposite one ($state.raw stacks update by reassignment)
739
+ function apply_history(direction: `undo` | `redo`) {
740
+ // Check the stack before snapshotting: $state.snapshot deep-clones the structure
741
+ if (!structure || (direction === `undo` ? undo_stack : redo_stack).length === 0) return
742
+ const result = step_history(
743
+ [undo_stack, redo_stack],
744
+ direction,
745
+ snapshot_edit_state(structure),
746
+ )
747
+ if (!result) return
710
748
  is_internal_edit = true
711
- target.push($state.snapshot(structure) as AnyStructure)
712
- structure = restored
749
+ ;[undo_stack, redo_stack] = result.stacks
750
+ structure = result.restored.structure
751
+ bonds = result.restored.bonds
713
752
  clear_selection()
714
753
  }
715
754
 
716
- const undo = () => apply_history(undo_stack, redo_stack)
717
- const redo = () => apply_history(redo_stack, undo_stack)
755
+ const undo = () => apply_history(`undo`)
756
+ const redo = () => apply_history(`redo`)
718
757
 
719
758
  // Clear undo/redo stacks when structure changes externally (file load, etc.)
720
759
  // Internal edits set is_internal_edit=true before modifying structure.
@@ -757,7 +796,10 @@
757
796
  $effect(() => {
758
797
  void bond_edit_mode
759
798
  untrack(() => {
760
- if (measure_mode === `edit-bonds` && (selected_sites.length > 0 || measured_sites.length > 0)) {
799
+ if (
800
+ measure_mode === `edit-bonds` &&
801
+ (selected_sites.length > 0 || measured_sites.length > 0)
802
+ ) {
761
803
  clear_selection()
762
804
  }
763
805
  })
@@ -794,10 +836,13 @@
794
836
  ])
795
837
 
796
838
  // Normalize structure coordinates: wrap fractional coords to [0,1) and recompute Cartesian
797
- // This ensures atoms are rendered inside the unit cell regardless of data source
839
+ // This ensures atoms are rendered inside the unit cell regardless of data source.
840
+ // ensure_lattice_params covers structures that bypass parse_any_structure (direct
841
+ // props, trajectory JSON frames) with matrix-only pymatgen lattices, which would
842
+ // otherwise NaN the camera auto-fit and render blank.
798
843
  let normalized_structure = $derived.by(() => {
799
844
  if (!structure || !(`lattice` in structure)) return structure
800
- return normalize_fractional_coords(structure) as AnyStructure
845
+ return ensure_lattice_params(normalize_fractional_coords(structure)) as AnyStructure
801
846
  })
802
847
 
803
848
  let structure_with_bonds = $derived.by(() => {
@@ -812,7 +857,8 @@
812
857
  // This must happen BEFORE supercell transformation
813
858
  let cell_transformed_structure = $derived.by(() => {
814
859
  if (
815
- !structure_with_bonds || !(`lattice` in structure_with_bonds) ||
860
+ !structure_with_bonds ||
861
+ !(`lattice` in structure_with_bonds) ||
816
862
  cell_type === `original`
817
863
  ) {
818
864
  return structure_with_bonds
@@ -829,8 +875,11 @@
829
875
  }
830
876
  })
831
877
 
832
- // Create supercell if needed (uses cell_transformed_structure as base)
833
- let supercell_structure = $state(structure)
878
+ // Create supercell if needed (uses cell_transformed_structure as base).
879
+ // $state.raw: supercells can hold thousands of sites — deep-proxying them makes
880
+ // every downstream read (bonding, PBC images, instancing) traverse proxy traps,
881
+ // measured ~15x slower for compute_bonds on a 704-site supercell.
882
+ let supercell_structure = $state.raw(structure)
834
883
  let supercell_loading = $state(false)
835
884
  let has_supercell = $derived(
836
885
  Boolean(supercell_scaling) && ![``, `1x1x1`, `1`].includes(supercell_scaling),
@@ -863,21 +912,34 @@
863
912
  })
864
913
 
865
914
  let supercell_timeout: ReturnType<typeof setTimeout> | undefined
915
+ // Compute the supercell, falling back to the unscaled structure on error
916
+ const make_supercell_safe = (base: Crystal): Crystal => {
917
+ try {
918
+ return make_supercell(base, supercell_scaling)
919
+ } catch (error) {
920
+ console.error(`Failed to create supercell:`, error)
921
+ show_toast(`Failed to create supercell: ${to_error(error).message}`)
922
+ return base
923
+ }
924
+ }
866
925
  $effect(() => {
867
926
  const base_structure = cell_transformed_structure
868
927
  clearTimeout(supercell_timeout)
869
- if (!base_structure || !(`lattice` in base_structure) || !has_supercell) {
870
- supercell_structure = base_structure
871
- supercell_loading = false
872
- } else if (!is_valid_supercell_input(supercell_scaling)) {
928
+ if (
929
+ !base_structure ||
930
+ !(`lattice` in base_structure) ||
931
+ !has_supercell ||
932
+ !is_valid_supercell_input(supercell_scaling)
933
+ ) {
873
934
  supercell_structure = base_structure
874
935
  supercell_loading = false
875
936
  } else {
876
937
  // For large supercells, show loading state and use async generation
877
938
  const sites_count = base_structure.sites?.length || 0
878
- const [nx_str, ny_str, nz_str] = supercell_scaling.split(/[x×]/)
879
- const scaling_mult = (parseInt(nx_str, 10) || 1) * (parseInt(ny_str, 10) || 1) *
880
- (parseInt(nz_str, 10) || 1)
939
+ // lenient parse just for a size estimate (invalid factors count as 1)
940
+ const scaling_mult = supercell_scaling
941
+ .split(/[x×]/)
942
+ .reduce((product, factor) => product * (Number(factor) || 1), 1)
881
943
  const estimated_sites = sites_count * scaling_mult
882
944
 
883
945
  // Show spinner for supercells with >1000 estimated sites or scaling >8
@@ -887,27 +949,11 @@
887
949
  supercell_loading = true
888
950
  // Use setTimeout to allow UI to update before heavy computation
889
951
  supercell_timeout = setTimeout(() => {
890
- try {
891
- if (base_structure && `lattice` in base_structure) {
892
- supercell_structure = make_supercell(
893
- base_structure as Crystal,
894
- supercell_scaling,
895
- )
896
- }
897
- } catch (error) {
898
- console.error(`Failed to create supercell:`, error)
899
- supercell_structure = base_structure
900
- } finally {
901
- supercell_loading = false
902
- }
952
+ supercell_structure = make_supercell_safe(base_structure as Crystal)
953
+ supercell_loading = false
903
954
  }, 10)
904
955
  } else {
905
- if (base_structure && `lattice` in base_structure) {
906
- supercell_structure = make_supercell(
907
- base_structure as Crystal,
908
- supercell_scaling,
909
- )
910
- }
956
+ supercell_structure = make_supercell_safe(base_structure as Crystal)
911
957
  supercell_loading = false
912
958
  }
913
959
  }
@@ -934,9 +980,12 @@
934
980
  })
935
981
  })
936
982
 
937
- // Apply element mapping then image atoms to the supercell structure.
938
- // Skip get_pbc_image_sites during atom drags — the vector math + doubled site
939
- // count causes frame drops. Image atoms reappear instantly on drag release.
983
+ // Element-map + PBC image atoms. Skipped during drags (doubled site count drops frames);
984
+ // images return on release.
985
+ // The render path reads the internal $state.raw copy, NOT the bindable prop:
986
+ // if a consumer binds displayed_structure to a deep $state, reading the prop
987
+ // back would re-proxy every site and slow all scene computations ~10x.
988
+ let internal_displayed_structure = $state.raw<AnyStructure | undefined>(undefined)
940
989
  $effect(() => {
941
990
  let struct = supercell_structure
942
991
  if (struct && element_mapping && Object.keys(element_mapping).length > 0) {
@@ -953,124 +1002,90 @@
953
1002
  })),
954
1003
  }
955
1004
  }
956
- displayed_structure =
957
- !dragging_atoms && show_image_atoms && struct && `lattice` in struct &&
958
- struct.lattice
1005
+ // Local var, NOT a read-back of the states written below: reading our own
1006
+ // fresh-object write inside this effect would self-retrigger it forever.
1007
+ const next =
1008
+ !dragging_atoms && show_image_atoms && struct && `lattice` in struct && struct.lattice
959
1009
  ? get_pbc_image_sites(struct)
960
1010
  : struct
1011
+ internal_displayed_structure = next
1012
+ displayed_structure = next
961
1013
  })
962
1014
 
963
- // Track if camera has ever been moved from initial position
964
- let camera_has_moved = $state(false)
965
- let camera_is_moving = $state(false)
1015
+ // scene + camera of the primary pane, bound out for the export pane. All other camera
1016
+ // handling (move tracking, reset, re-framing) lives in StructureViewport.
966
1017
  let scene = $state<Scene | undefined>(undefined)
967
1018
  let camera = $state<Camera | undefined>(undefined)
968
- let orbit_controls = $state<
969
- ComponentProps<typeof StructureScene>[`orbit_controls`]
970
- >(undefined)
971
- let rotation_target_ref = $state<Vec3 | undefined>(undefined)
972
- let initial_computed_zoom = $state<number | undefined>(undefined)
1019
+
1020
+ // Multi-side view state: index of the pane the pointer is over (gets edit interactions),
1021
+ // a token bumped to reset every pane, and the set of panes whose camera has moved (so
1022
+ // the reset button stays visible until every moved pane is reset). Pane 0 = primary.
1023
+ let active_pane_idx = $state(0)
1024
+ let reset_token = $state(0)
1025
+ // SvelteSet is already reactive; do NOT wrap in $state (double-proxying breaks it)
1026
+ const moved_panes = new SvelteSet<number>()
1027
+ let any_camera_moved = $derived(moved_panes.size > 0)
1028
+
1029
+ // Inputs shared by every StructureViewport (single + all multi-view panes). Camera,
1030
+ // selection bindings, and per-pane chrome differ and stay on each snippet below.
1031
+ let shared_viewport_props = $derived({
1032
+ structure: internal_displayed_structure,
1033
+ base_structure: cell_transformed_structure,
1034
+ scene_props,
1035
+ gizmo: scene_gizmo,
1036
+ lattice_props,
1037
+ volumetric_data: supercell_volume,
1038
+ isosurface_settings,
1039
+ bond_edits_enabled,
1040
+ bond_edit_order,
1041
+ measure_mode,
1042
+ atom_color_config,
1043
+ sym_data,
1044
+ active_sites: active_scene_sites,
1045
+ on_sites_moved: handle_sites_moved,
1046
+ on_operation_start: push_undo,
1047
+ on_bond_edit_start: push_bond_undo,
1048
+ on_add_atom: handle_add_atom,
1049
+ })
973
1050
 
974
1051
  // Mutual exclusion: opening one pane closes others
975
1052
  $effect(() => {
976
1053
  if (info_pane_open) {
977
- untrack(() => [controls_open, export_pane_open] = [false, false])
1054
+ untrack(() => ([controls_open, export_pane_open] = [false, false]))
978
1055
  }
979
1056
  })
980
1057
  $effect(() => {
981
1058
  if (controls_open) {
982
- untrack(() => [info_pane_open, export_pane_open] = [false, false])
1059
+ untrack(() => ([info_pane_open, export_pane_open] = [false, false]))
983
1060
  }
984
1061
  })
985
1062
  $effect(() => {
986
1063
  if (export_pane_open) {
987
- untrack(() => [info_pane_open, controls_open] = [false, false])
1064
+ untrack(() => ([info_pane_open, controls_open] = [false, false]))
988
1065
  }
989
1066
  })
990
1067
 
991
- // Reset tracking when structure changes
1068
+ // Reset moved-pane tracking when structure changes
992
1069
  $effect(() => {
993
- if (structure) camera_has_moved = false
1070
+ // untrack: clearing must not add moved_panes as a dependency, else a pane move
1071
+ // (which adds to moved_panes) would immediately re-run this and clear it again.
1072
+ if (structure) untrack(() => moved_panes.clear())
994
1073
  })
995
1074
 
996
1075
  // Clear stale camera target and position so StructureScene uses the new
997
1076
  // structure's rotation_target (unit cell center) and auto-positions the camera.
998
1077
  function clear_camera_state() {
1078
+ // Reset to a fresh [0,0,0] so the primary viewport re-frames the new structure.
1079
+ // Side panes reset their local camera state in StructureViewport's structure effect.
999
1080
  scene_props.camera_target = undefined
1000
1081
  scene_props.camera_position = [0, 0, 0]
1001
1082
  }
1002
1083
 
1003
- const read_orbit_target = (): Vec3 | undefined => {
1004
- if (!orbit_controls?.target) return
1005
- const { x, y, z } = orbit_controls.target
1006
- return [x, y, z]
1007
- }
1008
-
1009
- const read_camera_position = (): Vec3 | undefined =>
1010
- camera
1011
- ? [camera.position.x, camera.position.y, camera.position.z]
1012
- : scene_props.camera_position
1013
-
1014
- // Emit debounced camera updates while controls are active.
1015
- $effect(() => {
1016
- if (!camera_is_moving) return
1017
- camera_has_moved = true
1018
-
1019
- const emit_camera_move = () => {
1020
- const camera_position = read_camera_position()
1021
- if (camera_position === undefined) return
1022
- const camera_target = read_orbit_target()
1023
- scene_props.camera_position = camera_position
1024
- scene_props.camera_target = camera_target
1025
- on_camera_move?.({
1026
- structure,
1027
- camera_has_moved,
1028
- camera_position,
1029
- camera_target,
1030
- })
1031
- }
1032
-
1033
- emit_camera_move()
1034
- const emit_interval = setInterval(emit_camera_move, 200)
1035
- return () => clearInterval(emit_interval)
1036
- })
1037
-
1038
- function reset_camera() {
1039
- // Reset camera position to trigger automatic positioning.
1040
- scene_props.camera_position = [0, 0, 0]
1041
- scene_props.camera_target = rotation_target_ref
1042
- camera_has_moved = false
1043
-
1044
- let camera_position: Vec3 = [0, 0, 0]
1045
- let camera_target: Vec3 | undefined = rotation_target_ref
1046
-
1047
- // Reset pan/zoom and ensure controls target returns to structure center.
1048
- if (orbit_controls && camera) {
1049
- if (
1050
- `reset` in orbit_controls &&
1051
- typeof orbit_controls.reset === `function`
1052
- ) orbit_controls.reset()
1053
- if (orbit_controls.target && rotation_target_ref) {
1054
- const [target_x, target_y, target_z] = rotation_target_ref
1055
- orbit_controls.target.set(target_x, target_y, target_z)
1056
- }
1057
-
1058
- // Reset zoom for orthographic camera
1059
- if (`zoom` in camera && initial_computed_zoom !== undefined) {
1060
- const ortho_camera = camera as OrthographicCamera
1061
- ortho_camera.zoom = initial_computed_zoom
1062
- ortho_camera.updateProjectionMatrix()
1063
- }
1064
-
1065
- // Call update to apply changes immediately
1066
- if (typeof orbit_controls.update === `function`) orbit_controls.update()
1067
- camera_position = read_camera_position() ?? camera_position
1068
- camera_target = read_orbit_target()
1069
- }
1070
-
1071
- scene_props.camera_position = camera_position
1072
- scene_props.camera_target = camera_target
1073
- on_camera_reset?.({ structure, camera_has_moved, camera_position, camera_target })
1084
+ // Reset every pane's camera (each StructureViewport resets on a reset_token bump and,
1085
+ // for the primary, emits on_camera_reset).
1086
+ function reset_all_cameras() {
1087
+ reset_token += 1
1088
+ moved_panes.clear()
1074
1089
  }
1075
1090
 
1076
1091
  const emit_file_load_event = (
@@ -1081,19 +1096,14 @@
1081
1096
  on_file_load?.({
1082
1097
  structure: loaded_structure,
1083
1098
  filename,
1084
- file_size: typeof content === `string`
1085
- ? new Blob([content]).size
1086
- : content.byteLength,
1099
+ file_size: typeof content === `string` ? new Blob([content]).size : content.byteLength,
1087
1100
  total_atoms: loaded_structure.sites?.length || 0,
1088
1101
  })
1089
1102
 
1090
1103
  // Try to parse content as a volumetric file, setting both structure and volumetric data.
1091
1104
  // Delegates format detection entirely to parse_volumetric_file (filename + content sniffing).
1092
1105
  // Returns the parsed structure on success, or null if the file isn't a volumetric format.
1093
- function try_parse_volumetric(
1094
- text_content: string,
1095
- filename: string,
1096
- ): AnyStructure | null {
1106
+ function try_parse_volumetric(text_content: string, filename: string): AnyStructure | null {
1097
1107
  const vol_result = parse_volumetric_file(text_content, filename)
1098
1108
  if (!vol_result) return null
1099
1109
  // parse_volumetric_file extracts structure from file header;
@@ -1128,15 +1138,12 @@
1128
1138
  on_drop: (content, filename) => {
1129
1139
  if (on_file_drop) return on_file_drop(content, filename)
1130
1140
  try {
1131
- const text_content = content instanceof ArrayBuffer
1132
- ? new TextDecoder().decode(content)
1133
- : content
1141
+ const text_content =
1142
+ content instanceof ArrayBuffer ? new TextDecoder().decode(content) : content
1134
1143
  const parsed = parse_file_content(text_content, filename)
1135
1144
  emit_file_load_event(parsed, filename, content)
1136
1145
  } catch (err) {
1137
- error_msg = `Failed to parse structure: ${
1138
- to_error(err).message
1139
- }`
1146
+ error_msg = `Failed to parse structure: ${to_error(err).message}`
1140
1147
  on_error?.({ error_msg, filename })
1141
1148
  }
1142
1149
  },
@@ -1263,8 +1270,10 @@
1263
1270
  }
1264
1271
  // Duplicate selected atoms at a small offset
1265
1272
  if (
1266
- key === `d` && (event.ctrlKey || event.metaKey) &&
1267
- selected_sites.length > 0 && structure?.sites
1273
+ key === `d` &&
1274
+ (event.ctrlKey || event.metaKey) &&
1275
+ selected_sites.length > 0 &&
1276
+ structure?.sites
1268
1277
  ) {
1269
1278
  is_internal_edit = true
1270
1279
  push_undo()
@@ -1273,11 +1282,7 @@
1273
1282
  const new_sites = structure.sites
1274
1283
  .filter((_, idx) => orig_indices.has(idx))
1275
1284
  .map((site) => {
1276
- const new_xyz: Vec3 = [
1277
- site.xyz[0] + 0.5,
1278
- site.xyz[1] + 0.5,
1279
- site.xyz[2] + 0.5,
1280
- ]
1285
+ const new_xyz: Vec3 = [site.xyz[0] + 0.5, site.xyz[1] + 0.5, site.xyz[2] + 0.5]
1281
1286
  return {
1282
1287
  ...site,
1283
1288
  xyz: new_xyz,
@@ -1293,9 +1298,7 @@
1293
1298
  // Select the newly duplicated atoms
1294
1299
  selected_sites = new_sites.map((_, idx) => base_idx + idx)
1295
1300
  measured_sites = [...selected_sites]
1296
- show_toast(
1297
- `Duplicated ${new_sites.length} site${new_sites.length > 1 ? `s` : ``}`,
1298
- )
1301
+ show_toast(`Duplicated ${new_sites.length} site${new_sites.length > 1 ? `s` : ``}`)
1299
1302
  return true
1300
1303
  }
1301
1304
 
@@ -1320,6 +1323,9 @@
1320
1323
  } else if (event.key === `i` && has_modifier && enable_info_pane) {
1321
1324
  info_pane_open = !info_pane_open
1322
1325
  return true
1326
+ } else if (event.key === `g` && has_modifier && controls_config.visible(`multi-view`)) {
1327
+ multi_view = !multi_view
1328
+ return true
1323
1329
  } else if (event.key === `Escape`) {
1324
1330
  // Prioritize closing panes, then exit edit modes, then exit fullscreen
1325
1331
  if (info_pane_open) info_pane_open = false
@@ -1351,7 +1357,7 @@
1351
1357
  ): SvelteSet<number> {
1352
1358
  const result = new SvelteSet<number>()
1353
1359
  for (const scene_idx of scene_indices) {
1354
- const displayed_site = displayed_structure?.sites?.[scene_idx]
1360
+ const displayed_site = internal_displayed_structure?.sites?.[scene_idx]
1355
1361
  if (!displayed_site) continue
1356
1362
  if (skip_image_atoms && displayed_site.properties?.orig_site_idx != null) {
1357
1363
  continue
@@ -1389,10 +1395,9 @@
1389
1395
  const orig_indices = scene_to_structure_indices(scene_indices)
1390
1396
  // For crystals, wrap to [0,1) inline so normalize_fractional_coords fast-paths.
1391
1397
  // For molecules (no lattice), just apply the Cartesian delta directly.
1392
- const lattice = `lattice` in structure
1393
- ? (structure as Crystal).lattice.matrix
1394
- : null
1395
- const cart_to_frac = lattice ? create_cart_to_frac(lattice) : null
1398
+ const lattice = `lattice` in structure ? (structure as Crystal).lattice.matrix : null
1399
+ // get_cart_to_frac guards matrix_inverse_3x3, which throws on singular lattices
1400
+ const cart_to_frac = get_cart_to_frac()
1396
1401
  const frac_to_cart = lattice ? create_frac_to_cart(lattice) : null
1397
1402
  structure = {
1398
1403
  ...structure,
@@ -1434,9 +1439,7 @@
1434
1439
  change_element_mode = false
1435
1440
  change_element_value = ``
1436
1441
  show_toast(
1437
- `Changed ${orig_indices.size} site${
1438
- orig_indices.size > 1 ? `s` : ``
1439
- } to ${elem}`,
1442
+ `Changed ${orig_indices.size} site${orig_indices.size > 1 ? `s` : ``} to ${elem}`,
1440
1443
  )
1441
1444
  }
1442
1445
 
@@ -1451,13 +1454,16 @@
1451
1454
  push_undo()
1452
1455
  structure = {
1453
1456
  ...structure,
1454
- sites: [...structure.sites, {
1455
- species: [{ element: elem, occu: 1, oxidation_state: 0 }],
1456
- xyz,
1457
- abc: get_cart_to_frac()?.(xyz) ?? xyz,
1458
- label: elem,
1459
- properties: {},
1460
- }],
1457
+ sites: [
1458
+ ...structure.sites,
1459
+ {
1460
+ species: [{ element: elem, occu: 1, oxidation_state: 0 }],
1461
+ xyz,
1462
+ abc: get_cart_to_frac()?.(xyz) ?? xyz,
1463
+ label: elem,
1464
+ properties: {},
1465
+ },
1466
+ ],
1461
1467
  }
1462
1468
  show_toast(`Added ${elem} at (${xyz.map((coord) => coord.toFixed(2)).join(`, `)})`)
1463
1469
  }
@@ -1469,35 +1475,22 @@
1469
1475
  const alpha_hex = Math.round(background_opacity * 255)
1470
1476
  .toString(16)
1471
1477
  .padStart(2, `0`)
1472
- wrapper.style.setProperty(
1473
- `--struct-bg-override`,
1474
- `${background_color}${alpha_hex}`,
1475
- )
1478
+ wrapper.style.setProperty(`--struct-bg-override`, `${background_color}${alpha_hex}`)
1476
1479
  } else if (typeof window !== `undefined` && wrapper) {
1477
1480
  // Remove override to use theme system
1478
1481
  wrapper.style.removeProperty(`--struct-bg-override`)
1479
1482
  }
1480
1483
  })
1481
1484
 
1482
- $effect(() => { // fullscreen and background
1483
- if (typeof window !== `undefined`) {
1484
- if (fullscreen && !document.fullscreenElement && wrapper) {
1485
- wrapper.requestFullscreen().catch(console.error)
1486
- } else if (!fullscreen && document.fullscreenElement) {
1487
- document.exitFullscreen()
1488
- }
1489
- }
1490
- set_fullscreen_bg(wrapper, fullscreen, `--struct-bg-fullscreen`)
1485
+ sync_fullscreen({
1486
+ get_wrapper: () => wrapper,
1487
+ get_fullscreen: () => fullscreen,
1488
+ set_fullscreen: (val) => (fullscreen = val),
1489
+ bg_css_var: `--struct-bg-fullscreen`,
1490
+ on_change: (val) => on_fullscreen_change?.({ structure, fullscreen: val }),
1491
1491
  })
1492
1492
  </script>
1493
1493
 
1494
- <svelte:document
1495
- onfullscreenchange={() => {
1496
- fullscreen = Boolean(document.fullscreenElement)
1497
- on_fullscreen_change?.({ structure, fullscreen })
1498
- }}
1499
- />
1500
-
1501
1494
  <!-- Forward shortcuts to the hovered viewer when focus is on <body> (see
1502
1495
  forward_window_keydown). Edit modes are excluded so destructive keys
1503
1496
  (delete/undo) still require focus, not just a hovering mouse. -->
@@ -1508,9 +1501,9 @@
1508
1501
  class:dragover
1509
1502
  class:active={info_pane_open || controls_open || export_pane_open}
1510
1503
  class:gizmo-visible={Boolean(scene_gizmo)}
1504
+ class:multi-view={multi_view}
1511
1505
  role="application"
1512
1506
  tabindex="0"
1513
- style:--canvas-cursor={canvas_cursor}
1514
1507
  aria-label="Structure viewer"
1515
1508
  bind:this={wrapper}
1516
1509
  bind:clientWidth={width}
@@ -1523,30 +1516,14 @@
1523
1516
  focused = false
1524
1517
  }
1525
1518
  }}
1526
- ondblclick={(event) => {
1527
- const target = event.target
1528
- if (!(target instanceof HTMLElement)) return
1529
- // Don't reset if double-click was on UI controls/panes/legend
1530
- if (
1531
- [`.control-buttons`, `.structure-legend`, `.atom-legend`, `.info-pane`, `.export-pane`, `.controls-pane`].some((selector) => target.closest(selector))
1532
- || target.tagName === `BUTTON` || target.tagName === `INPUT` || target.tagName === `SELECT`
1533
- ) return
1534
- // Reset camera for double-clicks on the 3D scene
1535
- reset_camera()
1536
- }}
1537
1519
  ondrop={handle_file_drop}
1538
- ondragover={(event) => {
1539
- event.preventDefault()
1540
- if (!allow_file_drop) return
1541
- dragover = true
1542
- }}
1543
- ondragleave={(event) => {
1544
- event.preventDefault()
1545
- dragover = false
1546
- }}
1520
+ {...drag_over_handlers({
1521
+ allow: () => allow_file_drop,
1522
+ set_dragover: (over) => (dragover = over),
1523
+ })}
1547
1524
  onkeydown={handle_and_prevent(handle_keydown)}
1548
1525
  {...rest}
1549
- class="structure {rest.class ?? ``}"
1526
+ class={[`structure`, rest.class]}
1550
1527
  >
1551
1528
  {@render children?.({ structure, fullscreen })}
1552
1529
  {#if loading}
@@ -1558,294 +1535,292 @@
1558
1535
  {:else if error_msg}
1559
1536
  <StatusMessage bind:message={error_msg} type="error" dismissible />
1560
1537
  {:else if (structure?.sites?.length ?? 0) > 0}
1561
- <section
1562
- class="control-buttons {controls_config.class}"
1563
- style={controls_config.style}
1538
+ {#snippet reset_camera_btn()}
1539
+ {#if any_camera_moved && controls_config.visible(`reset-camera`)}
1540
+ <button
1541
+ class="reset-camera"
1542
+ onclick={reset_all_cameras}
1543
+ title={reset_text}
1544
+ aria-label={reset_text}
1545
+ >
1546
+ <!-- Target/Focus icon for reset camera -->
1547
+ <Icon icon="Reset" />
1548
+ </button>
1549
+ {/if}
1550
+ {/snippet}
1551
+ <ViewerChrome
1552
+ {controls_config}
1553
+ {fullscreen}
1554
+ {fullscreen_toggle}
1555
+ fullscreen_btn_style="padding: 0 3px"
1556
+ {wrapper}
1557
+ before={reset_camera_btn}
1558
+ style="--viewer-buttons-gap: 4pt; --viewer-buttons-btn-padding: 1px 6px; --viewer-buttons-align: stretch"
1564
1559
  >
1565
- {#if controls_config.mode !== `never`}
1566
- {#if camera_has_moved && controls_config.visible(`reset-camera`)}
1567
- <button class="reset-camera" onclick={reset_camera} title={reset_text}>
1568
- <!-- Target/Focus icon for reset camera -->
1569
- <Icon icon="Reset" />
1570
- </button>
1571
- {/if}
1572
- {#if fullscreen_toggle && controls_config.visible(`fullscreen`)}
1560
+ {#if controls_config.visible(`multi-view`)}
1561
+ <button
1562
+ class="multi-view-toggle"
1563
+ class:active={multi_view}
1564
+ onclick={() => (multi_view = !multi_view)}
1565
+ title="Toggle multi-side view 2×2 grid (Cmd/Ctrl+G)"
1566
+ aria-label="Toggle multi-side view"
1567
+ aria-pressed={multi_view}
1568
+ {@attach tooltip()}
1569
+ >
1570
+ <Icon icon="Grid2x2" />
1571
+ </button>
1572
+ {/if}
1573
+
1574
+ {#if enable_measure_mode && controls_config.visible(`measure-mode`)}
1575
+ <div
1576
+ class="measure-mode-dropdown"
1577
+ {@attach click_outside({ callback: () => (measure_menu_open = false) })}
1578
+ >
1573
1579
  <button
1574
- type="button"
1575
- onclick={() => fullscreen_toggle && toggle_fullscreen(wrapper)}
1576
- title="{fullscreen ? `Exit` : `Enter`} fullscreen"
1577
- aria-pressed={fullscreen}
1578
- class="fullscreen-toggle"
1579
- style="padding: 0 3px"
1580
+ onclick={() => (measure_menu_open = !measure_menu_open)}
1581
+ title="Measure / Edit"
1582
+ aria-label="Measure / Edit"
1583
+ class="view-mode-button"
1584
+ class:active={measure_menu_open}
1585
+ aria-expanded={measure_menu_open}
1586
+ style="transform: scale(1.2)"
1580
1587
  {@attach tooltip()}
1581
1588
  >
1582
- {#if typeof fullscreen_toggle === `function`}
1583
- {@render fullscreen_toggle({ fullscreen })}
1589
+ {#if show_measure_selection_limit}
1590
+ <span class="selection-limit-text">
1591
+ {measured_sites.length}/{MAX_SELECTED_SITES}
1592
+ </span>
1584
1593
  {:else}
1585
- <Icon icon="{fullscreen ? `Exit` : ``}Fullscreen" />
1586
- {/if}
1587
- </button>
1588
- {/if}
1589
-
1590
- {#if enable_measure_mode && controls_config.visible(`measure-mode`)}
1591
- <div
1592
- class="measure-mode-dropdown"
1593
- {@attach click_outside({ callback: () => measure_menu_open = false })}
1594
- >
1595
- <button
1596
- onclick={() => (measure_menu_open = !measure_menu_open)}
1597
- title="Measure / Edit"
1598
- class="view-mode-button"
1599
- class:active={measure_menu_open}
1600
- aria-expanded={measure_menu_open}
1601
- style="transform: scale(1.2)"
1602
- >
1603
- {#if show_measure_selection_limit}
1604
- <span class="selection-limit-text">
1605
- {measured_sites.length}/{MAX_SELECTED_SITES}
1606
- </span>
1607
- {:else}
1608
- <Icon
1609
- icon={({
1594
+ <Icon
1595
+ icon={(
1596
+ {
1610
1597
  distance: `Ruler`,
1611
1598
  angle: `Angle`,
1612
1599
  'edit-bonds': `Link`,
1613
1600
  'edit-atoms': `Edit`,
1614
- } as const)[measure_mode]}
1615
- />
1616
- {/if}
1617
- <Icon
1618
- icon="Arrow{measure_menu_open ? `Up` : `Down`}"
1619
- style="margin-left: -2px"
1601
+ } as const
1602
+ )[measure_mode]}
1620
1603
  />
1621
- </button>
1622
- {#if show_selection_reset}
1623
- <button
1624
- type="button"
1625
- aria-label="Reset selection and bond edits"
1626
- onclick={() => {
1627
- clear_selection()
1628
- clear_bond_edits()
1629
- }}
1630
- >
1631
- <Icon icon="Reset" style="margin-left: -4px" />
1632
- </button>
1633
- {/if}
1634
- {#if measure_menu_open}
1635
- <div class="view-mode-dropdown">
1636
- {#each [
1637
- { mode: `distance`, icon: `Ruler`, label: `Distance`, scale: 1.1 },
1638
- { mode: `angle`, icon: `Angle`, label: `Angle`, scale: 1.3 },
1639
- { mode: `edit-atoms`, icon: `Edit`, label: `Edit Atoms`, scale: 1.0 },
1640
- { mode: `edit-bonds`, icon: `Link`, label: `Edit Bonds`, scale: 1.0 },
1641
- ] as const as { mode, icon, label, scale } (mode)}
1642
- <button
1643
- class="view-mode-option"
1644
- class:selected={measure_mode === mode}
1645
- disabled={mode === `edit-bonds` && !bond_edits_enabled}
1646
- title={mode === `edit-bonds` && !bond_edits_enabled
1647
- ? `Bond editing is only available for the original 1x1x1 cell`
1648
- : label}
1649
- onclick={() => {
1650
- if (mode === `edit-bonds` && !bond_edits_enabled) return
1651
- ;[measure_mode, measure_menu_open] = [mode, false]
1652
- }}
1653
- >
1654
- <Icon {icon} style="transform: scale({scale})" />
1655
- <span>{label}</span>
1656
- </button>
1657
- {/each}
1658
- </div>
1659
1604
  {/if}
1660
- </div>
1661
-
1662
- <!-- Undo/redo buttons (only in edit-atoms mode) -->
1663
- {#if measure_mode === `edit-atoms`}
1664
- <div class="undo-redo-container">
1665
- <button
1666
- type="button"
1667
- aria-label="Undo (Cmd/Ctrl+Z)"
1668
- disabled={undo_stack.length === 0}
1669
- onclick={undo}
1670
- title="Undo (Cmd/Ctrl+Z)"
1671
- class="undo-redo-btn"
1672
- >
1673
- <Icon icon="Undo" />
1674
- {#if undo_stack.length > 0}
1675
- <span class="history-count">{undo_stack.length}</span>
1676
- {/if}
1677
- </button>
1678
- <button
1679
- type="button"
1680
- aria-label="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1681
- disabled={redo_stack.length === 0}
1682
- onclick={redo}
1683
- title="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1684
- class="undo-redo-btn"
1685
- >
1686
- <Icon icon="Redo" />
1687
- {#if redo_stack.length > 0}
1688
- <span class="history-count">{redo_stack.length}</span>
1689
- {/if}
1690
- </button>
1691
- </div>
1605
+ <Icon icon="Arrow{measure_menu_open ? `Up` : `Down`}" style="margin-left: -2px" />
1606
+ </button>
1607
+ {#if show_selection_reset}
1608
+ <button
1609
+ type="button"
1610
+ aria-label="Reset selection and bond edits"
1611
+ onclick={() => {
1612
+ clear_selection()
1613
+ clear_bond_edits()
1614
+ }}
1615
+ >
1616
+ <Icon icon="Reset" style="margin-left: -4px" />
1617
+ </button>
1692
1618
  {/if}
1693
-
1694
- {#if measure_mode === `edit-bonds`}
1695
- <div class="bond-edit-toolbar" aria-label="Bond editing controls">
1696
- {#if bond_edit_mode === `add`}
1697
- <label>
1698
- <span>Bond order</span>
1699
- <select bind:value={bond_edit_order}>
1700
- {#each BOND_ORDER_OPTIONS as { order, label } (label)}
1701
- <option value={order}>{label}</option>
1702
- {/each}
1703
- </select>
1704
- </label>
1705
- {/if}
1706
- <div class="bond-edit-mode-toggle">
1707
- {#each [
1708
- { mode: `add`, label: `Add`, title: `Add: click two atoms` },
1709
- { mode: `delete`, label: `Delete`, title: `Delete: click a bond` },
1710
- ] as const as { mode, label, title } (mode)}
1711
- <button
1712
- type="button"
1713
- class:selected={bond_edit_mode === mode}
1714
- aria-pressed={bond_edit_mode === mode}
1715
- title="{title} ({label[0]})"
1716
- onclick={() => (bond_edit_mode = mode)}
1717
- >
1718
- {label}
1719
- </button>
1720
- {/each}
1721
- </div>
1722
- </div>
1723
- <div class="undo-redo-container">
1724
- <button
1725
- type="button"
1726
- aria-label="Undo bond edit (Cmd/Ctrl+Z)"
1727
- disabled={bond_undo_stack.length === 0}
1728
- onclick={undo_bond_edit}
1729
- title="Undo bond edit (Cmd/Ctrl+Z)"
1730
- class="undo-redo-btn"
1731
- >
1732
- <Icon icon="Undo" />
1733
- {#if bond_undo_stack.length > 0}
1734
- <span class="history-count">{bond_undo_stack.length}</span>
1735
- {/if}
1736
- </button>
1737
- <button
1738
- type="button"
1739
- aria-label="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1740
- disabled={bond_redo_stack.length === 0}
1741
- onclick={redo_bond_edit}
1742
- title="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1743
- class="undo-redo-btn"
1744
- >
1745
- <Icon icon="Redo" />
1746
- {#if bond_redo_stack.length > 0}
1747
- <span class="history-count">{bond_redo_stack.length}</span>
1748
- {/if}
1749
- </button>
1619
+ {#if measure_menu_open}
1620
+ <div class="view-mode-dropdown">
1621
+ {#each [{ mode: `distance`, icon: `Ruler`, label: `Distance`, scale: 1.1 }, { mode: `angle`, icon: `Angle`, label: `Angle`, scale: 1.3 }, { mode: `edit-atoms`, icon: `Edit`, label: `Edit Atoms`, scale: 1.0 }, { mode: `edit-bonds`, icon: `Link`, label: `Edit Bonds`, scale: 1.0 }] as const as { mode, icon, label, scale } (mode)}
1622
+ <button
1623
+ class="view-mode-option"
1624
+ class:selected={measure_mode === mode}
1625
+ disabled={mode === `edit-bonds` && !bond_edits_enabled}
1626
+ title={mode === `edit-bonds` && !bond_edits_enabled
1627
+ ? `Bond editing is only available for the original 1x1x1 cell`
1628
+ : label}
1629
+ onclick={() => {
1630
+ if (mode === `edit-bonds` && !bond_edits_enabled) return
1631
+ ;[measure_mode, measure_menu_open] = [mode, false]
1632
+ }}
1633
+ >
1634
+ <Icon {icon} style="transform: scale({scale})" />
1635
+ <span>{label}</span>
1636
+ </button>
1637
+ {/each}
1750
1638
  </div>
1751
1639
  {/if}
1640
+ </div>
1752
1641
 
1753
- <!-- Add-atom element input (shown when add_atom_mode is active) -->
1754
- {#if measure_mode === `edit-atoms` && add_atom_mode}
1755
- <div class="add-atom-input">
1756
- <label>
1757
- <span>Element:</span>
1758
- <input
1759
- type="text"
1760
- bind:value={add_element}
1761
- maxlength="2"
1762
- placeholder="C"
1763
- style="width: 3em; text-align: center"
1764
- />
1765
- </label>
1766
- <span style="font-size: 0.75em; opacity: 0.7">Click to place</span>
1767
- </div>
1768
- {/if}
1642
+ <!-- Undo/redo buttons (only in edit-atoms mode) -->
1643
+ {#if measure_mode === `edit-atoms`}
1644
+ <div class="undo-redo-container">
1645
+ <button
1646
+ type="button"
1647
+ aria-label="Undo (Cmd/Ctrl+Z)"
1648
+ disabled={undo_stack.length === 0}
1649
+ onclick={undo}
1650
+ title="Undo (Cmd/Ctrl+Z)"
1651
+ class="undo-redo-btn"
1652
+ >
1653
+ <Icon icon="Undo" />
1654
+ {#if undo_stack.length > 0}
1655
+ <span class="history-count">{undo_stack.length}</span>
1656
+ {/if}
1657
+ </button>
1658
+ <button
1659
+ type="button"
1660
+ aria-label="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1661
+ disabled={redo_stack.length === 0}
1662
+ onclick={redo}
1663
+ title="Redo (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1664
+ class="undo-redo-btn"
1665
+ >
1666
+ <Icon icon="Redo" />
1667
+ {#if redo_stack.length > 0}
1668
+ <span class="history-count">{redo_stack.length}</span>
1669
+ {/if}
1670
+ </button>
1671
+ </div>
1672
+ {/if}
1769
1673
 
1770
- <!-- Change-element input (shown when 'e' pressed with selection) -->
1771
- {#if measure_mode === `edit-atoms` && change_element_mode &&
1772
- selected_sites.length > 0}
1773
- <div class="add-atom-input">
1674
+ {#if measure_mode === `edit-bonds`}
1675
+ <div class="bond-edit-toolbar" aria-label="Bond editing controls">
1676
+ {#if bond_edit_mode === `add`}
1774
1677
  <label>
1775
- <span>New element:</span>
1776
- <input
1777
- type="text"
1778
- bind:value={change_element_value}
1779
- maxlength="2"
1780
- placeholder="Fe"
1781
- style="width: 3em; text-align: center"
1782
- onkeydown={(event: KeyboardEvent) => {
1783
- if (event.key === `Enter`) {
1784
- handle_change_element(change_element_value)
1785
- } else if (event.key === `Escape`) {
1786
- change_element_mode = false
1787
- }
1788
- event.stopPropagation()
1789
- }}
1790
- {@attach (node: HTMLInputElement) => {
1791
- node.focus()
1792
- }}
1793
- />
1678
+ <span>Bond order</span>
1679
+ <select bind:value={bond_edit_order}>
1680
+ {#each BOND_ORDER_OPTIONS as { order, label } (label)}
1681
+ <option value={order}>{label}</option>
1682
+ {/each}
1683
+ </select>
1794
1684
  </label>
1795
- <span style="font-size: 0.75em; opacity: 0.7">Enter to apply</span>
1685
+ {/if}
1686
+ <div class="bond-edit-mode-toggle">
1687
+ {#each [{ mode: `add`, label: `Add`, title: `Add: click two atoms` }, { mode: `delete`, label: `Delete`, title: `Delete: click a bond` }] as const as { mode, label, title } (mode)}
1688
+ <button
1689
+ type="button"
1690
+ class:selected={bond_edit_mode === mode}
1691
+ aria-pressed={bond_edit_mode === mode}
1692
+ title="{title} ({label[0]})"
1693
+ onclick={() => (bond_edit_mode = mode)}
1694
+ >
1695
+ {label}
1696
+ </button>
1697
+ {/each}
1796
1698
  </div>
1797
- {/if}
1699
+ </div>
1700
+ <div class="undo-redo-container">
1701
+ <button
1702
+ type="button"
1703
+ aria-label="Undo bond edit (Cmd/Ctrl+Z)"
1704
+ disabled={bond_undo_stack.length === 0}
1705
+ onclick={undo_bond_edit}
1706
+ title="Undo bond edit (Cmd/Ctrl+Z)"
1707
+ class="undo-redo-btn"
1708
+ >
1709
+ <Icon icon="Undo" />
1710
+ {#if bond_undo_stack.length > 0}
1711
+ <span class="history-count">{bond_undo_stack.length}</span>
1712
+ {/if}
1713
+ </button>
1714
+ <button
1715
+ type="button"
1716
+ aria-label="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1717
+ disabled={bond_redo_stack.length === 0}
1718
+ onclick={redo_bond_edit}
1719
+ title="Redo bond edit (Cmd/Ctrl+Y or Cmd+Shift+Z)"
1720
+ class="undo-redo-btn"
1721
+ >
1722
+ <Icon icon="Redo" />
1723
+ {#if bond_redo_stack.length > 0}
1724
+ <span class="history-count">{bond_redo_stack.length}</span>
1725
+ {/if}
1726
+ </button>
1727
+ </div>
1798
1728
  {/if}
1799
1729
 
1800
- {#if enable_info_pane && normalized_structure &&
1801
- controls_config.visible(`info-pane`)}
1802
- <StructureInfoPane
1803
- structure={normalized_structure}
1804
- bind:pane_open={info_pane_open}
1805
- bind:highlighted_sites
1806
- bind:hovered_site_idx
1807
- bind:selected_sites
1808
- {sym_data}
1809
- {@attach tooltip({ content: `Structure info pane` })}
1810
- />
1730
+ <!-- Add-atom element input (shown when add_atom_mode is active) -->
1731
+ {#if measure_mode === `edit-atoms` && add_atom_mode}
1732
+ <div class="add-atom-input">
1733
+ <label>
1734
+ <span>Element:</span>
1735
+ <input
1736
+ type="text"
1737
+ bind:value={add_element}
1738
+ maxlength="2"
1739
+ placeholder="C"
1740
+ style="width: 3em; text-align: center"
1741
+ />
1742
+ </label>
1743
+ <span style="font-size: 0.75em; opacity: 0.7">Click to place</span>
1744
+ </div>
1811
1745
  {/if}
1812
1746
 
1813
- {#if controls_config.visible(`export-pane`)}
1814
- <StructureExportPane
1815
- bind:export_pane_open
1816
- structure={normalized_structure}
1817
- {wrapper}
1818
- {scene}
1819
- {camera}
1820
- bind:png_dpi
1821
- pane_props={{ style: `max-height: calc(${height}px - 50px)` }}
1822
- />
1747
+ <!-- Change-element input (shown when 'e' pressed with selection) -->
1748
+ {#if measure_mode === `edit-atoms` && change_element_mode && selected_sites.length > 0}
1749
+ <div class="add-atom-input">
1750
+ <label>
1751
+ <span>New element:</span>
1752
+ <input
1753
+ type="text"
1754
+ bind:value={change_element_value}
1755
+ maxlength="2"
1756
+ placeholder="Fe"
1757
+ style="width: 3em; text-align: center"
1758
+ onkeydown={(event: KeyboardEvent) => {
1759
+ if (event.key === `Enter`) {
1760
+ handle_change_element(change_element_value)
1761
+ } else if (event.key === `Escape`) {
1762
+ change_element_mode = false
1763
+ }
1764
+ event.stopPropagation()
1765
+ }}
1766
+ {@attach (node: HTMLInputElement) => {
1767
+ node.focus()
1768
+ }}
1769
+ />
1770
+ </label>
1771
+ <span style="font-size: 0.75em; opacity: 0.7">Enter to apply</span>
1772
+ </div>
1823
1773
  {/if}
1774
+ {/if}
1824
1775
 
1825
- {#if controls_config.visible(`controls`)}
1826
- <StructureControls
1827
- bind:controls_open
1828
- bind:scene_props
1829
- bind:lattice_props
1830
- bind:show_image_atoms
1831
- bind:supercell_scaling
1832
- bind:background_color
1833
- bind:background_opacity
1834
- bind:color_scheme
1835
- bind:atom_color_config
1836
- bind:cell_type
1837
- bind:volumetric_data
1838
- bind:isosurface_settings
1839
- bind:active_volume_idx
1840
- {structure}
1841
- {supercell_loading}
1842
- {sym_data}
1843
- />
1844
- {/if}
1776
+ {#if enable_info_pane && normalized_structure && controls_config.visible(`info-pane`)}
1777
+ <StructureInfoPane
1778
+ structure={normalized_structure}
1779
+ bind:pane_open={info_pane_open}
1780
+ bind:highlighted_sites
1781
+ bind:hovered_site_idx
1782
+ bind:selected_sites
1783
+ {sym_data}
1784
+ {@attach tooltip({ content: `Structure info pane` })}
1785
+ />
1786
+ {/if}
1845
1787
 
1846
- {@render top_right_controls?.()}
1788
+ {#if controls_config.visible(`export-pane`)}
1789
+ <StructureExportPane
1790
+ bind:export_pane_open
1791
+ structure={normalized_structure}
1792
+ {wrapper}
1793
+ {scene}
1794
+ {camera}
1795
+ bind:png_dpi
1796
+ pane_props={{ style: `max-height: calc(${height}px - 50px)` }}
1797
+ />
1798
+ {/if}
1799
+
1800
+ {#if controls_config.visible(`controls`)}
1801
+ <StructureControls
1802
+ bind:controls_open
1803
+ bind:scene_props
1804
+ bind:lattice_props
1805
+ bind:show_image_atoms
1806
+ bind:supercell_scaling
1807
+ bind:background_color
1808
+ bind:background_opacity
1809
+ bind:color_scheme
1810
+ bind:atom_color_config
1811
+ bind:cell_type
1812
+ bind:volumetric_data
1813
+ bind:isosurface_settings
1814
+ bind:active_volume_idx
1815
+ {structure}
1816
+ {supercell_loading}
1817
+ {sym_data}
1818
+ {polyhedra_rendered_elements}
1819
+ />
1847
1820
  {/if}
1848
- </section>
1821
+
1822
+ {@render top_right_controls?.()}
1823
+ </ViewerChrome>
1849
1824
 
1850
1825
  <AtomLegend
1851
1826
  bind:atom_color_config
@@ -1857,74 +1832,108 @@
1857
1832
  bind:element_radius_overrides
1858
1833
  bind:site_radius_overrides
1859
1834
  selected_sites={atom_legend_selected_sites}
1860
- structure={displayed_structure}
1835
+ structure={internal_displayed_structure}
1861
1836
  show_mode_toggle={viewer_active}
1862
1837
  {sym_data}
1863
1838
  >
1864
- {#if structure && `lattice` in structure}
1865
- <CellSelect
1866
- bind:supercell_scaling
1867
- bind:cell_type
1868
- {sym_data}
1869
- loading={supercell_loading}
1870
- direction="up"
1871
- />
1872
- {/if}
1839
+ {#snippet children({ mode_menu_open })}
1840
+ {#if structure && `lattice` in structure}
1841
+ <CellSelect
1842
+ bind:supercell_scaling
1843
+ bind:cell_type
1844
+ {sym_data}
1845
+ loading={supercell_loading}
1846
+ direction="up"
1847
+ suppress_hover={mode_menu_open}
1848
+ />
1849
+ {/if}
1850
+ {/snippet}
1873
1851
  </AtomLegend>
1874
1852
 
1853
+ <!-- One StructureViewport renders the single view; four render the 2x2 multi-view.
1854
+ The primary pane (index 0) carries the external camera API: scene/camera are bound
1855
+ out for export and camera_position/target persist into scene_props, and it emits
1856
+ on_camera_move/on_camera_reset. All camera handling itself lives in StructureViewport. -->
1857
+ {#snippet primary_viewport(view: StructureView)}
1858
+ <StructureViewport
1859
+ in_grid={multi_view}
1860
+ label={multi_view ? view.label : undefined}
1861
+ active={multi_view && active_pane_idx === 0}
1862
+ interactive={!multi_view || active_pane_idx === 0}
1863
+ onactivate={() => (active_pane_idx = 0)}
1864
+ {reset_token}
1865
+ report_moved={(moved) => (moved ? moved_panes.add(0) : moved_panes.delete(0))}
1866
+ {on_camera_move}
1867
+ {on_camera_reset}
1868
+ {...shared_viewport_props}
1869
+ camera_direction={view.direction}
1870
+ camera_projection={view.projection ?? scene_props.camera_projection}
1871
+ bind:camera_position={scene_props.camera_position}
1872
+ bind:camera_target={scene_props.camera_target}
1873
+ bind:scene
1874
+ bind:camera
1875
+ bind:selected_sites
1876
+ bind:measured_sites
1877
+ bind:hovered_site_idx
1878
+ bind:hidden_elements
1879
+ bind:hidden_prop_vals
1880
+ bind:element_radius_overrides
1881
+ bind:site_radius_overrides
1882
+ bind:added_bonds
1883
+ bind:removed_bonds
1884
+ bind:bond_order_overrides
1885
+ bind:bond_edit_mode
1886
+ bind:add_atom_mode
1887
+ bind:add_element
1888
+ bind:dragging_atoms
1889
+ bind:polyhedra_rendered_elements
1890
+ />
1891
+ {/snippet}
1892
+
1893
+ {#snippet extra_viewport(view: StructureView, pane_idx: number)}
1894
+ <StructureViewport
1895
+ in_grid
1896
+ label={view.label}
1897
+ active={active_pane_idx === pane_idx}
1898
+ interactive={active_pane_idx === pane_idx}
1899
+ onactivate={() => (active_pane_idx = pane_idx)}
1900
+ {reset_token}
1901
+ report_moved={(moved) =>
1902
+ moved ? moved_panes.add(pane_idx) : moved_panes.delete(pane_idx)}
1903
+ {...shared_viewport_props}
1904
+ camera_direction={view.direction}
1905
+ camera_projection={view.projection ?? scene_props.camera_projection}
1906
+ bind:selected_sites
1907
+ bind:measured_sites
1908
+ bind:hovered_site_idx
1909
+ bind:hidden_elements
1910
+ bind:hidden_prop_vals
1911
+ bind:element_radius_overrides
1912
+ bind:site_radius_overrides
1913
+ bind:added_bonds
1914
+ bind:removed_bonds
1915
+ bind:bond_order_overrides
1916
+ bind:bond_edit_mode
1917
+ bind:add_atom_mode
1918
+ bind:add_element
1919
+ bind:dragging_atoms
1920
+ />
1921
+ {/snippet}
1922
+
1875
1923
  <!-- prevent from rendering in vitest runner since WebGLRenderingContext not available -->
1876
1924
  {#if typeof WebGLRenderingContext !== `undefined`}
1877
- <!-- prevent HTML labels from rendering outside of the canvas -->
1878
- <div style="overflow: hidden; height: 100%; width: 100%">
1879
- <Canvas>
1880
- <StructureScene
1881
- structure={displayed_structure}
1882
- base_structure={cell_transformed_structure}
1883
- {...scene_props}
1884
- gizmo={scene_gizmo}
1885
- {lattice_props}
1886
- volumetric_data={supercell_volume}
1887
- {isosurface_settings}
1888
- bind:camera_is_moving
1889
- bind:selected_sites
1890
- active_sites={active_scene_sites}
1891
- bind:hovered_idx={hovered_site_idx}
1892
- bind:measured_sites
1893
- bind:scene
1894
- bind:camera
1895
- bind:orbit_controls
1896
- bind:rotation_target_ref
1897
- bind:initial_computed_zoom
1898
- bind:hidden_elements
1899
- bind:hidden_prop_vals
1900
- bind:element_radius_overrides
1901
- bind:site_radius_overrides
1902
- bind:added_bonds
1903
- bind:removed_bonds
1904
- bind:bond_order_overrides
1905
- {bond_edits_enabled}
1906
- bind:bond_edit_mode
1907
- {bond_edit_order}
1908
- {measure_mode}
1909
- {width}
1910
- {height}
1911
- {atom_color_config}
1912
- {sym_data}
1913
- on_sites_moved={handle_sites_moved}
1914
- on_operation_start={push_undo}
1915
- on_bond_edit_start={push_bond_undo}
1916
- on_add_atom={handle_add_atom}
1917
- bind:add_atom_mode
1918
- bind:add_element
1919
- bind:cursor={canvas_cursor}
1920
- bind:dragging_atoms
1921
- />
1922
- </Canvas>
1925
+ <div class:multi={multi_view} class="viewport-stage">
1926
+ {@render primary_viewport(multi_view ? (views[0] ?? {}) : {})}
1927
+ {#if multi_view}
1928
+ {#each views.slice(1) as view, idx (idx)}
1929
+ {@render extra_viewport(view, idx + 1)}
1930
+ {/each}
1931
+ {/if}
1923
1932
  </div>
1924
1933
  {/if}
1925
1934
 
1926
1935
  <div class="bottom-left">
1927
- {@render bottom_left?.({ structure: displayed_structure })}
1936
+ {@render bottom_left?.({ structure: internal_displayed_structure })}
1928
1937
  </div>
1929
1938
 
1930
1939
  {#if toast_msg}
@@ -1934,9 +1943,7 @@
1934
1943
  {#if symmetry_error}
1935
1944
  <div class="symmetry-error">
1936
1945
  <span>{symmetry_error}</span>
1937
- <button onclick={() => (symmetry_error = undefined)} aria-label="Dismiss">
1938
- ×
1939
- </button>
1946
+ <button onclick={() => (symmetry_error = undefined)} aria-label="Dismiss"> × </button>
1940
1947
  </div>
1941
1948
  {/if}
1942
1949
  {:else if structure}
@@ -1966,7 +1973,9 @@
1966
1973
  background: var(--struct-bg-fullscreen, var(--struct-bg));
1967
1974
  overflow: hidden;
1968
1975
  }
1969
- .structure:fullscreen :global(canvas) {
1976
+ /* Single view: stretch the lone canvas to the full screen in fullscreen mode.
1977
+ In multi-view the grid fills the screen and each canvas fills its 1fr cell. */
1978
+ .structure:fullscreen:not(.multi-view) :global(canvas) {
1970
1979
  height: 100vh !important;
1971
1980
  width: 100vw !important;
1972
1981
  }
@@ -1974,6 +1983,22 @@
1974
1983
  background: var(--struct-dragover-bg, var(--dragover-bg));
1975
1984
  border: var(--struct-dragover-border, var(--dragover-border));
1976
1985
  }
1986
+ .viewport-stage {
1987
+ height: 100%;
1988
+ width: 100%;
1989
+ }
1990
+ /* 2x2 multi-side view grid: four equal subcanvases. grid-auto-rows keeps rows
1991
+ equal-height if a custom `views` array supplies more than four entries. */
1992
+ .viewport-stage.multi {
1993
+ display: grid;
1994
+ grid-template-columns: 1fr 1fr;
1995
+ grid-template-rows: 1fr 1fr;
1996
+ grid-auto-rows: 1fr;
1997
+ gap: var(--struct-viewport-gap, 2px);
1998
+ }
1999
+ .multi-view-toggle.active {
2000
+ color: var(--accent-color, #4a9eff);
2001
+ }
1977
2002
  /* Ensure canvas is transparent so the themed --struct-bg shows through */
1978
2003
  .structure :global(canvas) {
1979
2004
  background: transparent;
@@ -1986,7 +2011,7 @@
1986
2011
  }
1987
2012
  /* Avoid accidental text selection while interacting with the viewer */
1988
2013
  .structure :global(canvas),
1989
- .structure section.control-buttons,
2014
+ .structure :global(section.control-buttons),
1990
2015
  .structure .bottom-left {
1991
2016
  user-select: none;
1992
2017
  }
@@ -1997,44 +2022,6 @@
1997
2022
  font-size: var(--struct-bottom-left-font-size, 1.2em);
1998
2023
  padding: var(--struct-bottom-left-padding, 1pt 5pt);
1999
2024
  }
2000
- section.control-buttons {
2001
- position: absolute;
2002
- display: flex;
2003
- top: var(--struct-buttons-top, var(--ctrl-btn-top, 1ex));
2004
- right: var(--struct-buttons-right, var(--ctrl-btn-right, 1ex));
2005
- gap: 4pt;
2006
- /* buttons need higher z-index than AtomLegend to make info/controls panes occlude legend */
2007
- /* we also need crazy high z-index to make info/control pane occlude threlte/extras' <HTML> elements for site labels */
2008
- z-index: var(
2009
- --struct-buttons-z-index,
2010
- var(--z-index-overlay-controls, 100000000)
2011
- );
2012
- opacity: 0;
2013
- pointer-events: none;
2014
- transition: opacity 0.2s ease;
2015
- }
2016
- /* Mode: always - controls always visible */
2017
- section.control-buttons.always-visible {
2018
- opacity: 1;
2019
- pointer-events: auto;
2020
- }
2021
- /* Mode: hover - controls visible on component hover */
2022
- .structure:hover section.control-buttons.hover-visible,
2023
- .structure:focus-within section.control-buttons.hover-visible {
2024
- opacity: 1;
2025
- pointer-events: auto;
2026
- }
2027
- /* Mode: never - stays hidden (default state, no additional CSS needed) */
2028
- section.control-buttons > :global(button) {
2029
- background-color: transparent;
2030
- display: flex;
2031
- padding: 1px 6px;
2032
- border-radius: var(--border-radius, 3pt);
2033
- font-size: clamp(0.85em, 2cqmin, 1.3em);
2034
- }
2035
- section.control-buttons :global(button:hover) {
2036
- background-color: color-mix(in srgb, currentColor 8%, transparent);
2037
- }
2038
2025
  /* Match Trajectory dropdown UI */
2039
2026
  .view-mode-dropdown {
2040
2027
  position: absolute;
@@ -2042,7 +2029,9 @@
2042
2029
  right: 0;
2043
2030
  background: var(--surface-bg);
2044
2031
  border-radius: var(--border-radius, 3pt);
2045
- box-shadow: 0 8px 16px -4px rgba(0, 0, 0, 0.3), 0 4px 8px -2px rgba(0, 0, 0, 0.1);
2032
+ box-shadow:
2033
+ 0 8px 16px -4px rgba(0, 0, 0, 0.3),
2034
+ 0 4px 8px -2px rgba(0, 0, 0, 0.1);
2046
2035
  display: flex;
2047
2036
  flex-direction: column;
2048
2037
  }
@@ -2081,7 +2070,7 @@
2081
2070
  .measure-mode-dropdown > button {
2082
2071
  background: transparent;
2083
2072
  padding: 1px 6px;
2084
- font-size: clamp(0.85em, 2cqmin, 1.3em);
2073
+ font-size: var(--ctrl-btn-icon-size, clamp(0.7rem, 2cqmin, 0.85rem));
2085
2074
  }
2086
2075
  .selection-limit-text {
2087
2076
  font-weight: bold;
@@ -2143,7 +2132,8 @@
2143
2132
  opacity: 0;
2144
2133
  }
2145
2134
  @keyframes toast-fade {
2146
- 0%, 70% {
2135
+ 0%,
2136
+ 70% {
2147
2137
  opacity: 1;
2148
2138
  }
2149
2139
  100% {