ak_vis/viewer/
session.rs

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}