Skip to main content

ranim/app/
mod.rs

1//! Project configuration and scene registration for Ranim.
2//!
3//! A project binary is a [`bevy_app::App`]: plugins register scenes and
4//! resources, then [`RanimAppExt::build`] finalizes plugin construction into a
5//! [`RanimApp`] that preview/render/inspect sessions consume.
6
7use std::ops::{Deref, DerefMut};
8
9use bevy_app::{App, PluginsState};
10use bevy_ecs::prelude::*;
11
12mod scene;
13pub use scene::{Output, OutputFormat, Scene, SceneConfig, SceneConstructor};
14
15/// Resolve the project's rendering configuration for every session host.
16#[cfg(any(
17    feature = "cmd-render",
18    all(target_family = "wasm", feature = "render")
19))]
20pub(crate) fn render_setup_of(app: &App) -> ranim_render::RenderSetup {
21    app.world()
22        .get_resource::<ranim_render::RenderSetup>()
23        .cloned()
24        .unwrap_or_else(|| {
25            ranim_render::RenderSetup::new(|_render_app| {
26                #[cfg(feature = "items")]
27                _render_app.add_plugins(ranim_items::RanimItemsPlugin);
28            })
29        })
30}
31
32/// The scenes registered in a project.
33#[derive(Resource, Default)]
34pub struct Scenes(Vec<Scene>);
35
36/// A project with plugin construction finalized.
37///
38/// Produced by [`RanimAppExt::build`], which runs bevy's plugin `finish` and
39/// `cleanup` exactly once. CLI and canvas/interactive preview hosts consume
40/// this project and may install render sub-apps into it. Offline render workers
41/// instead receive a selected [`Scene`] and its rendering configuration.
42/// This wrapper is no longer a plugin-construction target.
43pub struct RanimApp(App);
44
45impl RanimApp {
46    /// Consume the wrapper, returning the inner bevy app.
47    pub fn into_inner(self) -> App {
48        self.0
49    }
50}
51
52impl Deref for RanimApp {
53    type Target = App;
54
55    fn deref(&self) -> &Self::Target {
56        &self.0
57    }
58}
59
60impl DerefMut for RanimApp {
61    fn deref_mut(&mut self) -> &mut Self::Target {
62        &mut self.0
63    }
64}
65
66/// Finalize a project into a [`RanimApp`].
67pub trait RanimAppExt {
68    /// Run plugin `finish` and `cleanup`, then hand the project to sessions.
69    ///
70    /// # Panics
71    ///
72    /// Panics if plugins are still being added (asynchronous plugin building
73    /// has not completed).
74    fn build(&mut self) -> RanimApp;
75}
76
77impl RanimAppExt for App {
78    fn build(&mut self) -> RanimApp {
79        assert_eq!(
80            self.plugins_state(),
81            PluginsState::Ready,
82            "plugins must be ready before building a RanimApp"
83        );
84        self.finish();
85        self.cleanup();
86        RanimApp(std::mem::take(self))
87    }
88}
89
90/// Scene registration and selection for a project [`App`].
91pub trait AppExt {
92    /// Register a scene without running its constructor.
93    ///
94    /// # Panics
95    ///
96    /// Panics if a scene with the same name is already registered.
97    fn add_scene(&mut self, scene: Scene) -> &mut Self;
98
99    /// Register scenes in iteration order.
100    ///
101    /// # Panics
102    ///
103    /// Panics if any scene name is already registered, including duplicate
104    /// names within this collection.
105    fn add_scenes(&mut self, scenes: impl IntoIterator<Item = Scene>) -> &mut Self {
106        for scene in scenes {
107            self.add_scene(scene);
108        }
109        self
110    }
111
112    /// Return the scene descriptions in registration order.
113    fn scenes(&self) -> &[Scene];
114
115    /// Select a scene by name, cloning its descriptor.
116    fn scene(&self, name: &str) -> Option<Scene>;
117
118    /// Select the first registered scene, if the project has any scenes.
119    fn first_scene(&self) -> Option<Scene> {
120        self.scenes().first().cloned()
121    }
122}
123
124impl AppExt for bevy_app::App {
125    fn add_scene(&mut self, scene: Scene) -> &mut Self {
126        let mut scenes = self.world_mut().get_resource_or_init::<Scenes>();
127        assert!(
128            !scenes.0.iter().any(|existing| existing.name == scene.name),
129            "scene `{}` is already registered",
130            scene.name,
131        );
132        scenes.0.push(scene);
133        self
134    }
135
136    fn scenes(&self) -> &[Scene] {
137        self.world()
138            .get_resource::<Scenes>()
139            .map(|scenes| scenes.0.as_slice())
140            .unwrap_or(&[])
141    }
142
143    fn scene(&self, name: &str) -> Option<Scene> {
144        self.scenes()
145            .iter()
146            .find(|scene| scene.name == name)
147            .cloned()
148    }
149}
150
151#[cfg(test)]
152mod tests;