ak_vis/viewer/
app.rs

1use ak_core::{Structure, Trajectory};
2use bevy::{input_focus::InputDispatchPlugin, prelude::*, ui_widgets::UiWidgetsPlugins};
3use bevy_panorbit_camera::{PanOrbitCameraPlugin, PanOrbitCameraSystemSet};
4use std::sync::{Arc, Mutex, mpsc};
5use std::thread;
6
7use crate::ui::{
8    handle_playback_buttons, set_camera_input_enabled, setup_ui, sync_inspector_camera,
9    sync_inspector_state, sync_inspector_text, sync_playback_camera, sync_playback_slider_value,
10    sync_playback_state, sync_playback_text, sync_playback_visibility, toggle_hints_visibility,
11};
12use crate::viewer::ViewerConfig;
13use crate::viewer::controls::{
14    keyboard_controls, navigate_frames, screenshot_on_spacebar, screenshot_saving,
15    toggle_ui_visibility, toggle_view,
16};
17use crate::viewer::orientation_widget::{
18    setup_orientation_widget, sync_orientation_widget, sync_orientation_widget_letter_strokes,
19    update_orientation_widget_viewport,
20};
21use crate::viewer::runtime::{asset_root, configure_shared_app};
22use crate::viewer::session::{ViewerCommand, ViewerReadiness, ViewerSessionHandle};
23use crate::viewer::systems::{
24    MarqueeSelectionState, handle_atom_clicks, handle_marquee_selection, setup_marquee_overlay,
25    sync_marquee_overlay,
26};
27
28#[derive(Resource, Clone)]
29struct ViewerLifecycle {
30    readiness: Arc<ViewerReadiness>,
31    ready_signaled: bool,
32}
33
34impl Drop for ViewerLifecycle {
35    fn drop(&mut self) {
36        self.readiness.mark_closed();
37    }
38}
39
40fn build_app(
41    trajectory: Trajectory,
42    config: ViewerConfig,
43    receiver: Option<mpsc::Receiver<ViewerCommand>>,
44    readiness: Arc<ViewerReadiness>,
45    snapshot: Arc<Mutex<crate::viewer::session::ViewerSnapshot>>,
46) -> App {
47    let mut app = App::new();
48    let mut plugins = DefaultPlugins.build().disable::<bevy::audio::AudioPlugin>();
49    plugins = plugins.set(AssetPlugin {
50        file_path: asset_root(),
51        ..default()
52    });
53
54    if config.window_width.is_some() || config.window_height.is_some() {
55        let width = config.window_width.unwrap_or(750);
56        let height = config.window_height.unwrap_or(750);
57        plugins = plugins.set(WindowPlugin {
58            primary_window: Some(Window {
59                resolution: (width, height).into(),
60                ..default()
61            }),
62            ..default()
63        });
64    }
65
66    app.add_plugins((
67        plugins,
68        MeshPickingPlugin,
69        PanOrbitCameraPlugin,
70        UiWidgetsPlugins,
71        InputDispatchPlugin,
72    ));
73    configure_shared_app(&mut app, trajectory, config, receiver, snapshot);
74    app.insert_resource(MarqueeSelectionState::default());
75    app.insert_resource(ViewerLifecycle {
76        readiness,
77        ready_signaled: false,
78    })
79    .add_systems(Startup, (setup_orientation_widget, setup_marquee_overlay))
80    .add_systems(
81        Update,
82        (
83            (
84                handle_marquee_selection,
85                handle_atom_clicks,
86                sync_marquee_overlay,
87            )
88                .chain(),
89            toggle_view,
90            keyboard_controls,
91            screenshot_on_spacebar,
92            screenshot_saving,
93            navigate_frames,
94            sync_orientation_widget,
95            sync_orientation_widget_letter_strokes,
96            update_orientation_widget_viewport,
97            signal_viewer_ready,
98        ),
99    );
100
101    if app.world().resource::<ViewerConfig>().render.show_ui {
102        app.add_systems(Startup, setup_ui);
103        app.add_systems(
104            Update,
105            (
106                toggle_ui_visibility,
107                toggle_hints_visibility,
108                handle_playback_buttons,
109                sync_playback_slider_value,
110            ),
111        );
112        app.add_systems(
113            PostUpdate,
114            set_camera_input_enabled.before(PanOrbitCameraSystemSet),
115        );
116        app.add_systems(
117            PostUpdate,
118            (
119                sync_inspector_camera,
120                sync_inspector_state,
121                sync_inspector_text,
122                sync_playback_camera,
123                sync_playback_state,
124                sync_playback_visibility,
125                sync_playback_text,
126            )
127                .chain(),
128        );
129    }
130
131    app
132}
133
134fn run_app(
135    trajectory: Trajectory,
136    config: ViewerConfig,
137    receiver: Option<mpsc::Receiver<ViewerCommand>>,
138    readiness: Arc<ViewerReadiness>,
139    snapshot: Arc<Mutex<crate::viewer::session::ViewerSnapshot>>,
140) {
141    let mut app = build_app(trajectory, config, receiver, readiness, snapshot);
142    app.run();
143}
144
145fn signal_viewer_ready(
146    mut lifecycle: ResMut<'_, ViewerLifecycle>,
147    windows: Query<'_, '_, Entity, With<Window>>,
148) {
149    if lifecycle.ready_signaled || windows.is_empty() {
150        return;
151    }
152
153    lifecycle.ready_signaled = true;
154    lifecycle.readiness.mark_ready();
155}
156
157pub fn run_prepared(
158    trajectory: Trajectory,
159    config: ViewerConfig,
160    receiver: mpsc::Receiver<ViewerCommand>,
161    readiness: Arc<ViewerReadiness>,
162    snapshot: Arc<Mutex<crate::viewer::session::ViewerSnapshot>>,
163) {
164    run_app(trajectory, config, Some(receiver), readiness, snapshot);
165}
166
167pub fn launch(trajectory: Trajectory, config: ViewerConfig) -> ViewerSessionHandle {
168    let (sender, receiver) = mpsc::channel();
169    let handle = ViewerSessionHandle::new(sender);
170    let launch_readiness = handle.readiness().clone();
171    let launch_snapshot = handle.snapshot().clone();
172
173    thread::Builder::new()
174        .name("ak-viewer-session".to_string())
175        .spawn(move || {
176            run_app(
177                trajectory,
178                config,
179                Some(receiver),
180                launch_readiness,
181                launch_snapshot,
182            );
183        })
184        .expect("failed to launch viewer session thread");
185
186    handle
187}
188
189pub fn run_with_session<F>(trajectory: Trajectory, config: ViewerConfig, driver: F)
190where
191    F: FnOnce(ViewerSessionHandle) + Send + 'static,
192{
193    let (sender, receiver) = mpsc::channel();
194    let handle = ViewerSessionHandle::new(sender);
195    let readiness = handle.readiness().clone();
196    let snapshot = handle.snapshot().clone();
197
198    thread::Builder::new()
199        .name("ak-viewer-session-driver".to_string())
200        .spawn(move || {
201            driver(handle);
202        })
203        .expect("failed to launch viewer session driver thread");
204
205    run_app(trajectory, config, Some(receiver), readiness, snapshot);
206}
207
208pub fn run(trajectory: Trajectory, config: ViewerConfig) {
209    run_app(
210        trajectory,
211        config,
212        None,
213        Arc::new(ViewerReadiness::new()),
214        Arc::new(Mutex::new(crate::viewer::session::ViewerSnapshot::default())),
215    );
216}
217
218pub fn run_default(trajectory: Trajectory) {
219    let config = ViewerConfig::default();
220    run(trajectory, config)
221}
222
223pub fn run_structure(structure: Structure, config: ViewerConfig) {
224    let trajectory = Trajectory::new(vec![structure]);
225    run(trajectory, config)
226}
227
228pub fn run_structure_default(structure: Structure) {
229    let trajectory = Trajectory::new(vec![structure]);
230    run_default(trajectory);
231}