bevy_symbios_shape 0.7.0

Bevy integration for Symbios Shape.
//! Snap-plane recordings exposed as a Bevy resource.
//!
//! Upstream `symbios-shape` accumulates [`symbios_shape::SnapPlane`] records on
//! a [`ShapeModel`][symbios_shape::ShapeModel] every time a grammar rule
//! executes a `RegSnap("label")` op. Those planes are the alignment landmarks
//! that downstream rules can reference via `Split(axis, snap="label") { … }`.
//!
//! This module surfaces those records to Bevy users as the [`SnapPlanes`]
//! resource. After every `spawn_shape` call, the resource is overwritten with
//! the planes recorded during that derivation (last-spawn-wins). This is
//! sufficient for the common single-grammar case; consumers that need to keep
//! planes from older derivations should `clone()` the resource on each
//! spawn-completion frame.

use bevy::math::Vec3;
use bevy::prelude::Resource;

/// A single snap-plane recorded during grammar derivation, downcast from the
/// upstream double-precision representation.
///
/// The plane is defined in world space by `point` (any point on the plane) and
/// `normal` (a unit-length plane normal, i.e. the +X / +Y / +Z / −X / −Y / −Z
/// face axis of the scope that emitted the `RegSnap`). `label` groups planes
/// from the same `RegSnap("…")` op so consumers can filter selectively.
#[derive(Debug, Clone, PartialEq)]
pub struct SnapPlane {
    pub point: Vec3,
    pub normal: Vec3,
    pub label: String,
}

impl From<&symbios_shape::SnapPlane> for SnapPlane {
    fn from(p: &symbios_shape::SnapPlane) -> Self {
        Self {
            point: Vec3::new(p.point.x as f32, p.point.y as f32, p.point.z as f32),
            normal: Vec3::new(p.normal.x as f32, p.normal.y as f32, p.normal.z as f32),
            label: p.label.clone(),
        }
    }
}

/// Bevy resource holding the snap planes recorded during the most recent
/// [`SpawnShapeExt::spawn_shape`] call.
///
/// The resource is overwritten on every spawn (last-wins). It is initialised
/// empty by [`BevySymbiosShapePlugin`].
///
/// [`SpawnShapeExt::spawn_shape`]: crate::spawner::SpawnShapeExt::spawn_shape
/// [`BevySymbiosShapePlugin`]: crate::BevySymbiosShapePlugin
#[derive(Resource, Debug, Default, Clone)]
pub struct SnapPlanes(pub Vec<SnapPlane>);

impl SnapPlanes {
    pub fn iter(&self) -> std::slice::Iter<'_, SnapPlane> {
        self.0.iter()
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// Returns an iterator over planes whose `label` exactly matches `label`.
    pub fn by_label<'a>(&'a self, label: &'a str) -> impl Iterator<Item = &'a SnapPlane> {
        self.0.iter().filter(move |p| p.label == label)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use glam::DVec3;

    #[test]
    fn from_upstream_downcasts_f64_to_f32() {
        let upstream = symbios_shape::SnapPlane {
            point: DVec3::new(1.5, 2.5, 3.5),
            normal: DVec3::new(0.0, 1.0, 0.0),
            label: "bays".into(),
        };
        let plane = SnapPlane::from(&upstream);
        assert_eq!(plane.point, Vec3::new(1.5, 2.5, 3.5));
        assert_eq!(plane.normal, Vec3::new(0.0, 1.0, 0.0));
        assert_eq!(plane.label, "bays");
    }

    #[test]
    fn by_label_filters() {
        let planes = SnapPlanes(vec![
            SnapPlane {
                point: Vec3::ZERO,
                normal: Vec3::Y,
                label: "a".into(),
            },
            SnapPlane {
                point: Vec3::ZERO,
                normal: Vec3::X,
                label: "b".into(),
            },
            SnapPlane {
                point: Vec3::ZERO,
                normal: Vec3::Z,
                label: "a".into(),
            },
        ]);
        assert_eq!(planes.by_label("a").count(), 2);
        assert_eq!(planes.by_label("b").count(), 1);
        assert_eq!(planes.by_label("c").count(), 0);
    }
}