matterviz 0.4.2 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/dist/FilePicker.svelte +40 -50
  2. package/dist/api/optimade.js +10 -7
  3. package/dist/app.css +15 -0
  4. package/dist/brillouin/BrillouinZone.svelte +18 -9
  5. package/dist/brillouin/BrillouinZoneScene.svelte +20 -29
  6. package/dist/brillouin/compute.js +10 -16
  7. package/dist/brillouin/geometry.js +2 -2
  8. package/dist/chempot-diagram/ChemPotDiagram.svelte +18 -15
  9. package/dist/chempot-diagram/ChemPotDiagram2D.svelte +17 -113
  10. package/dist/chempot-diagram/ChemPotDiagram3D.svelte +104 -264
  11. package/dist/chempot-diagram/async-compute.svelte.js +12 -13
  12. package/dist/chempot-diagram/color.d.ts +21 -1
  13. package/dist/chempot-diagram/color.js +70 -1
  14. package/dist/chempot-diagram/compute.d.ts +7 -0
  15. package/dist/chempot-diagram/compute.js +85 -26
  16. package/dist/chempot-diagram/export.js +1 -6
  17. package/dist/chempot-diagram/temperature.js +9 -10
  18. package/dist/composition/BarChart.svelte +7 -9
  19. package/dist/composition/Composition.svelte +3 -8
  20. package/dist/composition/Formula.svelte +2 -2
  21. package/dist/composition/FormulaFilter.svelte +27 -93
  22. package/dist/composition/format.js +6 -8
  23. package/dist/composition/parse.js +50 -105
  24. package/dist/constants.d.ts +0 -1
  25. package/dist/constants.js +6 -17
  26. package/dist/convex-hull/ConvexHull2D.svelte +0 -2
  27. package/dist/convex-hull/ConvexHull3D.svelte +19 -47
  28. package/dist/convex-hull/ConvexHull4D.svelte +65 -95
  29. package/dist/convex-hull/StructurePopup.svelte +1 -1
  30. package/dist/convex-hull/gas-thermodynamics.js +21 -53
  31. package/dist/convex-hull/helpers.js +1 -7
  32. package/dist/convex-hull/thermodynamics.js +19 -27
  33. package/dist/coordination/CoordinationBarPlot.svelte +12 -21
  34. package/dist/element/BohrAtom.svelte +0 -1
  35. package/dist/element/ElementTile.svelte +26 -44
  36. package/dist/element/data.d.ts +2 -0
  37. package/dist/element/data.js +5 -1
  38. package/dist/element/index.d.ts +1 -1
  39. package/dist/element/index.js +1 -1
  40. package/dist/fermi-surface/FermiSurface.svelte +22 -23
  41. package/dist/fermi-surface/FermiSurface.svelte.d.ts +2 -6
  42. package/dist/fermi-surface/FermiSurfaceScene.svelte +32 -77
  43. package/dist/fermi-surface/compute.js +22 -55
  44. package/dist/fermi-surface/parse.js +38 -70
  45. package/dist/file-viewer/JsonBrowser.svelte +1043 -0
  46. package/dist/file-viewer/JsonBrowser.svelte.d.ts +9 -0
  47. package/dist/file-viewer/PlotPanel.svelte +350 -0
  48. package/dist/file-viewer/PlotPanel.svelte.d.ts +9 -0
  49. package/dist/file-viewer/TrajectoryWithDos.svelte +30 -0
  50. package/dist/file-viewer/TrajectoryWithDos.svelte.d.ts +10 -0
  51. package/dist/file-viewer/detect.d.ts +11 -0
  52. package/dist/file-viewer/detect.js +381 -0
  53. package/dist/file-viewer/eligibility.d.ts +4 -0
  54. package/dist/file-viewer/eligibility.js +53 -0
  55. package/dist/file-viewer/host-protocol.d.ts +50 -0
  56. package/dist/file-viewer/host-transfer.d.ts +25 -0
  57. package/dist/file-viewer/host-transfer.js +42 -0
  58. package/dist/file-viewer/index.d.ts +5 -0
  59. package/dist/file-viewer/index.js +5 -0
  60. package/dist/file-viewer/main.d.ts +44 -0
  61. package/dist/file-viewer/main.js +535 -0
  62. package/dist/file-viewer/parse.d.ts +9 -0
  63. package/dist/file-viewer/parse.js +170 -0
  64. package/dist/file-viewer/plot-utils.d.ts +24 -0
  65. package/dist/file-viewer/plot-utils.js +191 -0
  66. package/dist/file-viewer/types.d.ts +4 -0
  67. package/dist/file-viewer/types.js +5 -0
  68. package/dist/heatmap-matrix/HeatmapMatrix.svelte +57 -84
  69. package/dist/heatmap-matrix/HeatmapMatrixControls.svelte.d.ts +1 -1
  70. package/dist/index.d.ts +2 -2
  71. package/dist/index.js +1 -1
  72. package/dist/io/ExportPane.svelte +1 -1
  73. package/dist/io/decompress.d.ts +2 -0
  74. package/dist/io/decompress.js +3 -2
  75. package/dist/io/export.d.ts +7 -4
  76. package/dist/io/export.js +70 -18
  77. package/dist/io/file-drop.js +60 -35
  78. package/dist/io/url-drop.d.ts +2 -0
  79. package/dist/io/url-drop.js +28 -16
  80. package/dist/isosurface/Isosurface.svelte +687 -196
  81. package/dist/isosurface/Isosurface.svelte.d.ts +7 -1
  82. package/dist/isosurface/IsosurfaceControls.svelte +564 -85
  83. package/dist/isosurface/IsosurfaceControls.svelte.d.ts +1 -1
  84. package/dist/isosurface/VolumeSlice.svelte +224 -0
  85. package/dist/isosurface/VolumeSlice.svelte.d.ts +28 -0
  86. package/dist/isosurface/coloring.d.ts +20 -0
  87. package/dist/isosurface/coloring.js +118 -0
  88. package/dist/isosurface/geometry-worker-types.d.ts +38 -0
  89. package/dist/isosurface/geometry.worker.d.ts +1 -0
  90. package/dist/isosurface/geometry.worker.js +59 -0
  91. package/dist/isosurface/grid.d.ts +10 -0
  92. package/dist/isosurface/grid.js +31 -0
  93. package/dist/isosurface/index.d.ts +4 -0
  94. package/dist/isosurface/index.js +4 -0
  95. package/dist/isosurface/profile.d.ts +12 -0
  96. package/dist/isosurface/profile.js +19 -0
  97. package/dist/isosurface/sampling.d.ts +34 -0
  98. package/dist/isosurface/sampling.js +408 -0
  99. package/dist/isosurface/slice-rendering.d.ts +13 -0
  100. package/dist/isosurface/slice-rendering.js +79 -0
  101. package/dist/isosurface/slice.d.ts +22 -2
  102. package/dist/isosurface/slice.js +200 -116
  103. package/dist/isosurface/types.d.ts +24 -1
  104. package/dist/isosurface/types.js +120 -8
  105. package/dist/json-path.d.ts +4 -0
  106. package/dist/json-path.js +89 -0
  107. package/dist/labels.js +1 -1
  108. package/dist/layout/InfoCard.svelte +1 -3
  109. package/dist/layout/SettingsSection.svelte +52 -61
  110. package/dist/layout/SettingsSection.svelte.d.ts +6 -7
  111. package/dist/layout/ViewerChrome.svelte +7 -1
  112. package/dist/layout/json-tree/JsonNode.svelte +14 -48
  113. package/dist/layout/json-tree/JsonTree.svelte +48 -95
  114. package/dist/layout/json-tree/JsonValue.svelte +3 -18
  115. package/dist/layout/json-tree/utils.d.ts +0 -3
  116. package/dist/layout/json-tree/utils.js +50 -179
  117. package/dist/marching-cubes.d.ts +8 -1
  118. package/dist/marching-cubes.js +115 -48
  119. package/dist/math.d.ts +3 -2
  120. package/dist/math.js +29 -38
  121. package/dist/overlays/ContextMenu.svelte +15 -32
  122. package/dist/overlays/DraggablePane.svelte +39 -58
  123. package/dist/overlays/DraggablePane.svelte.d.ts +1 -1
  124. package/dist/overlays/GlassChip.svelte +3 -1
  125. package/dist/periodic-table/PeriodicTableControls.svelte +26 -52
  126. package/dist/phase-diagram/IsobaricBinaryPhaseDiagram.svelte +18 -34
  127. package/dist/phase-diagram/build-diagram.js +1 -6
  128. package/dist/phase-diagram/svg-to-diagram.js +9 -14
  129. package/dist/plot/bar/BarPlot.svelte +38 -42
  130. package/dist/plot/bar/SpacegroupBarPlot.svelte +18 -42
  131. package/dist/plot/box/BoxPlot.svelte +27 -23
  132. package/dist/plot/core/auto-place.d.ts +1 -1
  133. package/dist/plot/core/auto-place.js +1 -1
  134. package/dist/plot/core/data-transform.d.ts +1 -2
  135. package/dist/plot/core/data-transform.js +0 -10
  136. package/dist/plot/core/layout.d.ts +12 -5
  137. package/dist/plot/core/layout.js +37 -27
  138. package/dist/plot/core/pan-zoom.svelte.d.ts +2 -4
  139. package/dist/plot/core/pan-zoom.svelte.js +34 -17
  140. package/dist/plot/core/placed-tween.svelte.d.ts +3 -0
  141. package/dist/plot/core/placed-tween.svelte.js +68 -21
  142. package/dist/plot/core/svg.js +0 -1
  143. package/dist/plot/core/utils/hierarchy-chart.d.ts +1 -0
  144. package/dist/plot/core/utils/hierarchy-chart.js +12 -0
  145. package/dist/plot/core/utils/hierarchy-labels.js +3 -2
  146. package/dist/plot/histogram/Histogram.svelte +21 -26
  147. package/dist/plot/sankey/sankey.js +13 -9
  148. package/dist/plot/scatter/BinnedScatterPlot.svelte +59 -43
  149. package/dist/plot/scatter/ScatterPlot.svelte +67 -52
  150. package/dist/plot/scatter/adaptive-density.d.ts +1 -1
  151. package/dist/plot/scatter/adaptive-density.js +36 -20
  152. package/dist/plot/sunburst/Sunburst.svelte +20 -20
  153. package/dist/plot/treemap/Treemap.svelte +175 -60
  154. package/dist/plot/treemap/Treemap.svelte.d.ts +6 -0
  155. package/dist/plot/treemap/index.d.ts +1 -0
  156. package/dist/plot/treemap/labels.d.ts +38 -0
  157. package/dist/plot/treemap/labels.js +80 -0
  158. package/dist/plot/treemap/treemap.d.ts +4 -2
  159. package/dist/plot/treemap/treemap.js +5 -2
  160. package/dist/rdf/RdfPlot.svelte +17 -35
  161. package/dist/rdf/calc-rdf.js +59 -44
  162. package/dist/rdf/index.d.ts +0 -1
  163. package/dist/sanitize.js +84 -26
  164. package/dist/scene/SceneCamera.svelte +1 -1
  165. package/dist/scene/SceneCamera.svelte.d.ts +1 -1
  166. package/dist/scene/index.d.ts +1 -2
  167. package/dist/scene/index.js +1 -1
  168. package/dist/scene/{props.js → props.svelte.js} +15 -1
  169. package/dist/settings.js +159 -482
  170. package/dist/spectral/Bands.svelte +151 -158
  171. package/dist/spectral/BrillouinBandsDos.svelte +4 -19
  172. package/dist/spectral/Dos.svelte +26 -24
  173. package/dist/spectral/helpers.js +40 -105
  174. package/dist/structure/AtomLegend.svelte +8 -13
  175. package/dist/structure/AtomLegend.svelte.d.ts +2 -3
  176. package/dist/structure/CellSelect.svelte +35 -10
  177. package/dist/structure/Structure.svelte +177 -99
  178. package/dist/structure/Structure.svelte.d.ts +1 -1
  179. package/dist/structure/StructureCarousel.svelte +10 -3
  180. package/dist/structure/StructureControls.svelte +8 -26
  181. package/dist/structure/StructureControls.svelte.d.ts +1 -1
  182. package/dist/structure/StructureInfoPane.svelte +65 -76
  183. package/dist/structure/StructureScene.svelte +75 -109
  184. package/dist/structure/StructureScene.svelte.d.ts +3 -1
  185. package/dist/structure/StructureViewport.svelte +120 -55
  186. package/dist/structure/StructureViewport.svelte.d.ts +3 -1
  187. package/dist/structure/atom-properties.d.ts +1 -1
  188. package/dist/structure/atom-properties.js +4 -3
  189. package/dist/structure/bond-order-perception.js +3 -8
  190. package/dist/structure/bonding.d.ts +0 -1
  191. package/dist/structure/bonding.js +16 -23
  192. package/dist/structure/export.js +50 -74
  193. package/dist/structure/format-detect.js +5 -7
  194. package/dist/structure/index.js +6 -12
  195. package/dist/structure/parse.js +38 -100
  196. package/dist/structure/partial-occupancy.js +7 -4
  197. package/dist/structure/pbc.js +8 -10
  198. package/dist/structure/polyhedra.js +5 -4
  199. package/dist/structure/prop-groups.d.ts +85 -0
  200. package/dist/structure/prop-groups.js +18 -0
  201. package/dist/symmetry/SymmetryStats.svelte +0 -4
  202. package/dist/symmetry/WyckoffTable.svelte +5 -4
  203. package/dist/symmetry/cell-transform.js +5 -14
  204. package/dist/symmetry/index.js +6 -9
  205. package/dist/table/HeatmapTable.svelte +61 -103
  206. package/dist/table/ToggleMenu.svelte +166 -44
  207. package/dist/table/ToggleMenu.svelte.d.ts +1 -1
  208. package/dist/theme/embedded.d.ts +18 -0
  209. package/dist/theme/embedded.js +206 -0
  210. package/dist/theme/index.d.ts +4 -0
  211. package/dist/theme/index.js +1 -2
  212. package/dist/theme/themes.mjs +31 -60
  213. package/dist/tooltip/TooltipContent.svelte +1 -1
  214. package/dist/tooltip/TooltipContent.svelte.d.ts +1 -1
  215. package/dist/tooltip/index.d.ts +8 -1
  216. package/dist/trajectory/Trajectory.svelte +140 -120
  217. package/dist/trajectory/TrajectoryError.svelte +1 -66
  218. package/dist/trajectory/TrajectoryExportPane.svelte +0 -1
  219. package/dist/trajectory/TrajectoryInfoPane.svelte +55 -80
  220. package/dist/trajectory/TrajectoryInfoPane.svelte.d.ts +2 -1
  221. package/dist/trajectory/extract.js +19 -29
  222. package/dist/trajectory/format-detect.d.ts +2 -0
  223. package/dist/trajectory/format-detect.js +7 -5
  224. package/dist/trajectory/frame-reader.js +3 -4
  225. package/dist/trajectory/helpers.js +16 -49
  226. package/dist/trajectory/index.js +2 -8
  227. package/dist/trajectory/parse/ase.js +1 -1
  228. package/dist/trajectory/parse/index.d.ts +4 -3
  229. package/dist/trajectory/parse/index.js +12 -17
  230. package/dist/trajectory/parse/vaspout-h5.d.ts +0 -1
  231. package/dist/trajectory/parse/vaspout-h5.js +1 -4
  232. package/dist/trajectory/parse/xyz.js +48 -2
  233. package/dist/trajectory/plotting.js +14 -37
  234. package/dist/xrd/XrdPlot.svelte +43 -68
  235. package/dist/xrd/calc-xrd.js +22 -58
  236. package/dist/xrd/parse.js +13 -22
  237. package/package.json +40 -8
  238. package/readme.md +6 -5
  239. package/dist/scene/types.d.ts +0 -26
  240. package/dist/tooltip/types.d.ts +0 -8
  241. package/dist/trajectory/constants.d.ts +0 -6
  242. package/dist/trajectory/constants.js +0 -11
  243. /package/dist/{scene/types.js → file-viewer/host-protocol.js} +0 -0
  244. /package/dist/{tooltip/types.js → isosurface/geometry-worker-types.js} +0 -0
@@ -8,6 +8,7 @@
8
8
  // callbacks so it drives Structure's external camera API. Camera state is per-pane:
9
9
  // the primary pane binds it back to Structure's scene_props, while side panes keep it local.
10
10
  import type { ElementSymbol } from '../element'
11
+ import { StatusMessage } from '../feedback'
11
12
  import type { IsosurfaceSettings, VolumetricData } from '../isosurface/types'
12
13
  import type { Vec3 } from '../math'
13
14
  import type { CameraProjection } from '../settings'
@@ -46,13 +47,46 @@
46
47
  }
47
48
  }
48
49
 
49
- const create_renderer = (canvas: HTMLCanvasElement) =>
50
- new StructureRenderer({
50
+ // Self-heal evicted WebGL contexts: browsers cap live contexts (~8-16 per
51
+ // page) and evict the oldest without notice, leaving its canvas permanently
52
+ // blank. three.js handles `webglcontextrestored` when the browser restores
53
+ // on its own; when it doesn't, remount the <Canvas> for a fresh context.
54
+ // Bounded retries avoid eviction ping-pong when over the context budget.
55
+ let canvas_remount_token = $state(0)
56
+ let remount_timer: ReturnType<typeof setTimeout> | undefined
57
+ let recovery_reset_timer: ReturnType<typeof setTimeout> | undefined
58
+ let recovery_attempts = 0
59
+ let recovery_failed = $state(false)
60
+
61
+ const create_renderer = (canvas: HTMLCanvasElement) => {
62
+ canvas.addEventListener(`webglcontextlost`, () => {
63
+ // canvas.isConnected is false when the loss came from our own dispose()
64
+ // (component unmounted), not from browser eviction
65
+ if (!canvas.isConnected) return
66
+ clearTimeout(recovery_reset_timer)
67
+ if (recovery_attempts >= 3) {
68
+ recovery_failed = true
69
+ return
70
+ }
71
+ recovery_attempts += 1
72
+ remount_timer = setTimeout(() => (canvas_remount_token += 1), 1000)
73
+ })
74
+ canvas.addEventListener(`webglcontextrestored`, () => {
75
+ clearTimeout(remount_timer)
76
+ recovery_attempts = 0
77
+ recovery_failed = false
78
+ })
79
+ const renderer = new StructureRenderer({
51
80
  canvas,
52
81
  powerPreference: `high-performance`,
53
82
  antialias: true,
54
83
  alpha: true,
55
84
  })
85
+ // Reset only after a stable remount; an immediate reset would allow
86
+ // endless eviction ping-pong while the page exceeds its context budget.
87
+ recovery_reset_timer = setTimeout(() => (recovery_attempts = 0), 5000)
88
+ return renderer
89
+ }
56
90
 
57
91
  let {
58
92
  // Multi-view chrome
@@ -74,6 +108,8 @@
74
108
  lattice_props = {},
75
109
  volumetric_data = undefined,
76
110
  isosurface_settings = undefined,
111
+ active_volume_idx = 0,
112
+ volume_scaling = [1, 1, 1],
77
113
  bond_edits_enabled = true,
78
114
  bond_edit_order = 1,
79
115
  measure_mode = `distance`,
@@ -126,8 +162,10 @@
126
162
  scene_props?: ComponentProps<typeof StructureScene>
127
163
  gizmo?: boolean | ComponentProps<typeof StructureScene>[`gizmo`]
128
164
  lattice_props?: ComponentProps<typeof StructureScene>[`lattice_props`]
129
- volumetric_data?: VolumetricData
165
+ volumetric_data?: VolumetricData | VolumetricData[]
130
166
  isosurface_settings?: IsosurfaceSettings
167
+ active_volume_idx?: number
168
+ volume_scaling?: Vec3
131
169
  bond_edits_enabled?: boolean
132
170
  bond_edit_order?: BondOrder
133
171
  measure_mode?: MeasureMode
@@ -275,6 +313,11 @@
275
313
  return
276
314
  reset_camera()
277
315
  }
316
+
317
+ $effect(() => () => {
318
+ clearTimeout(remount_timer)
319
+ clearTimeout(recovery_reset_timer)
320
+ })
278
321
  </script>
279
322
 
280
323
  <!-- svelte-ignore a11y_no_static_element_interactions -->
@@ -289,57 +332,69 @@
289
332
  ondblclick={handle_dblclick}
290
333
  >
291
334
  {#if label}<span class="viewport-label">{label}</span>{/if}
292
- <Canvas createRenderer={create_renderer}>
293
- <StructureScene
294
- {structure}
295
- {base_structure}
296
- {...scene_props}
297
- {...in_grid ? { auto_rotate: 0 } : {}}
298
- {camera_position}
299
- {camera_target}
300
- {camera_projection}
301
- {camera_direction}
302
- {interactive}
303
- gizmo={gizmo_prop}
304
- {lattice_props}
305
- {volumetric_data}
306
- {isosurface_settings}
307
- bind:camera_is_moving
308
- bind:selected_sites
309
- {active_sites}
310
- bind:hovered_idx={hovered_site_idx}
311
- bind:measured_sites
312
- bind:scene
313
- bind:camera
314
- bind:orbit_controls
315
- bind:rotation_target_ref
316
- bind:initial_computed_zoom
317
- bind:hidden_elements
318
- bind:hidden_prop_vals
319
- bind:element_radius_overrides
320
- bind:site_radius_overrides
321
- bind:added_bonds
322
- bind:removed_bonds
323
- bind:bond_order_overrides
324
- {bond_edits_enabled}
325
- bind:bond_edit_mode
326
- {bond_edit_order}
327
- {measure_mode}
328
- {width}
329
- {height}
330
- {atom_color_config}
331
- {sym_data}
332
- {on_sites_moved}
333
- {on_operation_start}
334
- {on_bond_edit_start}
335
- {on_add_atom}
336
- bind:add_atom_mode
337
- bind:add_element
338
- bind:cursor
339
- bind:dragging_atoms
340
- bind:polyhedra_rendered_elements
341
- />
342
- </Canvas>
335
+ {#if recovery_failed}
336
+ <div class="context-recovery-error">
337
+ <StatusMessage
338
+ message="Unable to restore the 3D view after repeated WebGL context loss. Reload the page to retry."
339
+ type="error"
340
+ />
341
+ </div>
342
+ {/if}
343
+ {#key canvas_remount_token}
344
+ <Canvas createRenderer={create_renderer}>
345
+ <StructureScene
346
+ {structure}
347
+ {base_structure}
348
+ {...scene_props}
349
+ {...in_grid ? { auto_rotate: 0 } : {}}
350
+ {camera_position}
351
+ {camera_target}
352
+ {camera_projection}
353
+ {camera_direction}
354
+ {interactive}
355
+ gizmo={gizmo_prop}
356
+ {lattice_props}
357
+ {volumetric_data}
358
+ {isosurface_settings}
359
+ {active_volume_idx}
360
+ {volume_scaling}
361
+ bind:camera_is_moving
362
+ bind:selected_sites
363
+ {active_sites}
364
+ bind:hovered_idx={hovered_site_idx}
365
+ bind:measured_sites
366
+ bind:scene
367
+ bind:camera
368
+ bind:orbit_controls
369
+ bind:rotation_target_ref
370
+ bind:initial_computed_zoom
371
+ bind:hidden_elements
372
+ bind:hidden_prop_vals
373
+ bind:element_radius_overrides
374
+ bind:site_radius_overrides
375
+ bind:added_bonds
376
+ bind:removed_bonds
377
+ bind:bond_order_overrides
378
+ {bond_edits_enabled}
379
+ bind:bond_edit_mode
380
+ {bond_edit_order}
381
+ {measure_mode}
382
+ {width}
383
+ {height}
384
+ {atom_color_config}
385
+ {sym_data}
386
+ {on_sites_moved}
387
+ {on_operation_start}
388
+ {on_bond_edit_start}
389
+ {on_add_atom}
390
+ bind:add_atom_mode
391
+ bind:add_element
392
+ bind:cursor
393
+ bind:dragging_atoms
394
+ bind:polyhedra_rendered_elements
395
+ />
396
+ </Canvas>
397
+ {/key}
343
398
  </div>
344
399
 
345
400
  <style>
@@ -368,7 +423,7 @@
368
423
  position: absolute;
369
424
  top: 3px;
370
425
  left: 5px;
371
- z-index: 1;
426
+ z-index: var(--z-index-viewer-label, 1);
372
427
  pointer-events: none;
373
428
  font-size: var(--struct-viewport-label-font-size, 0.8em);
374
429
  font-weight: 500;
@@ -380,6 +435,16 @@
380
435
  color-mix(in srgb, var(--page-bg, Canvas) 65%, transparent)
381
436
  );
382
437
  }
438
+ .context-recovery-error {
439
+ position: absolute;
440
+ inset: 0;
441
+ z-index: var(--z-index-viewer-tooltip, 1000);
442
+ display: grid;
443
+ place-items: center;
444
+ padding: 1em;
445
+ pointer-events: none;
446
+ will-change: transform;
447
+ }
383
448
  .viewport-cell :global(canvas) {
384
449
  cursor: var(--canvas-cursor, default);
385
450
  }
@@ -24,8 +24,10 @@ type $$ComponentProps = {
24
24
  scene_props?: ComponentProps<typeof StructureScene>;
25
25
  gizmo?: boolean | ComponentProps<typeof StructureScene>[`gizmo`];
26
26
  lattice_props?: ComponentProps<typeof StructureScene>[`lattice_props`];
27
- volumetric_data?: VolumetricData;
27
+ volumetric_data?: VolumetricData | VolumetricData[];
28
28
  isosurface_settings?: IsosurfaceSettings;
29
+ active_volume_idx?: number;
30
+ volume_scaling?: Vec3;
29
31
  bond_edits_enabled?: boolean;
30
32
  bond_edit_order?: BondOrder;
31
33
  measure_mode?: MeasureMode;
@@ -1,5 +1,5 @@
1
1
  import type { ColorScaleType, D3InterpolateName } from '../colors';
2
- import { type CoordinationData } from '../coordination/calc-coordination';
2
+ import type { CoordinationData } from '../coordination/calc-coordination';
3
3
  import type { AtomColorMode } from '../settings';
4
4
  import type { AnyStructure, Site } from './';
5
5
  import type { BondingStrategy } from './bonding';
@@ -1,8 +1,9 @@
1
1
  // Utility functions for computing atom properties and applying color scales
2
2
  import { get_d3_interpolator } from '../colors';
3
- import { calc_coordination_nums, } from '../coordination/calc-coordination';
3
+ import { calc_coordination_nums } from '../coordination/calc-coordination';
4
+ import { element_by_symbol } from '../element/data';
4
5
  import * as math from '../math';
5
- import { element_lookup, get_majority_element } from './bonding';
6
+ import { get_majority_element } from './bonding';
6
7
  import { wrap_frac_coord } from './pbc';
7
8
  import { rgb } from 'd3-color';
8
9
  import * as d3_sc from 'd3-scale-chromatic';
@@ -106,7 +107,7 @@ function expand_structure_for_pbc(structure, strategy) {
106
107
  // Covalent radius per atom (0 = unknown → forms no bonds → needs no images)
107
108
  const radii = cell_sites.map((site) => {
108
109
  const elem = get_majority_element(site);
109
- return (elem ? element_lookup.get(elem)?.covalent_radius : undefined) ?? 0;
110
+ return (elem ? element_by_symbol.get(elem)?.covalent_radius : undefined) ?? 0;
110
111
  });
111
112
  let max_radius = 0;
112
113
  for (const radius of radii)
@@ -52,12 +52,10 @@ function formal_charge(symbol, bond_valence) {
52
52
  const is_main_group = (symbol) => symbol in ATOMIC_VALENCE;
53
53
  // Cap per-fragment valence enumeration (3^k for catenated S/Se/Te/P chains).
54
54
  const MAX_VALENCE_COMBOS = 4096;
55
+ // Edges are bounds-checked by perceive_bond_orders before reaching here.
55
56
  function split_fragments(n_atoms, edges) {
56
57
  const adjacency = Array.from({ length: n_atoms }, () => []);
57
58
  for (const [atom_idx_1, atom_idx_2] of edges) {
58
- if (adjacency[atom_idx_1] === undefined || adjacency[atom_idx_2] === undefined) {
59
- throw new Error(`Invalid edge ${atom_idx_1}-${atom_idx_2} for ${n_atoms} atoms`);
60
- }
61
59
  adjacency[atom_idx_1].push(atom_idx_2);
62
60
  adjacency[atom_idx_2].push(atom_idx_1);
63
61
  }
@@ -140,9 +138,6 @@ function assign_bond_orders(edges, target_valence) {
140
138
  function find_rings(n_atoms, edges) {
141
139
  const adjacency = Array.from({ length: n_atoms }, () => new Set());
142
140
  for (const [atom_idx_1, atom_idx_2] of edges) {
143
- if (adjacency[atom_idx_1] === undefined || adjacency[atom_idx_2] === undefined) {
144
- throw new Error(`Invalid edge ${atom_idx_1}-${atom_idx_2} for ${n_atoms} atoms`);
145
- }
146
141
  adjacency[atom_idx_1].add(atom_idx_2);
147
142
  adjacency[atom_idx_2].add(atom_idx_1);
148
143
  }
@@ -200,7 +195,6 @@ function find_rings(n_atoms, edges) {
200
195
  }
201
196
  return [...uniq.values()];
202
197
  }
203
- const order_to_bond_order = (order) => order >= 3 ? 3 : order === 2 ? 2 : 1;
204
198
  // Conservative planarity check: degenerate first-3-atom planes are non-planar.
205
199
  function ring_is_planar(ring, sites) {
206
200
  if (ring.length < 3)
@@ -294,7 +288,8 @@ export function perceive_bond_orders(sites, bonds, opts = {}) {
294
288
  if (!solved)
295
289
  continue;
296
290
  local_edges.forEach((edge, edge_idx) => {
297
- const order = order_to_bond_order(solved[edge_idx]);
291
+ const solved_order = solved[edge_idx];
292
+ const order = solved_order >= 3 ? 3 : solved_order === 2 ? 2 : 1;
298
293
  result.set(edge.bond, { ...edge.bond, bond_order: order, perceived: true });
299
294
  });
300
295
  // Hückel aromatic post-pass, retaining Kekulé orders for display toggles.
@@ -1,7 +1,6 @@
1
1
  import type { ElementSymbol } from '../element';
2
2
  import type { Vec3 } from '../math';
3
3
  import type { AnyStructure, BondOrder, BondPair, Site, StructureBond } from './';
4
- export declare const element_lookup: Map<"Ac" | "Ag" | "Al" | "Am" | "Ar" | "As" | "At" | "Au" | "B" | "Ba" | "Be" | "Bh" | "Bi" | "Bk" | "Br" | "C" | "Ca" | "Cd" | "Ce" | "Cf" | "Cl" | "Cm" | "Co" | "Cr" | "Cs" | "Cu" | "Db" | "Dy" | "Er" | "Es" | "Eu" | "F" | "Fe" | "Fm" | "Fr" | "Ga" | "Gd" | "Ge" | "H" | "He" | "Hf" | "Hg" | "Ho" | "Hs" | "I" | "In" | "Ir" | "K" | "Kr" | "La" | "Li" | "Lr" | "Lu" | "Md" | "Mg" | "Mn" | "Mo" | "Mt" | "N" | "Na" | "Nb" | "Nd" | "Ne" | "Ni" | "No" | "Np" | "O" | "Os" | "P" | "Pa" | "Pb" | "Pd" | "Pm" | "Po" | "Pr" | "Pt" | "Pu" | "Ra" | "Rb" | "Re" | "Rf" | "Rh" | "Rn" | "Ru" | "S" | "Sb" | "Sc" | "Se" | "Sg" | "Si" | "Sm" | "Sn" | "Sr" | "Ta" | "Tb" | "Tc" | "Te" | "Th" | "Ti" | "Tl" | "Tm" | "U" | "V" | "W" | "Xe" | "Y" | "Yb" | "Zn" | "Zr" | "Ds" | "Rg" | "Cn" | "Nh" | "Fl" | "Mc" | "Lv" | "Ts" | "Og", import("../element").ChemicalElement>;
5
4
  export declare const get_majority_element: (site: Site | undefined) => ElementSymbol | null;
6
5
  export declare const is_spectator_center: (element: string) => boolean;
7
6
  export declare function has_framework_potential(elements: Iterable<string>): boolean;
@@ -1,8 +1,6 @@
1
1
  // Bonding algorithms for structure visualization
2
- import { default as element_data } from '../element/data';
2
+ import element_data, { element_by_symbol } from '../element/data';
3
3
  import * as math from '../math';
4
- // Shared per-symbol element data lookup (also used by pbc.ts and polyhedra.ts)
5
- export const element_lookup = new Map(element_data.map((el) => [el.symbol, el]));
6
4
  const covalent_radii = new Map(element_data.flatMap((el) => el.covalent_radius === null ? [] : [[el.symbol, el.covalent_radius]]));
7
5
  // Majority-occupancy element of a (possibly disordered) site
8
6
  export const get_majority_element = (site) => {
@@ -17,9 +15,8 @@ export const get_majority_element = (site) => {
17
15
  // or when force-included via `included_center_elements`. Shared by polyhedra.ts
18
16
  // (vertex/center selection) and pbc.ts (phase-2 boundary completion) so the bond
19
17
  // graph and the polyhedra it feeds stay consistent.
20
- const SPECTATOR_CATEGORIES = new Set([`alkali metal`]);
21
18
  const HEAVY_ALKALINE_EARTHS = new Set([`Ca`, `Sr`, `Ba`, `Ra`]);
22
- export const is_spectator_center = (element) => SPECTATOR_CATEGORIES.has(element_lookup.get(element)?.category ?? ``) ||
19
+ export const is_spectator_center = (element) => element_by_symbol.get(element)?.category === `alkali metal` ||
23
20
  HEAVY_ALKALINE_EARTHS.has(element);
24
21
  // True if the composition contains a framework cation: a non-spectator element
25
22
  // strictly less electronegative than the most electronegative element present
@@ -32,23 +29,18 @@ export function has_framework_potential(elements) {
32
29
  const els = [...new Set(elements)]; // dedupe so callers can pass per-site element lists
33
30
  let max_en = -Infinity;
34
31
  for (const el of els) {
35
- const en = element_lookup.get(el)?.electronegativity;
32
+ const en = element_by_symbol.get(el)?.electronegativity;
36
33
  if (en != null && en > max_en)
37
34
  max_en = en;
38
35
  }
39
36
  return els.some((el) => {
40
37
  if (is_spectator_center(el))
41
38
  return false;
42
- const en = element_lookup.get(el)?.electronegativity;
39
+ const en = element_by_symbol.get(el)?.electronegativity;
43
40
  return en != null && en < max_en;
44
41
  });
45
42
  }
46
43
  const is_zero_cell_shift = (cell_shift) => cell_shift === undefined || cell_shift.every((val) => val === 0);
47
- const format_cell_shift = (cell_shift) => {
48
- if (cell_shift === undefined || is_zero_cell_shift(cell_shift))
49
- return ``;
50
- return `@${cell_shift.join(`,`)}`;
51
- };
52
44
  const negate_cell_shift = (cell_shift) => [
53
45
  cell_shift[0] === 0 ? 0 : -cell_shift[0],
54
46
  cell_shift[1] === 0 ? 0 : -cell_shift[1],
@@ -83,7 +75,10 @@ export const normalize_structure_bond = (site_idx_1, site_idx_2, order, cell_shi
83
75
  };
84
76
  export const get_bond_key = (idx_1, idx_2, cell_shift) => {
85
77
  const normalized = normalize_bond_endpoints(idx_1, idx_2, cell_shift);
86
- return `${normalized.site_idx_1}-${normalized.site_idx_2}${format_cell_shift(normalized.cell_shift)}`;
78
+ const shift_suffix = normalized.cell_shift === undefined || is_zero_cell_shift(normalized.cell_shift)
79
+ ? ``
80
+ : `@${normalized.cell_shift.join(`,`)}`;
81
+ return `${normalized.site_idx_1}-${normalized.site_idx_2}${shift_suffix}`;
87
82
  };
88
83
  // Remap explicit bond metadata after site deletion: drop bonds touching deleted
89
84
  // sites and shift each surviving index down by the number of deleted indices below it.
@@ -280,7 +275,6 @@ export const merge_bond_edits = (base_bonds, added, removed, overrides) => {
280
275
  }
281
276
  return [...merged.values()];
282
277
  };
283
- const is_record = (value) => typeof value === `object` && value !== null;
284
278
  export function normalize_bond_order(order) {
285
279
  if (order === `aromatic`)
286
280
  return order;
@@ -338,13 +332,14 @@ export function get_explicit_bond_metadata(structure) {
338
332
  }
339
333
  const explicit_bonds = new Map();
340
334
  for (const [entry_idx, raw_bond] of raw_bonds.entries()) {
341
- if (!is_record(raw_bond)) {
335
+ if (typeof raw_bond !== `object` || raw_bond === null) {
342
336
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: expected object`);
343
337
  continue;
344
338
  }
345
- const { order } = raw_bond;
346
- const site_idx_1 = raw_bond.site_idx_1;
347
- const site_idx_2 = raw_bond.site_idx_2;
339
+ const bond_record = raw_bond;
340
+ const { order } = bond_record;
341
+ const site_idx_1 = bond_record.site_idx_1;
342
+ const site_idx_2 = bond_record.site_idx_2;
348
343
  if (typeof site_idx_1 !== `number` ||
349
344
  typeof site_idx_2 !== `number` ||
350
345
  !Number.isInteger(site_idx_1) ||
@@ -364,7 +359,7 @@ export function get_explicit_bond_metadata(structure) {
364
359
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: unsupported order ${String(order)}`);
365
360
  continue;
366
361
  }
367
- const cell_shift = normalize_cell_shift(raw_bond.cell_shift);
362
+ const cell_shift = normalize_cell_shift(bond_record.cell_shift);
368
363
  if (cell_shift === null) {
369
364
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: cell_shift must be three integers`);
370
365
  continue;
@@ -373,9 +368,7 @@ export function get_explicit_bond_metadata(structure) {
373
368
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: endpoints match`);
374
369
  continue;
375
370
  }
376
- if (cell_shift !== undefined &&
377
- !is_zero_cell_shift(cell_shift) &&
378
- !(`lattice` in structure)) {
371
+ if (!is_zero_cell_shift(cell_shift) && !(`lattice` in structure)) {
379
372
  console.warn(`Ignoring invalid explicit bond at index ${entry_idx}: cell_shift requires a crystal lattice`);
380
373
  continue;
381
374
  }
@@ -650,7 +643,7 @@ center_count = Infinity, } = {}) {
650
643
  const elem_id_lookup = new Map();
651
644
  for (let idx = 0; idx < n_sites; idx++) {
652
645
  const elem = get_majority_element(sites[idx]);
653
- const data = elem ? element_lookup.get(elem) : undefined;
646
+ const data = elem ? element_by_symbol.get(elem) : undefined;
654
647
  electronegs[idx] = data?.electronegativity ?? 2.0;
655
648
  metal_flags[idx] = data?.metal ? 1 : 0;
656
649
  nonmetal_flags[idx] = data?.nonmetal ? 1 : 0;
@@ -6,22 +6,15 @@ import { Color, Group, Matrix4, Mesh, MeshStandardMaterial, ShaderMaterial } fro
6
6
  import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
7
7
  import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter.js';
8
8
  // @internal exported only for tests - not part of the public API.
9
- // Type guard to check if a material has a color property (duck typing for bundler
10
- // compatibility across different Three.js instances - Threlte vs vanilla Three.js).
9
+ // Type guard to check if a material has a Color-like property with numeric r/g/b
10
+ // channels (duck typing for bundler compatibility across different Three.js
11
+ // instances - Threlte vs vanilla Three.js).
11
12
  export function has_color_property(mat) {
12
- if (!(`color` in mat))
13
- return false;
14
13
  const color = mat.color;
15
14
  if (!color || typeof color !== `object`)
16
15
  return false;
17
- // Check for Color-like object with numeric r/g/b channels (duck typing)
18
- const color_obj = color;
19
- const red_channel = color_obj.r;
20
- const green_channel = color_obj.g;
21
- const blue_channel = color_obj.b;
22
- return (typeof red_channel === `number` &&
23
- typeof green_channel === `number` &&
24
- typeof blue_channel === `number`);
16
+ const { r: red, g: green, b: blue } = color;
17
+ return typeof red === `number` && typeof green === `number` && typeof blue === `number`;
25
18
  }
26
19
  // Extract color from a ShaderMaterial by checking common color uniform patterns
27
20
  function extract_shader_color(shader_mat) {
@@ -34,8 +27,8 @@ function extract_shader_color(shader_mat) {
34
27
  return uniforms[key].value.clone();
35
28
  }
36
29
  }
37
- // For gradient bonds, try to extract midpoint color from instanceColorStart/End attributes
38
- // These are set as geometry attributes, not uniforms, so we return a default gray
30
+ // Gradient bonds store colors as instanceColorStart/End geometry attributes,
31
+ // not uniforms nothing to extract here (caller falls back to gray)
39
32
  return null;
40
33
  }
41
34
  // Convert ShaderMaterial to MeshStandardMaterial for export compatibility
@@ -146,13 +139,6 @@ export function generate_mtl_content(scene) {
146
139
  });
147
140
  return lines.join(`\n`);
148
141
  }
149
- // Extract color from material, returning RGB values or null if not found
150
- function extract_material_color(mat) {
151
- if (has_color_property(mat)) {
152
- return { r: mat.color.r, g: mat.color.g, b: mat.color.b };
153
- }
154
- return null;
155
- }
156
142
  // Helper function to convert InstancedMesh to regular Mesh objects for export
157
143
  // This is necessary because GLB/OBJ exporters don't handle InstancedMesh properly
158
144
  // Note: Threlte's InstancedMesh sets isInstancedMesh=true but type remains "Mesh"
@@ -209,25 +195,29 @@ export function convert_instanced_meshes_to_regular(scene) {
209
195
  else {
210
196
  // Extract shared material color for atoms
211
197
  const single_mat = Array.isArray(mat) ? mat[0] : mat;
212
- const color = extract_material_color(single_mat);
213
- if (color) {
214
- material_colors.set(mesh_id, color);
198
+ if (has_color_property(single_mat)) {
199
+ material_colors.set(mesh_id, {
200
+ r: single_mat.color.r,
201
+ g: single_mat.color.g,
202
+ b: single_mat.color.b,
203
+ });
215
204
  }
216
205
  }
217
206
  });
218
207
  // STEP 2: Clone the scene
219
208
  const cloned_scene = scene.clone();
220
- // STEP 3: Find all InstancedMesh objects in the cloned scene and build uuid mapping
221
- const original_meshes = [];
222
- const cloned_meshes = [];
223
- scene.traverse((object) => {
224
- if (is_instanced_mesh(object))
225
- original_meshes.push(object);
226
- });
227
- cloned_scene.traverse((object) => {
228
- if (is_instanced_mesh(object))
229
- cloned_meshes.push(object);
230
- });
209
+ // STEP 3: Find all InstancedMesh objects in original + cloned scene (same traversal
210
+ // order) to map cloned meshes back to their original uuid
211
+ const collect_instanced = (root) => {
212
+ const meshes = [];
213
+ root.traverse((object) => {
214
+ if (is_instanced_mesh(object))
215
+ meshes.push(object);
216
+ });
217
+ return meshes;
218
+ };
219
+ const original_meshes = collect_instanced(scene);
220
+ const cloned_meshes = collect_instanced(cloned_scene);
231
221
  // STEP 4: Convert each InstancedMesh to individual Mesh objects
232
222
  for (let mesh_idx = 0; mesh_idx < cloned_meshes.length; mesh_idx++) {
233
223
  const instanced_mesh = cloned_meshes[mesh_idx];
@@ -329,26 +319,18 @@ export function create_structure_filename(structure, extension) {
329
319
  if (formula && formula !== `Unknown`) {
330
320
  safe_push(formula.replaceAll(` `, ``));
331
321
  }
332
- // Add space group if available
333
- if (`symmetry` in structure &&
334
- structure.symmetry &&
335
- typeof structure.symmetry === `object` &&
336
- `space_group_symbol` in structure.symmetry) {
337
- const space_group = structure.symmetry.space_group_symbol;
338
- if (space_group && typeof space_group === `string`) {
339
- safe_push(space_group.replaceAll(` `, ``));
340
- }
341
- }
342
- // Add lattice system if available
343
- if (`lattice` in structure &&
344
- structure.lattice &&
345
- typeof structure.lattice === `object` &&
346
- `lattice_system` in structure.lattice) {
347
- const lattice_system = structure.lattice.lattice_system;
348
- if (lattice_system && typeof lattice_system === `string`) {
349
- safe_push(lattice_system);
350
- }
322
+ // Add space group and lattice system if available
323
+ const symmetry = `symmetry` in structure && is_plain_object(structure.symmetry)
324
+ ? structure.symmetry
325
+ : undefined;
326
+ if (typeof symmetry?.space_group_symbol === `string`) {
327
+ safe_push(symmetry.space_group_symbol.replaceAll(` `, ``));
351
328
  }
329
+ const lattice = `lattice` in structure && is_plain_object(structure.lattice)
330
+ ? structure.lattice
331
+ : undefined;
332
+ if (lattice && `lattice_system` in lattice && typeof lattice.lattice_system === `string`)
333
+ safe_push(lattice.lattice_system);
352
334
  // Add number of sites
353
335
  if (structure.sites?.length)
354
336
  parts.push(`${structure.sites.length}sites`);
@@ -465,21 +447,14 @@ export function structure_to_cif_str(structure) {
465
447
  }
466
448
  const lines = [];
467
449
  // CIF header with data block (required by pymatgen and CIF spec)
468
- lines.push(`# CIF file generated by MatterViz`);
469
- const block_name = get_cif_block_name(structure);
470
- lines.push(`data_${block_name}`);
471
- lines.push(``);
450
+ lines.push(`# CIF file generated by MatterViz`, `data_${get_cif_block_name(structure)}`, ``);
472
451
  // Cell parameters
473
452
  const lattice = structure.lattice;
474
453
  if (lattice.a && lattice.b && lattice.c) {
475
- lines.push(`_cell_length_a ${lattice.a.toFixed(6)}`);
476
- lines.push(`_cell_length_b ${lattice.b.toFixed(6)}`);
477
- lines.push(`_cell_length_c ${lattice.c.toFixed(6)}`);
454
+ lines.push(`_cell_length_a ${lattice.a.toFixed(6)}`, `_cell_length_b ${lattice.b.toFixed(6)}`, `_cell_length_c ${lattice.c.toFixed(6)}`);
478
455
  }
479
456
  if (lattice.alpha && lattice.beta && lattice.gamma) {
480
- lines.push(`_cell_angle_alpha ${lattice.alpha.toFixed(6)}`);
481
- lines.push(`_cell_angle_beta ${lattice.beta.toFixed(6)}`);
482
- lines.push(`_cell_angle_gamma ${lattice.gamma.toFixed(6)}`);
457
+ lines.push(`_cell_angle_alpha ${lattice.alpha.toFixed(6)}`, `_cell_angle_beta ${lattice.beta.toFixed(6)}`, `_cell_angle_gamma ${lattice.gamma.toFixed(6)}`);
483
458
  }
484
459
  // Space group information
485
460
  if (`symmetry` in structure && is_plain_object(structure.symmetry)) {
@@ -498,13 +473,7 @@ export function structure_to_cif_str(structure) {
498
473
  // full P1 list, so without it parsers would re-apply the H-M ops and multiply sites
499
474
  lines.push(`loop_`, `_symmetry_equiv_pos_as_xyz`, ` 'x, y, z'`, ``);
500
475
  // Atom site loop header
501
- lines.push(`loop_`);
502
- lines.push(`_atom_site_label`);
503
- lines.push(`_atom_site_type_symbol`);
504
- lines.push(`_atom_site_fract_x`);
505
- lines.push(`_atom_site_fract_y`);
506
- lines.push(`_atom_site_fract_z`);
507
- lines.push(`_atom_site_occupancy`);
476
+ lines.push(`loop_`, `_atom_site_label`, `_atom_site_type_symbol`, `_atom_site_fract_x`, `_atom_site_fract_y`, `_atom_site_fract_z`, `_atom_site_occupancy`);
508
477
  // Cache inverse transpose for Cartesian→fractional conversion (avoids recomputing per site)
509
478
  const cart_to_frac = lattice.matrix?.length === 3 ? math.create_cart_to_frac(lattice.matrix) : null;
510
479
  // Atom sites: one row per species entry so disordered (multi-species) sites
@@ -580,16 +549,23 @@ export function structure_to_poscar_str(structure) {
580
549
  for (const group of sites_by_element.values()) {
581
550
  for (const site of group) {
582
551
  const frac_coords = get_frac_coords(site, cart_to_frac);
583
- let selective_dynamics_str = ``;
552
+ const coords_str = frac_coords
553
+ .slice(0, 3)
554
+ .map((coord) => coord.toFixed(8))
555
+ .join(` `);
556
+ let sel_dyn_str = ``;
584
557
  if (has_selective_dynamics) {
585
558
  const sel_dyn = (site.properties?.selective_dynamics ?? [
586
559
  true,
587
560
  true,
588
561
  true,
589
562
  ]);
590
- selective_dynamics_str = ` ${sel_dyn[0] ? `T` : `F`} ${sel_dyn[1] ? `T` : `F`} ${sel_dyn[2] ? `T` : `F`}`;
563
+ sel_dyn_str = ` ${sel_dyn
564
+ .slice(0, 3)
565
+ .map((flag) => (flag ? `T` : `F`))
566
+ .join(` `)}`;
591
567
  }
592
- lines.push(`${frac_coords[0].toFixed(8)} ${frac_coords[1].toFixed(8)} ${frac_coords[2].toFixed(8)}${selective_dynamics_str}`);
568
+ lines.push(`${coords_str}${sel_dyn_str}`);
593
569
  }
594
570
  }
595
571
  return lines.join(`\n`);