bevy_symbios_shape 0.5.0

Bevy integration for Symbios Shape.
Documentation
//! Event-driven spawning surface for `bevy_symbios_shape`.
//!
//! [`SpawnShapeRequest`] is the Bevy-native counterpart to
//! [`SpawnShapeExt::spawn_shape`]. Triggering one queues a derivation that
//! the crate's built-in observer handles, then emits [`SpawnShapeSpawned`]
//! (success) or [`SpawnShapeFailed`] (parse / derivation error). Use this
//! when you want decoupling between the caller and the spawning logic —
//! e.g. UI code that issues requests without holding all the asset
//! registries.
//!
//! The imperative [`SpawnShapeExt`] API is still available for one-shot
//! use cases.
//!
//! [`SpawnShapeExt`]: crate::spawner::SpawnShapeExt
//! [`SpawnShapeExt::spawn_shape`]: crate::spawner::SpawnShapeExt::spawn_shape

use std::sync::Arc;

use bevy::prelude::*;
use symbios_shape::{Interpreter, Scope};

use crate::cache::ShapeMeshCache;
use crate::registry::ShapeRegistry;
use crate::spawner::SpawnShapeExt;

/// System-set tag for systems that emit shape-spawn work.
///
/// Add this to systems that call [`SpawnShapeExt::spawn_shape`] directly or
/// trigger [`SpawnShapeRequest`]. Downstream systems that depend on the
/// resulting entities can chain via `.after(SpawnShapeSystems)` so spawn
/// commands flush before they read the world.
///
/// The crate's own observer handles requests off-schedule (it runs whenever
/// commands flush after a trigger) so this set is a *labelling convenience*
/// rather than a wiring requirement.
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
pub struct SpawnShapeSystems;

/// Event requesting derivation and spawning of a CGA shape grammar.
///
/// Trigger via `commands.trigger(SpawnShapeRequest { … })` or
/// `world.trigger(SpawnShapeRequest { … })`. The crate's built-in observer
/// derives the grammar and spawns the resulting terminal hierarchy, then
/// triggers [`SpawnShapeSpawned`] (or [`SpawnShapeFailed`] on error).
///
/// `interpreter` is wrapped in an [`Arc`] so the same grammar can be reused
/// for many spawns without cloning the rule table on every request.
#[derive(Event, Clone)]
pub struct SpawnShapeRequest {
    pub interpreter: Arc<Interpreter>,
    pub root_scope: Scope,
    pub root_rule: String,
}

/// Emitted by the crate's observer after a successful
/// [`SpawnShapeRequest`]-driven spawn.
///
/// Field 0 is the entity returned by [`SpawnShapeExt::spawn_shape`] — the
/// root of the spawned terminal hierarchy.
#[derive(Event, Clone, Copy, Debug)]
pub struct SpawnShapeSpawned(pub Entity);

/// Emitted by the crate's observer when [`SpawnShapeRequest`]-driven
/// derivation fails.
///
/// The original [`symbios_shape::ShapeError`] isn't `Clone`, so we expose its
/// `Display` form. Callers that need the typed variant should run
/// [`Interpreter::derive`][symbios_shape::Interpreter::derive] themselves and
/// match on the result.
#[derive(Event, Clone, Debug)]
pub struct SpawnShapeFailed(pub String);

/// Observer system that handles [`SpawnShapeRequest`] events.
///
/// Registered automatically by [`BevySymbiosShapePlugin`]. End users normally
/// don't call this directly.
///
/// [`BevySymbiosShapePlugin`]: crate::BevySymbiosShapePlugin
pub fn handle_spawn_request(
    request: On<SpawnShapeRequest>,
    mut commands: Commands,
    registry: Res<ShapeRegistry>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut cache: ResMut<ShapeMeshCache>,
) {
    let req = request.event();
    match commands.spawn_shape(
        &req.interpreter,
        req.root_scope,
        &req.root_rule,
        &registry,
        &mut meshes,
        &mut materials,
        &mut cache,
    ) {
        Ok(entity) => {
            commands.trigger(SpawnShapeSpawned(entity));
        }
        Err(err) => {
            error!("spawn_shape failed: {err}");
            commands.trigger(SpawnShapeFailed(err.to_string()));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::BevySymbiosShapePlugin;
    use crate::query::LastDerivedModel;
    use std::sync::{Arc, Mutex};
    use symbios_shape::grammar::parse_ops;
    use symbios_shape::{Quat as DQuat, Vec3 as DVec3};

    /// Builds an App with the plugin and the asset registries it depends on.
    fn make_app() -> App {
        let mut app = App::new();
        app.add_plugins(bevy::asset::AssetPlugin::default())
            .init_resource::<Assets<Mesh>>()
            .init_resource::<Assets<StandardMaterial>>()
            .init_resource::<Assets<Scene>>()
            .add_plugins(BevySymbiosShapePlugin);
        app
    }

    fn simple_interp() -> Interpreter {
        let mut interp = Interpreter::new();
        interp.add_rule("Lot", parse_ops(r#"Extrude(2) I("Wall")"#).unwrap());
        interp
    }

    #[test]
    fn observer_spawns_entity_on_request() {
        let mut app = make_app();
        let captured: Arc<Mutex<Option<Entity>>> = Arc::new(Mutex::new(None));
        let captured_obs = captured.clone();
        app.add_observer(move |spawned: On<SpawnShapeSpawned>| {
            *captured_obs.lock().unwrap() = Some(spawned.event().0);
        });

        app.world_mut().trigger(SpawnShapeRequest {
            interpreter: Arc::new(simple_interp()),
            root_scope: Scope::new(DVec3::ZERO, DQuat::IDENTITY, DVec3::new(2.0, 0.0, 2.0)),
            root_rule: "Lot".into(),
        });
        // One update flushes the observer-issued commands (spawn + trigger).
        app.update();

        let entity = captured.lock().unwrap();
        assert!(entity.is_some(), "expected SpawnShapeSpawned to fire");

        // The full model should also be available via the resource.
        let model = app.world().resource::<LastDerivedModel>();
        assert!(
            !model.model().terminals.is_empty(),
            "LastDerivedModel should be populated"
        );
    }

    #[test]
    fn observer_emits_failed_on_invalid_scope() {
        // Unknown root rules are silently treated as implicit terminals upstream,
        // so to force a derive() failure we use a NaN scope size (caught by
        // Scope::validate → ShapeError::InvalidNumericValue).
        let mut app = make_app();
        let captured: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
        let captured_obs = captured.clone();
        app.add_observer(move |failed: On<SpawnShapeFailed>| {
            *captured_obs.lock().unwrap() = Some(failed.event().0.clone());
        });

        app.world_mut().trigger(SpawnShapeRequest {
            interpreter: Arc::new(simple_interp()),
            root_scope: Scope::new(DVec3::ZERO, DQuat::IDENTITY, DVec3::new(f64::NAN, 0.0, 1.0)),
            root_rule: "Lot".into(),
        });
        app.update();

        let msg = captured.lock().unwrap();
        assert!(msg.is_some(), "expected SpawnShapeFailed to fire");
    }
}