1use ak_core::{Structure, Trajectory};
2use bevy::prelude::{Resource, Vec3};
3use smallvec::SmallVec;
4use std::collections::{BTreeSet, HashMap};
5use std::sync::mpsc::Sender;
6use std::sync::{Arc, Condvar, Mutex};
7use std::time::{Duration, Instant};
8
9use crate::ScalarColorMap;
10
11mod camera;
12mod selection;
13
14#[cfg(test)]
15mod tests;
16
17pub use camera::camera_view_for_frame;
18
19pub enum ViewerCommand {
20 LoadTrajectory {
21 frames: Vec<Structure>,
22 initial_frame: usize,
23 },
24 AppendFrame {
25 frame: Structure,
26 },
27 SetCurrentFrame {
28 index: usize,
29 },
30 SetFollowTail {
31 enabled: bool,
32 },
33 SetAtomScalars {
34 name: String,
35 values: Vec<f32>,
36 frame_index: Option<usize>,
37 },
38 MapAppearanceByScalar {
39 name: String,
40 channel: AppearanceChannel,
41 palette: Option<ScalarColorMap>,
42 min: Option<f32>,
43 max: Option<f32>,
44 append: bool,
45 },
46 ResetAtomAppearance {
47 channel: Option<AppearanceChannel>,
48 },
49 SetBonds {
50 bonds: BondList,
51 frame_index: Option<usize>,
52 },
53 AddBonds {
54 bonds: BondList,
55 frame_index: Option<usize>,
56 },
57 RemoveBonds {
58 bonds: BondList,
59 frame_index: Option<usize>,
60 },
61 ClearBonds {
62 frame_index: Option<usize>,
63 },
64 SetFaces {
65 faces: FaceList,
66 frame_index: Option<usize>,
67 },
68 AddFaces {
69 faces: FaceList,
70 frame_index: Option<usize>,
71 },
72 RemoveFaces {
73 faces: FaceList,
74 frame_index: Option<usize>,
75 },
76 ClearFaces {
77 frame_index: Option<usize>,
78 },
79 SetRenderStyle {
80 style: RenderStyle,
81 selection: Vec<bool>,
82 frame_index: Option<usize>,
83 append: bool,
84 },
85 ResetRenderStyle,
86 ReplaceSelection {
87 selection: Vec<bool>,
88 frame_index: Option<usize>,
89 },
90 AddSelection {
91 selection: Vec<bool>,
92 frame_index: Option<usize>,
93 },
94 RemoveSelection {
95 selection: Vec<bool>,
96 frame_index: Option<usize>,
97 },
98 ClearSelection {
99 frame_index: Option<usize>,
100 },
101 ReplaceImageSelection {
102 selection: Vec<SelectedImageAtom>,
103 frame_index: Option<usize>,
104 },
105 AddImageSelection {
106 selection: Vec<SelectedImageAtom>,
107 frame_index: Option<usize>,
108 },
109 RemoveImageSelection {
110 selection: Vec<SelectedImageAtom>,
111 frame_index: Option<usize>,
112 },
113 ClearImageSelection {
114 frame_index: Option<usize>,
115 },
116 SetSupercell {
117 repeats: [u32; 3],
118 },
119 IncrementSupercellAxis {
120 axis: usize,
121 },
122 DecrementSupercellAxis {
123 axis: usize,
124 },
125 ResetSupercell,
126 SetGhostRepeatedImages {
127 enabled: bool,
128 },
129 ToggleGhostRepeatedImages,
130 SetCameraView {
131 focus: Option<[f32; 3]>,
132 radius: Option<f32>,
133 yaw: Option<f32>,
134 pitch: Option<f32>,
135 },
136 PanCamera {
137 delta: [f32; 3],
138 },
139 ZoomCamera {
140 factor: Option<f32>,
141 delta: Option<f32>,
142 },
143 OrbitCamera {
144 yaw_delta: f32,
145 pitch_delta: f32,
146 },
147 FrameAll,
148 StartOrbit {
149 yaw_rate: f32,
150 pitch_rate: f32,
151 },
152 StopCameraMotion,
153 Close,
154}
155
156#[derive(Clone)]
157pub struct ViewerSessionHandle {
158 sender: Sender<ViewerCommand>,
159 readiness: Arc<ViewerReadiness>,
160 snapshot: Arc<Mutex<ViewerSnapshot>>,
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq)]
164enum ViewerLifecycleState {
165 Pending,
166 Ready,
167 Closed,
168}
169
170#[derive(Debug)]
171pub struct ViewerReadiness {
172 state: Mutex<ViewerLifecycleState>,
173 changed: Condvar,
174}
175
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct SelectionFrames {
178 frames: Vec<Vec<bool>>,
179 ordered: Vec<Vec<usize>>,
180}
181
182#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
183pub struct SelectedImageAtom {
184 pub atom_index: usize,
185 pub image_offset: [i32; 3],
186}
187
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub struct ImageSelectionFrames {
190 frames: Vec<Vec<SelectedImageAtom>>,
191}
192
193#[derive(Clone, Copy, Debug, PartialEq, Eq)]
194pub struct SupercellSettings {
195 pub repeats: [u32; 3],
196 pub ghost_repeated_images: bool,
197}
198
199#[derive(Clone, Debug, PartialEq, Eq)]
200pub struct ViewerSnapshot {
201 pub current_frame: usize,
202 pub selection: SelectionFrames,
203 pub image_selection: ImageSelectionFrames,
204 pub supercell: SupercellSettings,
205}
206
207#[derive(Debug, Clone, Copy)]
208pub struct ViewerSessionClosed;
209
210impl std::fmt::Display for ViewerSessionClosed {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 write!(f, "viewer session is no longer available")
213 }
214}
215
216impl std::error::Error for ViewerSessionClosed {}
217
218#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
219pub enum AppearanceChannel {
220 Color,
221 Metallic,
222 PerceptualRoughness,
223}
224
225#[derive(Clone, Debug, PartialEq)]
226pub struct AtomAppearanceRule {
227 pub name: String,
228 pub channel: AppearanceChannel,
229 pub palette: Option<ScalarColorMap>,
230 pub min: Option<f32>,
231 pub max: Option<f32>,
232}
233
234#[derive(Clone, Debug, PartialEq, Eq)]
235pub struct BondList {
236 edges: Vec<(usize, usize)>,
237}
238
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub struct BondFrames {
241 frames: Vec<Option<BondList>>,
242}
243
244#[derive(Clone, Debug, PartialEq)]
245pub struct Face {
246 pub atoms: SmallVec<[usize; 4]>,
247 pub color: [f32; 4],
248}
249
250#[derive(Clone, Debug, PartialEq)]
251pub struct FaceList {
252 faces: Vec<Face>,
253}
254
255#[derive(Clone, Debug, PartialEq)]
256pub struct FaceFrames {
257 frames: Vec<Option<FaceList>>,
258}
259
260#[derive(Clone, Copy, Debug, PartialEq)]
261pub enum BondScope {
262 BothSelected,
263 TouchSelection,
264}
265
266#[derive(Clone, Copy, Debug, PartialEq)]
267pub struct BallAndStickStyle {
268 pub atom_scale: f32,
269 pub bond_radius: f32,
270 pub bond_color: [f32; 4],
271 pub bond_scope: BondScope,
272}
273
274#[derive(Clone, Copy, Debug, PartialEq)]
275pub enum RenderStyle {
276 SpaceFilling,
277 BallAndStick(BallAndStickStyle),
278}
279
280#[derive(Clone, Debug, PartialEq)]
281pub struct RenderStyleRule {
282 pub frame_index: usize,
283 pub selection: Vec<bool>,
284 pub style: RenderStyle,
285}
286
287#[derive(Clone, Copy, Debug, PartialEq)]
288pub struct OrbitMotion {
289 pub yaw_rate: f32,
290 pub pitch_rate: f32,
291}
292
293#[derive(Clone, Copy, Debug, PartialEq)]
294pub struct CameraView {
295 pub focus: Vec3,
296 pub radius: f32,
297 pub yaw: f32,
298 pub pitch: f32,
299}
300
301#[derive(Resource, Clone, Debug, PartialEq)]
302pub struct CameraState {
303 pub focus: Vec3,
304 pub radius: f32,
305 pub yaw: f32,
306 pub pitch: f32,
307 pub needs_apply: bool,
308 pub motion: Option<OrbitMotion>,
309}
310
311impl ViewerSessionHandle {
312 pub fn new(sender: Sender<ViewerCommand>) -> Self {
313 Self {
314 sender,
315 readiness: Arc::new(ViewerReadiness::new()),
316 snapshot: Arc::new(Mutex::new(ViewerSnapshot::default())),
317 }
318 }
319
320 pub fn with_readiness(sender: Sender<ViewerCommand>, readiness: Arc<ViewerReadiness>) -> Self {
321 Self {
322 sender,
323 readiness,
324 snapshot: Arc::new(Mutex::new(ViewerSnapshot::default())),
325 }
326 }
327
328 pub fn wait_until_ready(&self, timeout: Option<Duration>) -> bool {
329 self.readiness.wait(timeout)
330 }
331
332 pub fn readiness(&self) -> &Arc<ViewerReadiness> {
333 &self.readiness
334 }
335
336 pub fn snapshot(&self) -> &Arc<Mutex<ViewerSnapshot>> {
337 &self.snapshot
338 }
339
340 pub fn load_trajectory(
341 &self,
342 frames: Vec<Structure>,
343 initial_frame: usize,
344 ) -> Result<(), ViewerSessionClosed> {
345 self.sender
346 .send(ViewerCommand::LoadTrajectory {
347 frames,
348 initial_frame,
349 })
350 .map_err(|_| ViewerSessionClosed)
351 }
352
353 pub fn append_frame(&self, frame: Structure) -> Result<(), ViewerSessionClosed> {
354 self.sender
355 .send(ViewerCommand::AppendFrame { frame })
356 .map_err(|_| ViewerSessionClosed)
357 }
358
359 pub fn set_current_frame(&self, index: usize) -> Result<(), ViewerSessionClosed> {
360 self.sender
361 .send(ViewerCommand::SetCurrentFrame { index })
362 .map_err(|_| ViewerSessionClosed)
363 }
364
365 pub fn set_follow_tail(&self, enabled: bool) -> Result<(), ViewerSessionClosed> {
366 self.sender
367 .send(ViewerCommand::SetFollowTail { enabled })
368 .map_err(|_| ViewerSessionClosed)
369 }
370
371 pub fn set_atom_scalars(
372 &self,
373 name: String,
374 values: Vec<f32>,
375 frame_index: Option<usize>,
376 ) -> Result<(), ViewerSessionClosed> {
377 self.sender
378 .send(ViewerCommand::SetAtomScalars {
379 name,
380 values,
381 frame_index,
382 })
383 .map_err(|_| ViewerSessionClosed)
384 }
385
386 pub fn map_appearance_by_scalar(
387 &self,
388 name: String,
389 channel: AppearanceChannel,
390 palette: Option<ScalarColorMap>,
391 min: Option<f32>,
392 max: Option<f32>,
393 append: bool,
394 ) -> Result<(), ViewerSessionClosed> {
395 self.sender
396 .send(ViewerCommand::MapAppearanceByScalar {
397 name,
398 channel,
399 palette,
400 min,
401 max,
402 append,
403 })
404 .map_err(|_| ViewerSessionClosed)
405 }
406
407 pub fn reset_atom_appearance(
408 &self,
409 channel: Option<AppearanceChannel>,
410 ) -> Result<(), ViewerSessionClosed> {
411 self.sender
412 .send(ViewerCommand::ResetAtomAppearance { channel })
413 .map_err(|_| ViewerSessionClosed)
414 }
415
416 pub fn set_bonds(
417 &self,
418 bonds: BondList,
419 frame_index: Option<usize>,
420 ) -> Result<(), ViewerSessionClosed> {
421 self.sender
422 .send(ViewerCommand::SetBonds { bonds, frame_index })
423 .map_err(|_| ViewerSessionClosed)
424 }
425
426 pub fn add_bonds(
427 &self,
428 bonds: BondList,
429 frame_index: Option<usize>,
430 ) -> Result<(), ViewerSessionClosed> {
431 self.sender
432 .send(ViewerCommand::AddBonds { bonds, frame_index })
433 .map_err(|_| ViewerSessionClosed)
434 }
435
436 pub fn remove_bonds(
437 &self,
438 bonds: BondList,
439 frame_index: Option<usize>,
440 ) -> Result<(), ViewerSessionClosed> {
441 self.sender
442 .send(ViewerCommand::RemoveBonds { bonds, frame_index })
443 .map_err(|_| ViewerSessionClosed)
444 }
445
446 pub fn clear_bonds(&self, frame_index: Option<usize>) -> Result<(), ViewerSessionClosed> {
447 self.sender
448 .send(ViewerCommand::ClearBonds { frame_index })
449 .map_err(|_| ViewerSessionClosed)
450 }
451
452 pub fn set_faces(
453 &self,
454 faces: FaceList,
455 frame_index: Option<usize>,
456 ) -> Result<(), ViewerSessionClosed> {
457 self.sender
458 .send(ViewerCommand::SetFaces { faces, frame_index })
459 .map_err(|_| ViewerSessionClosed)
460 }
461
462 pub fn add_faces(
463 &self,
464 faces: FaceList,
465 frame_index: Option<usize>,
466 ) -> Result<(), ViewerSessionClosed> {
467 self.sender
468 .send(ViewerCommand::AddFaces { faces, frame_index })
469 .map_err(|_| ViewerSessionClosed)
470 }
471
472 pub fn remove_faces(
473 &self,
474 faces: FaceList,
475 frame_index: Option<usize>,
476 ) -> Result<(), ViewerSessionClosed> {
477 self.sender
478 .send(ViewerCommand::RemoveFaces { faces, frame_index })
479 .map_err(|_| ViewerSessionClosed)
480 }
481
482 pub fn clear_faces(&self, frame_index: Option<usize>) -> Result<(), ViewerSessionClosed> {
483 self.sender
484 .send(ViewerCommand::ClearFaces { frame_index })
485 .map_err(|_| ViewerSessionClosed)
486 }
487
488 pub fn set_render_style(
489 &self,
490 style: RenderStyle,
491 selection: Vec<bool>,
492 frame_index: Option<usize>,
493 append: bool,
494 ) -> Result<(), ViewerSessionClosed> {
495 self.sender
496 .send(ViewerCommand::SetRenderStyle {
497 style,
498 selection,
499 frame_index,
500 append,
501 })
502 .map_err(|_| ViewerSessionClosed)
503 }
504
505 pub fn reset_render_style(&self) -> Result<(), ViewerSessionClosed> {
506 self.sender
507 .send(ViewerCommand::ResetRenderStyle)
508 .map_err(|_| ViewerSessionClosed)
509 }
510
511 pub fn replace_selection(
512 &self,
513 selection: Vec<bool>,
514 frame_index: Option<usize>,
515 ) -> Result<(), ViewerSessionClosed> {
516 self.sender
517 .send(ViewerCommand::ReplaceSelection {
518 selection,
519 frame_index,
520 })
521 .map_err(|_| ViewerSessionClosed)
522 }
523
524 pub fn add_selection(
525 &self,
526 selection: Vec<bool>,
527 frame_index: Option<usize>,
528 ) -> Result<(), ViewerSessionClosed> {
529 self.sender
530 .send(ViewerCommand::AddSelection {
531 selection,
532 frame_index,
533 })
534 .map_err(|_| ViewerSessionClosed)
535 }
536
537 pub fn remove_selection(
538 &self,
539 selection: Vec<bool>,
540 frame_index: Option<usize>,
541 ) -> Result<(), ViewerSessionClosed> {
542 self.sender
543 .send(ViewerCommand::RemoveSelection {
544 selection,
545 frame_index,
546 })
547 .map_err(|_| ViewerSessionClosed)
548 }
549
550 pub fn clear_selection(&self, frame_index: Option<usize>) -> Result<(), ViewerSessionClosed> {
551 self.sender
552 .send(ViewerCommand::ClearSelection { frame_index })
553 .map_err(|_| ViewerSessionClosed)
554 }
555
556 pub fn replace_image_selection(
557 &self,
558 selection: Vec<SelectedImageAtom>,
559 frame_index: Option<usize>,
560 ) -> Result<(), ViewerSessionClosed> {
561 self.sender
562 .send(ViewerCommand::ReplaceImageSelection {
563 selection,
564 frame_index,
565 })
566 .map_err(|_| ViewerSessionClosed)
567 }
568
569 pub fn add_image_selection(
570 &self,
571 selection: Vec<SelectedImageAtom>,
572 frame_index: Option<usize>,
573 ) -> Result<(), ViewerSessionClosed> {
574 self.sender
575 .send(ViewerCommand::AddImageSelection {
576 selection,
577 frame_index,
578 })
579 .map_err(|_| ViewerSessionClosed)
580 }
581
582 pub fn remove_image_selection(
583 &self,
584 selection: Vec<SelectedImageAtom>,
585 frame_index: Option<usize>,
586 ) -> Result<(), ViewerSessionClosed> {
587 self.sender
588 .send(ViewerCommand::RemoveImageSelection {
589 selection,
590 frame_index,
591 })
592 .map_err(|_| ViewerSessionClosed)
593 }
594
595 pub fn clear_image_selection(
596 &self,
597 frame_index: Option<usize>,
598 ) -> Result<(), ViewerSessionClosed> {
599 self.sender
600 .send(ViewerCommand::ClearImageSelection { frame_index })
601 .map_err(|_| ViewerSessionClosed)
602 }
603
604 pub fn selected_atoms(&self, frame_index: Option<usize>) -> Vec<usize> {
605 let Ok(snapshot) = self.snapshot.lock() else {
606 return Vec::new();
607 };
608 let target_frame = frame_index.unwrap_or(snapshot.current_frame);
609 snapshot.selection.selected_indices(target_frame)
610 }
611
612 pub fn selected_images(&self, frame_index: Option<usize>) -> Vec<SelectedImageAtom> {
613 let Ok(snapshot) = self.snapshot.lock() else {
614 return Vec::new();
615 };
616 let target_frame = frame_index.unwrap_or(snapshot.current_frame);
617 snapshot.image_selection.selected(target_frame)
618 }
619
620 pub fn set_supercell(&self, repeats: [u32; 3]) -> Result<(), ViewerSessionClosed> {
621 self.sender
622 .send(ViewerCommand::SetSupercell { repeats })
623 .map_err(|_| ViewerSessionClosed)
624 }
625
626 pub fn increment_supercell_axis(&self, axis: usize) -> Result<(), ViewerSessionClosed> {
627 self.sender
628 .send(ViewerCommand::IncrementSupercellAxis { axis })
629 .map_err(|_| ViewerSessionClosed)
630 }
631
632 pub fn decrement_supercell_axis(&self, axis: usize) -> Result<(), ViewerSessionClosed> {
633 self.sender
634 .send(ViewerCommand::DecrementSupercellAxis { axis })
635 .map_err(|_| ViewerSessionClosed)
636 }
637
638 pub fn reset_supercell(&self) -> Result<(), ViewerSessionClosed> {
639 self.sender
640 .send(ViewerCommand::ResetSupercell)
641 .map_err(|_| ViewerSessionClosed)
642 }
643
644 pub fn set_ghost_repeated_images(&self, enabled: bool) -> Result<(), ViewerSessionClosed> {
645 self.sender
646 .send(ViewerCommand::SetGhostRepeatedImages { enabled })
647 .map_err(|_| ViewerSessionClosed)
648 }
649
650 pub fn toggle_ghost_repeated_images(&self) -> Result<(), ViewerSessionClosed> {
651 self.sender
652 .send(ViewerCommand::ToggleGhostRepeatedImages)
653 .map_err(|_| ViewerSessionClosed)
654 }
655
656 pub fn supercell(&self) -> SupercellSettings {
657 let Ok(snapshot) = self.snapshot.lock() else {
658 return SupercellSettings::default();
659 };
660 snapshot.supercell
661 }
662
663 pub fn set_camera_view(
664 &self,
665 focus: Option<[f32; 3]>,
666 radius: Option<f32>,
667 yaw: Option<f32>,
668 pitch: Option<f32>,
669 ) -> Result<(), ViewerSessionClosed> {
670 self.sender
671 .send(ViewerCommand::SetCameraView {
672 focus,
673 radius,
674 yaw,
675 pitch,
676 })
677 .map_err(|_| ViewerSessionClosed)
678 }
679
680 pub fn pan_camera(&self, delta: [f32; 3]) -> Result<(), ViewerSessionClosed> {
681 self.sender
682 .send(ViewerCommand::PanCamera { delta })
683 .map_err(|_| ViewerSessionClosed)
684 }
685
686 pub fn zoom_camera(
687 &self,
688 factor: Option<f32>,
689 delta: Option<f32>,
690 ) -> Result<(), ViewerSessionClosed> {
691 self.sender
692 .send(ViewerCommand::ZoomCamera { factor, delta })
693 .map_err(|_| ViewerSessionClosed)
694 }
695
696 pub fn orbit_camera(
697 &self,
698 yaw_delta: f32,
699 pitch_delta: f32,
700 ) -> Result<(), ViewerSessionClosed> {
701 self.sender
702 .send(ViewerCommand::OrbitCamera {
703 yaw_delta,
704 pitch_delta,
705 })
706 .map_err(|_| ViewerSessionClosed)
707 }
708
709 pub fn frame_all(&self) -> Result<(), ViewerSessionClosed> {
710 self.sender
711 .send(ViewerCommand::FrameAll)
712 .map_err(|_| ViewerSessionClosed)
713 }
714
715 pub fn start_orbit(&self, yaw_rate: f32, pitch_rate: f32) -> Result<(), ViewerSessionClosed> {
716 self.sender
717 .send(ViewerCommand::StartOrbit {
718 yaw_rate,
719 pitch_rate,
720 })
721 .map_err(|_| ViewerSessionClosed)
722 }
723
724 pub fn stop_camera_motion(&self) -> Result<(), ViewerSessionClosed> {
725 self.sender
726 .send(ViewerCommand::StopCameraMotion)
727 .map_err(|_| ViewerSessionClosed)
728 }
729
730 pub fn close(&self) -> Result<(), ViewerSessionClosed> {
731 self.sender
732 .send(ViewerCommand::Close)
733 .map_err(|_| ViewerSessionClosed)
734 }
735}
736
737impl ViewerReadiness {
738 pub fn new() -> Self {
739 Self {
740 state: Mutex::new(ViewerLifecycleState::Pending),
741 changed: Condvar::new(),
742 }
743 }
744
745 pub fn mark_ready(&self) {
746 let mut state = self.state.lock().expect("viewer readiness mutex poisoned");
747 if *state == ViewerLifecycleState::Pending {
748 *state = ViewerLifecycleState::Ready;
749 self.changed.notify_all();
750 }
751 }
752
753 pub fn mark_closed(&self) {
754 let mut state = self.state.lock().expect("viewer readiness mutex poisoned");
755 if *state != ViewerLifecycleState::Closed {
756 *state = ViewerLifecycleState::Closed;
757 self.changed.notify_all();
758 }
759 }
760
761 pub fn wait(&self, timeout: Option<Duration>) -> bool {
762 let mut state = self.state.lock().expect("viewer readiness mutex poisoned");
763
764 match timeout {
765 Some(timeout) => {
766 let deadline = Instant::now() + timeout;
767 while *state == ViewerLifecycleState::Pending {
768 let now = Instant::now();
769 if now >= deadline {
770 return false;
771 }
772
773 let remaining = deadline.saturating_duration_since(now);
774 let (next_state, result) = self
775 .changed
776 .wait_timeout(state, remaining)
777 .expect("viewer readiness mutex poisoned");
778 state = next_state;
779 if result.timed_out() && *state == ViewerLifecycleState::Pending {
780 return false;
781 }
782 }
783 }
784 None => {
785 while *state == ViewerLifecycleState::Pending {
786 state = self
787 .changed
788 .wait(state)
789 .expect("viewer readiness mutex poisoned");
790 }
791 }
792 }
793
794 *state == ViewerLifecycleState::Ready
795 }
796}
797
798impl Default for ViewerReadiness {
799 fn default() -> Self {
800 Self::new()
801 }
802}
803
804#[derive(Clone, Debug, PartialEq)]
805pub struct DisplayAtom {
806 pub identity: SelectedImageAtom,
807 pub position: [f64; 3],
808 pub is_main_cell: bool,
809}
810
811#[derive(Resource)]
812pub struct ViewerState {
813 pub traj: Trajectory,
814 pub current: usize,
815 pub follow_tail: bool,
816 pub atom_scalars: HashMap<String, Vec<Option<Vec<f32>>>>,
817 pub atom_appearance_rules: Vec<AtomAppearanceRule>,
818 pub bonds: BondFrames,
819 pub faces: FaceFrames,
820 pub render_style_rules: Vec<RenderStyleRule>,
821 pub selection: SelectionFrames,
822 pub image_selection: ImageSelectionFrames,
823 pub supercell: SupercellSettings,
824 pub needs_render: bool,
825 pub needs_camera_reset: bool,
826}
827
828impl ViewerState {
829 pub fn new(traj: Trajectory, initial_frame: usize) -> Self {
830 let frame_count = traj.len();
831 let current = clamp_frame(initial_frame, traj.len());
832 let selection = SelectionFrames::new(&traj);
833 let image_selection = ImageSelectionFrames::new(frame_count);
834 Self {
835 traj,
836 current,
837 follow_tail: false,
838 atom_scalars: HashMap::new(),
839 atom_appearance_rules: Vec::new(),
840 bonds: BondFrames::new(frame_count),
841 faces: FaceFrames::new(frame_count),
842 render_style_rules: Vec::new(),
843 selection,
844 image_selection,
845 supercell: SupercellSettings::default(),
846 needs_render: true,
847 needs_camera_reset: true,
848 }
849 }
850
851 pub fn trajectory_len(&self) -> usize {
852 self.traj.len()
853 }
854
855 pub fn has_frames(&self) -> bool {
856 !self.traj.is_empty()
857 }
858
859 pub fn apply_command(&mut self, command: ViewerCommand) -> CommandOutcome {
860 match command {
861 ViewerCommand::LoadTrajectory {
862 frames,
863 initial_frame,
864 } => {
865 let frame_count = frames.len();
866 self.traj = Trajectory::new(frames);
867 self.current = clamp_frame(initial_frame, self.traj.len());
868 self.resize_scalar_storage(frame_count);
869 self.bonds = BondFrames::new(frame_count);
870 self.faces = FaceFrames::new(frame_count);
871 self.render_style_rules.clear();
872 self.selection = SelectionFrames::new(&self.traj);
873 self.image_selection = ImageSelectionFrames::new(frame_count);
874 self.supercell = SupercellSettings::default();
875 self.needs_render = true;
876 self.needs_camera_reset = true;
877 CommandOutcome::default()
878 }
879 ViewerCommand::AppendFrame { frame } => {
880 let was_empty = self.traj.is_empty();
881 let previous = if was_empty { None } else { Some(self.current) };
882 self.traj.append(frame);
883 self.bonds.resize(self.traj.len());
884 self.faces.resize(self.traj.len());
885 self.selection.append_empty_for_atom_count(
886 self.traj.view(self.traj.len() - 1).positions.len(),
887 );
888 self.image_selection.append_empty_frame();
889 if was_empty {
890 self.current = 0;
891 self.needs_render = true;
892 self.needs_camera_reset = true;
893 } else if self.follow_tail {
894 let next = self.traj.len() - 1;
895 if let Some(previous) = previous {
896 self.copy_selection_between_compatible_frames(previous, next);
897 }
898 self.current = next;
899 self.needs_render = true;
900 self.needs_camera_reset = previous
901 .map(|old| self.cell_changed(old, self.current))
902 .unwrap_or(false);
903 }
904 CommandOutcome::default()
905 }
906 ViewerCommand::SetCurrentFrame { index } => {
907 if index < self.traj.len() && index != self.current {
908 let previous = self.current;
909 self.copy_selection_between_compatible_frames(previous, index);
910 self.current = index;
911 self.needs_render = true;
912 self.needs_camera_reset = self.cell_changed(previous, self.current);
913 }
914 CommandOutcome::default()
915 }
916 ViewerCommand::SetFollowTail { enabled } => {
917 self.follow_tail = enabled;
918 CommandOutcome::default()
919 }
920 ViewerCommand::SetAtomScalars {
921 name,
922 values,
923 frame_index,
924 } => {
925 let target_frame = frame_index.unwrap_or(self.current);
926 let should_refresh_active_mode = self.current == target_frame
927 && self
928 .atom_appearance_rules
929 .iter()
930 .any(|rule| rule.name == name);
931 if self.validate_scalar_values(target_frame, &values) {
932 self.store_scalars(name, target_frame, values);
933 self.needs_render = should_refresh_active_mode;
934 }
935 CommandOutcome::default()
936 }
937 ViewerCommand::MapAppearanceByScalar {
938 name,
939 channel,
940 palette,
941 min,
942 max,
943 append,
944 } => {
945 let rule = AtomAppearanceRule {
946 name,
947 channel,
948 palette,
949 min,
950 max,
951 };
952 if append {
953 self.atom_appearance_rules.push(rule);
954 } else {
955 self.atom_appearance_rules
956 .retain(|existing| existing.channel != channel);
957 self.atom_appearance_rules.push(rule);
958 }
959 self.needs_render = true;
960 CommandOutcome::default()
961 }
962 ViewerCommand::ResetAtomAppearance { channel } => {
963 match channel {
964 Some(channel) => {
965 self.atom_appearance_rules
966 .retain(|rule| rule.channel != channel);
967 }
968 None => self.atom_appearance_rules.clear(),
969 }
970 self.needs_render = true;
971 CommandOutcome::default()
972 }
973 ViewerCommand::SetBonds { bonds, frame_index } => {
974 let target_frame = frame_index.unwrap_or(self.current);
975 if target_frame < self.traj.len() {
976 self.bonds.set(target_frame, bonds);
977 self.needs_render = self.current == target_frame;
978 }
979 CommandOutcome::default()
980 }
981 ViewerCommand::AddBonds { bonds, frame_index } => {
982 let target_frame = frame_index.unwrap_or(self.current);
983 if target_frame < self.traj.len() {
984 self.bonds.add(target_frame, bonds);
985 self.needs_render = self.current == target_frame;
986 }
987 CommandOutcome::default()
988 }
989 ViewerCommand::RemoveBonds { bonds, frame_index } => {
990 let target_frame = frame_index.unwrap_or(self.current);
991 if target_frame < self.traj.len() {
992 self.bonds.remove(target_frame, &bonds);
993 self.needs_render = self.current == target_frame;
994 }
995 CommandOutcome::default()
996 }
997 ViewerCommand::ClearBonds { frame_index } => {
998 let target_frame = frame_index.unwrap_or(self.current);
999 if target_frame < self.traj.len() {
1000 self.bonds.clear(target_frame);
1001 self.needs_render = self.current == target_frame;
1002 }
1003 CommandOutcome::default()
1004 }
1005 ViewerCommand::SetFaces { faces, frame_index } => {
1006 let target_frame = frame_index.unwrap_or(self.current);
1007 if target_frame < self.traj.len() {
1008 let atom_count = self.traj.view(target_frame).positions.len();
1009 self.faces
1010 .set(target_frame, faces.validated_for_atom_count(atom_count));
1011 self.needs_render = self.current == target_frame;
1012 }
1013 CommandOutcome::default()
1014 }
1015 ViewerCommand::AddFaces { faces, frame_index } => {
1016 let target_frame = frame_index.unwrap_or(self.current);
1017 if target_frame < self.traj.len() {
1018 let atom_count = self.traj.view(target_frame).positions.len();
1019 self.faces
1020 .add(target_frame, faces.validated_for_atom_count(atom_count));
1021 self.needs_render = self.current == target_frame;
1022 }
1023 CommandOutcome::default()
1024 }
1025 ViewerCommand::RemoveFaces { faces, frame_index } => {
1026 let target_frame = frame_index.unwrap_or(self.current);
1027 if target_frame < self.traj.len() {
1028 let atom_count = self.traj.view(target_frame).positions.len();
1029 self.faces
1030 .remove(target_frame, &faces.validated_for_atom_count(atom_count));
1031 self.needs_render = self.current == target_frame;
1032 }
1033 CommandOutcome::default()
1034 }
1035 ViewerCommand::ClearFaces { frame_index } => {
1036 let target_frame = frame_index.unwrap_or(self.current);
1037 if target_frame < self.traj.len() {
1038 self.faces.clear(target_frame);
1039 self.needs_render = self.current == target_frame;
1040 }
1041 CommandOutcome::default()
1042 }
1043 ViewerCommand::SetRenderStyle {
1044 style,
1045 selection,
1046 frame_index,
1047 append,
1048 } => {
1049 let target_frame = frame_index.unwrap_or(self.current);
1050 if self.validate_selection(target_frame, &selection) {
1051 let rule = RenderStyleRule {
1052 frame_index: target_frame,
1053 selection,
1054 style,
1055 };
1056 if append {
1057 self.render_style_rules.push(rule);
1058 } else {
1059 self.render_style_rules.clear();
1060 self.render_style_rules.push(rule);
1061 }
1062 self.needs_render = self.current == target_frame;
1063 }
1064 CommandOutcome::default()
1065 }
1066 ViewerCommand::ResetRenderStyle => {
1067 self.render_style_rules.clear();
1068 self.needs_render = true;
1069 CommandOutcome::default()
1070 }
1071 ViewerCommand::ReplaceSelection {
1072 selection,
1073 frame_index,
1074 } => {
1075 let target_frame = frame_index.unwrap_or(self.current);
1076 if self.validate_selection(target_frame, &selection) {
1077 self.selection.replace(target_frame, selection);
1078 self.image_selection.replace(
1079 target_frame,
1080 self.selection.selected_main_images(target_frame),
1081 );
1082 self.needs_render = self.current == target_frame;
1083 }
1084 CommandOutcome::default()
1085 }
1086 ViewerCommand::AddSelection {
1087 selection,
1088 frame_index,
1089 } => {
1090 let target_frame = frame_index.unwrap_or(self.current);
1091 if self.validate_selection(target_frame, &selection) {
1092 self.selection.add(target_frame, &selection);
1093 self.image_selection.replace(
1094 target_frame,
1095 self.selection.selected_main_images(target_frame),
1096 );
1097 self.needs_render = self.current == target_frame;
1098 }
1099 CommandOutcome::default()
1100 }
1101 ViewerCommand::RemoveSelection {
1102 selection,
1103 frame_index,
1104 } => {
1105 let target_frame = frame_index.unwrap_or(self.current);
1106 if self.validate_selection(target_frame, &selection) {
1107 self.selection.remove(target_frame, &selection);
1108 self.image_selection.replace(
1109 target_frame,
1110 self.selection.selected_main_images(target_frame),
1111 );
1112 self.needs_render = self.current == target_frame;
1113 }
1114 CommandOutcome::default()
1115 }
1116 ViewerCommand::ClearSelection { frame_index } => {
1117 let target_frame = frame_index.unwrap_or(self.current);
1118 if target_frame < self.traj.len() {
1119 self.selection.clear(target_frame);
1120 self.image_selection.clear(target_frame);
1121 self.needs_render = self.current == target_frame;
1122 }
1123 CommandOutcome::default()
1124 }
1125 ViewerCommand::ReplaceImageSelection {
1126 selection,
1127 frame_index,
1128 } => {
1129 let target_frame = frame_index.unwrap_or(self.current);
1130 if self.validate_image_selection(target_frame, &selection) {
1131 self.image_selection.replace(target_frame, selection);
1132 self.sync_main_selection_from_images(target_frame);
1133 self.needs_render = self.current == target_frame;
1134 }
1135 CommandOutcome::default()
1136 }
1137 ViewerCommand::AddImageSelection {
1138 selection,
1139 frame_index,
1140 } => {
1141 let target_frame = frame_index.unwrap_or(self.current);
1142 if self.validate_image_selection(target_frame, &selection) {
1143 self.image_selection.add(target_frame, &selection);
1144 self.sync_main_selection_from_images(target_frame);
1145 self.needs_render = self.current == target_frame;
1146 }
1147 CommandOutcome::default()
1148 }
1149 ViewerCommand::RemoveImageSelection {
1150 selection,
1151 frame_index,
1152 } => {
1153 let target_frame = frame_index.unwrap_or(self.current);
1154 if self.validate_image_selection(target_frame, &selection) {
1155 self.image_selection.remove(target_frame, &selection);
1156 self.sync_main_selection_from_images(target_frame);
1157 self.needs_render = self.current == target_frame;
1158 }
1159 CommandOutcome::default()
1160 }
1161 ViewerCommand::ClearImageSelection { frame_index } => {
1162 let target_frame = frame_index.unwrap_or(self.current);
1163 if target_frame < self.traj.len() {
1164 self.image_selection.clear(target_frame);
1165 self.sync_main_selection_from_images(target_frame);
1166 self.needs_render = self.current == target_frame;
1167 }
1168 CommandOutcome::default()
1169 }
1170 ViewerCommand::SetSupercell { repeats } => {
1171 if self.supercell.repeats != repeats {
1172 self.supercell.repeats = repeats;
1173 self.needs_render = true;
1174 }
1175 CommandOutcome::default()
1176 }
1177 ViewerCommand::IncrementSupercellAxis { axis } => {
1178 if axis < 3 {
1179 self.supercell.repeats[axis] = self.supercell.repeats[axis].saturating_add(1);
1180 self.needs_render = true;
1181 }
1182 CommandOutcome::default()
1183 }
1184 ViewerCommand::DecrementSupercellAxis { axis } => {
1185 if axis < 3 {
1186 let next = self.supercell.repeats[axis].saturating_sub(1);
1187 if self.supercell.repeats[axis] != next {
1188 self.supercell.repeats[axis] = next;
1189 self.needs_render = true;
1190 }
1191 }
1192 CommandOutcome::default()
1193 }
1194 ViewerCommand::ResetSupercell => {
1195 if self.supercell.repeats != [0, 0, 0] {
1196 self.supercell.repeats = [0, 0, 0];
1197 self.needs_render = true;
1198 }
1199 CommandOutcome::default()
1200 }
1201 ViewerCommand::SetGhostRepeatedImages { enabled } => {
1202 if self.supercell.ghost_repeated_images != enabled {
1203 self.supercell.ghost_repeated_images = enabled;
1204 self.needs_render = true;
1205 }
1206 CommandOutcome::default()
1207 }
1208 ViewerCommand::ToggleGhostRepeatedImages => {
1209 self.supercell.ghost_repeated_images = !self.supercell.ghost_repeated_images;
1210 self.needs_render = true;
1211 CommandOutcome::default()
1212 }
1213 ViewerCommand::SetCameraView { .. }
1214 | ViewerCommand::PanCamera { .. }
1215 | ViewerCommand::ZoomCamera { .. }
1216 | ViewerCommand::OrbitCamera { .. }
1217 | ViewerCommand::FrameAll
1218 | ViewerCommand::StartOrbit { .. }
1219 | ViewerCommand::StopCameraMotion => CommandOutcome::default(),
1220 ViewerCommand::Close => CommandOutcome { should_close: true },
1221 }
1222 }
1223
1224 pub fn step_frame(&mut self, delta: isize) -> bool {
1225 if self.traj.is_empty() {
1226 return false;
1227 }
1228
1229 let next = self.current as isize + delta;
1230 if next < 0 || next >= self.traj.len() as isize {
1231 return false;
1232 }
1233
1234 let next = next as usize;
1235 if next == self.current {
1236 return false;
1237 }
1238
1239 self.set_current_frame_index(next)
1240 }
1241
1242 pub fn set_current_frame_index(&mut self, index: usize) -> bool {
1243 if index >= self.traj.len() || index == self.current {
1244 return false;
1245 }
1246
1247 let previous = self.current;
1248 self.copy_selection_between_compatible_frames(previous, index);
1249 self.current = index;
1250 self.needs_render = true;
1251 self.needs_camera_reset = self.cell_changed(previous, self.current);
1252 true
1253 }
1254
1255 pub fn is_at_last_frame(&self) -> bool {
1256 self.has_frames() && self.current + 1 >= self.traj.len()
1257 }
1258
1259 pub fn selected_atoms(&self, frame_index: usize) -> Vec<usize> {
1260 self.selection.selected_indices(frame_index)
1261 }
1262
1263 pub fn selected_images(&self, frame_index: usize) -> Vec<SelectedImageAtom> {
1264 let selected = self.image_selection.selected(frame_index);
1265 if selected.is_empty() {
1266 self.selection.selected_main_images(frame_index)
1267 } else {
1268 selected
1269 }
1270 }
1271
1272 pub fn current_selection(&self) -> &[bool] {
1273 self.selection.get(self.current).unwrap_or(&[])
1274 }
1275
1276 pub fn current_image_selection(&self) -> &[SelectedImageAtom] {
1277 self.image_selection.get(self.current).unwrap_or(&[])
1278 }
1279
1280 pub fn current_supercell(&self) -> SupercellSettings {
1281 self.supercell
1282 }
1283
1284 pub fn replace_atom_selection(&mut self, atom: SelectedImageAtom) -> bool {
1285 let changed = self.image_selection.replace_single(self.current, atom);
1286 if changed {
1287 self.sync_main_selection_from_images(self.current);
1288 }
1289 changed
1290 }
1291
1292 pub fn toggle_atom_selection(&mut self, atom: SelectedImageAtom) -> bool {
1293 let changed = self.image_selection.toggle(self.current, atom);
1294 if changed {
1295 self.sync_main_selection_from_images(self.current);
1296 }
1297 changed
1298 }
1299
1300 pub fn display_atoms(&self, frame_index: usize) -> Vec<DisplayAtom> {
1301 if frame_index >= self.traj.len() {
1302 return Vec::new();
1303 }
1304
1305 let view = self.traj.view(frame_index);
1306 let offsets = supercell_offsets(self.supercell.repeats);
1307 let mut atoms = Vec::with_capacity(view.positions.len() * offsets.len());
1308 for image_offset in offsets {
1309 let shift = scaled_cell_translation(view.cell, image_offset);
1310 for (atom_index, position) in view.positions.iter().copied().enumerate() {
1311 atoms.push(DisplayAtom {
1312 identity: SelectedImageAtom {
1313 atom_index,
1314 image_offset,
1315 },
1316 position: [
1317 position[0] + shift[0],
1318 position[1] + shift[1],
1319 position[2] + shift[2],
1320 ],
1321 is_main_cell: image_offset == [0, 0, 0],
1322 });
1323 }
1324 }
1325 atoms
1326 }
1327
1328 fn cell_changed(&self, old_index: usize, new_index: usize) -> bool {
1329 self.traj.view(old_index).cell != self.traj.view(new_index).cell
1330 }
1331
1332 fn frames_have_matching_atom_identity(&self, source_index: usize, target_index: usize) -> bool {
1333 if source_index >= self.traj.len() || target_index >= self.traj.len() {
1334 return false;
1335 }
1336
1337 let source = self.traj.view(source_index);
1338 let target = self.traj.view(target_index);
1339 source.positions.len() == target.positions.len() && source.numbers == target.numbers
1340 }
1341
1342 fn copy_selection_between_compatible_frames(
1343 &mut self,
1344 source_index: usize,
1345 target_index: usize,
1346 ) {
1347 if !self.frames_have_matching_atom_identity(source_index, target_index) {
1348 return;
1349 }
1350
1351 let selection = self.selected_images(source_index);
1352 if !self.validate_image_selection(target_index, &selection) {
1353 return;
1354 }
1355
1356 self.image_selection.replace(target_index, selection);
1357 self.sync_main_selection_from_images(target_index);
1358 }
1359
1360 fn validate_scalar_values(&self, frame_index: usize, values: &[f32]) -> bool {
1361 frame_index < self.traj.len() && self.traj.view(frame_index).positions.len() == values.len()
1362 }
1363
1364 fn validate_selection(&self, frame_index: usize, selection: &[bool]) -> bool {
1365 frame_index < self.traj.len()
1366 && self.traj.view(frame_index).positions.len() == selection.len()
1367 }
1368
1369 fn validate_image_selection(
1370 &self,
1371 frame_index: usize,
1372 selection: &[SelectedImageAtom],
1373 ) -> bool {
1374 if frame_index >= self.traj.len() {
1375 return false;
1376 }
1377 let atom_count = self.traj.view(frame_index).positions.len();
1378 let allowed_offsets = supercell_offsets(self.supercell.repeats);
1379 selection.iter().all(|atom| {
1380 atom.atom_index < atom_count && allowed_offsets.contains(&atom.image_offset)
1381 })
1382 }
1383
1384 fn store_scalars(&mut self, name: String, frame_index: usize, values: Vec<f32>) {
1385 let frame_count = self.traj.len();
1386 let entries = self
1387 .atom_scalars
1388 .entry(name)
1389 .or_insert_with(|| vec![None; frame_count]);
1390 if entries.len() < frame_count {
1391 entries.resize(frame_count, None);
1392 }
1393 entries[frame_index] = Some(values);
1394 }
1395
1396 fn resize_scalar_storage(&mut self, frame_count: usize) {
1397 for values in self.atom_scalars.values_mut() {
1398 values.resize(frame_count, None);
1399 }
1400 }
1401
1402 fn sync_main_selection_from_images(&mut self, frame_index: usize) {
1403 let Some(selected) = self.image_selection.get(frame_index) else {
1404 return;
1405 };
1406 let atom_count = self.traj.view(frame_index).positions.len();
1407 self.selection.replace(
1408 frame_index,
1409 SelectionFrames::mask_from_images(atom_count, selected),
1410 );
1411 self.selection.set_order(
1412 frame_index,
1413 SelectionFrames::ordered_atoms_from_images(selected),
1414 );
1415 }
1416}
1417
1418impl BondList {
1419 pub fn new(edges: impl IntoIterator<Item = (usize, usize)>) -> Self {
1420 let mut canonical = BTreeSet::new();
1421 for (i, j) in edges {
1422 if i == j {
1423 continue;
1424 }
1425 canonical.insert((i.min(j), i.max(j)));
1426 }
1427 Self {
1428 edges: canonical.into_iter().collect(),
1429 }
1430 }
1431
1432 pub fn iter(&self) -> impl Iterator<Item = &(usize, usize)> {
1433 self.edges.iter()
1434 }
1435
1436 pub fn len(&self) -> usize {
1437 self.edges.len()
1438 }
1439
1440 pub fn is_empty(&self) -> bool {
1441 self.edges.is_empty()
1442 }
1443
1444 pub fn merged(&self, other: &BondList) -> Self {
1445 Self::new(self.iter().copied().chain(other.iter().copied()))
1446 }
1447
1448 pub fn without(&self, other: &BondList) -> Self {
1449 let removals: BTreeSet<_> = other.iter().copied().collect();
1450 Self::new(self.iter().copied().filter(|edge| !removals.contains(edge)))
1451 }
1452}
1453
1454impl BondFrames {
1455 pub fn new(frame_count: usize) -> Self {
1456 Self {
1457 frames: vec![None; frame_count],
1458 }
1459 }
1460
1461 pub fn set(&mut self, frame_index: usize, bonds: BondList) {
1462 if frame_index < self.frames.len() {
1463 self.frames[frame_index] = Some(bonds);
1464 }
1465 }
1466
1467 pub fn add(&mut self, frame_index: usize, bonds: BondList) {
1468 if frame_index >= self.frames.len() {
1469 return;
1470 }
1471 let merged = match self.frames[frame_index].take() {
1472 Some(existing) => existing.merged(&bonds),
1473 None => bonds,
1474 };
1475 self.frames[frame_index] = Some(merged);
1476 }
1477
1478 pub fn remove(&mut self, frame_index: usize, bonds: &BondList) {
1479 if frame_index >= self.frames.len() {
1480 return;
1481 }
1482 if let Some(existing) = self.frames[frame_index].take() {
1483 let updated = existing.without(bonds);
1484 self.frames[frame_index] = (!updated.is_empty()).then_some(updated);
1485 }
1486 }
1487
1488 pub fn clear(&mut self, frame_index: usize) {
1489 if frame_index < self.frames.len() {
1490 self.frames[frame_index] = None;
1491 }
1492 }
1493
1494 pub fn get(&self, frame_index: usize) -> Option<&BondList> {
1495 self.frames
1496 .get(frame_index)
1497 .and_then(|bonds| bonds.as_ref())
1498 }
1499
1500 pub fn resize(&mut self, frame_count: usize) {
1501 self.frames.resize(frame_count, None);
1502 }
1503}
1504
1505impl Face {
1506 pub fn new(atoms: impl IntoIterator<Item = usize>, color: [f32; 4]) -> Option<Self> {
1507 let atoms: SmallVec<[usize; 4]> = atoms.into_iter().collect();
1508 if atoms.len() < 3 {
1509 return None;
1510 }
1511 let unique: BTreeSet<_> = atoms.iter().copied().collect();
1512 if unique.len() != atoms.len() {
1513 return None;
1514 }
1515 Some(Self { atoms, color })
1516 }
1517
1518 pub fn color(self) -> bevy::color::Color {
1519 bevy::color::Color::srgba(self.color[0], self.color[1], self.color[2], self.color[3])
1520 }
1521
1522 fn canonical_atoms(&self) -> Vec<usize> {
1523 canonicalize_face_atoms(&self.atoms)
1524 }
1525}
1526
1527impl FaceList {
1528 pub fn new(faces: impl IntoIterator<Item = Face>) -> Self {
1529 let mut seen = BTreeSet::new();
1530 let mut canonical = Vec::new();
1531
1532 for face in faces {
1533 let key = face.canonical_atoms();
1534 if seen.insert(key) {
1535 canonical.push(face);
1536 }
1537 }
1538
1539 Self { faces: canonical }
1540 }
1541
1542 pub fn iter(&self) -> impl Iterator<Item = &Face> {
1543 self.faces.iter()
1544 }
1545
1546 pub fn len(&self) -> usize {
1547 self.faces.len()
1548 }
1549
1550 pub fn is_empty(&self) -> bool {
1551 self.faces.is_empty()
1552 }
1553
1554 pub fn validated_for_atom_count(&self, atom_count: usize) -> Self {
1555 Self::new(
1556 self.faces
1557 .iter()
1558 .filter(|face| face.atoms.iter().all(|&index| index < atom_count))
1559 .cloned(),
1560 )
1561 }
1562
1563 pub fn merged(&self, other: &FaceList) -> Self {
1564 Self::new(self.iter().cloned().chain(other.iter().cloned()))
1565 }
1566
1567 pub fn without(&self, other: &FaceList) -> Self {
1568 let removals: BTreeSet<_> = other.iter().map(Face::canonical_atoms).collect();
1569 Self::new(
1570 self.faces
1571 .iter()
1572 .filter(|face| !removals.contains(&face.canonical_atoms()))
1573 .cloned(),
1574 )
1575 }
1576}
1577
1578impl FaceFrames {
1579 pub fn new(frame_count: usize) -> Self {
1580 Self {
1581 frames: vec![None; frame_count],
1582 }
1583 }
1584
1585 pub fn set(&mut self, frame_index: usize, faces: FaceList) {
1586 if frame_index < self.frames.len() {
1587 self.frames[frame_index] = Some(faces);
1588 }
1589 }
1590
1591 pub fn add(&mut self, frame_index: usize, faces: FaceList) {
1592 if frame_index >= self.frames.len() {
1593 return;
1594 }
1595 let merged = match self.frames[frame_index].take() {
1596 Some(existing) => existing.merged(&faces),
1597 None => faces,
1598 };
1599 self.frames[frame_index] = Some(merged);
1600 }
1601
1602 pub fn remove(&mut self, frame_index: usize, faces: &FaceList) {
1603 if frame_index >= self.frames.len() {
1604 return;
1605 }
1606 if let Some(existing) = self.frames[frame_index].take() {
1607 let updated = existing.without(faces);
1608 self.frames[frame_index] = (!updated.is_empty()).then_some(updated);
1609 }
1610 }
1611
1612 pub fn clear(&mut self, frame_index: usize) {
1613 if frame_index < self.frames.len() {
1614 self.frames[frame_index] = None;
1615 }
1616 }
1617
1618 pub fn get(&self, frame_index: usize) -> Option<&FaceList> {
1619 self.frames
1620 .get(frame_index)
1621 .and_then(|faces| faces.as_ref())
1622 }
1623
1624 pub fn resize(&mut self, frame_count: usize) {
1625 self.frames.resize(frame_count, None);
1626 }
1627}
1628
1629impl BallAndStickStyle {
1630 pub fn bond_color(self) -> bevy::color::Color {
1631 bevy::color::Color::srgba(
1632 self.bond_color[0],
1633 self.bond_color[1],
1634 self.bond_color[2],
1635 self.bond_color[3],
1636 )
1637 }
1638}
1639
1640#[derive(Default)]
1641pub struct CommandOutcome {
1642 pub should_close: bool,
1643}
1644
1645impl Default for ViewerSnapshot {
1646 fn default() -> Self {
1647 Self {
1648 current_frame: 0,
1649 selection: SelectionFrames {
1650 frames: Vec::new(),
1651 ordered: Vec::new(),
1652 },
1653 image_selection: ImageSelectionFrames { frames: Vec::new() },
1654 supercell: SupercellSettings::default(),
1655 }
1656 }
1657}
1658
1659impl Default for SupercellSettings {
1660 fn default() -> Self {
1661 Self {
1662 repeats: [0, 0, 0],
1663 ghost_repeated_images: true,
1664 }
1665 }
1666}
1667
1668fn clamp_frame(index: usize, len: usize) -> usize {
1669 if len == 0 { 0 } else { index.min(len - 1) }
1670}
1671
1672fn axis_offsets(repeat_extent: u32) -> Vec<i32> {
1673 let repeat_extent = repeat_extent as i32;
1674 (-repeat_extent..=repeat_extent).collect()
1675}
1676
1677fn supercell_offsets(repeats: [u32; 3]) -> Vec<[i32; 3]> {
1678 let mut offsets = Vec::new();
1679 for ia in axis_offsets(repeats[0]) {
1680 for ib in axis_offsets(repeats[1]) {
1681 for ic in axis_offsets(repeats[2]) {
1682 offsets.push([ia, ib, ic]);
1683 }
1684 }
1685 }
1686 offsets.sort_by_key(|offset| {
1687 (
1688 offset.iter().map(|value| value.abs()).sum::<i32>(),
1689 offset[0].abs(),
1690 offset[1].abs(),
1691 offset[2].abs(),
1692 offset[0],
1693 offset[1],
1694 offset[2],
1695 )
1696 });
1697 offsets
1698}
1699
1700fn scaled_cell_translation(cell: ak_core::geometry::Cell, offset: [i32; 3]) -> [f64; 3] {
1701 let a = cell.a();
1702 let b = cell.b();
1703 let c = cell.c();
1704 let shift = a * offset[0] as f64 + b * offset[1] as f64 + c * offset[2] as f64;
1705 [shift[0], shift[1], shift[2]]
1706}
1707
1708fn canonicalize_face_atoms(atoms: &[usize]) -> Vec<usize> {
1709 let forward = minimum_face_rotation(atoms);
1710 let mut reversed = atoms.to_vec();
1711 reversed.reverse();
1712 let reversed = minimum_face_rotation(&reversed);
1713 if reversed < forward {
1714 reversed
1715 } else {
1716 forward
1717 }
1718}
1719
1720fn minimum_face_rotation(atoms: &[usize]) -> Vec<usize> {
1721 let mut best = atoms.to_vec();
1722 for shift in 1..atoms.len() {
1723 let rotated: Vec<usize> = atoms
1724 .iter()
1725 .cycle()
1726 .skip(shift)
1727 .take(atoms.len())
1728 .copied()
1729 .collect();
1730 if rotated < best {
1731 best = rotated;
1732 }
1733 }
1734 best
1735}