symbios-shape 0.2.0

A derivation engine for CGA Shape Grammars.
Documentation
use serde::{Deserialize, Serialize};

use crate::error::ShapeError;

/// A 3-component double-precision vector. Re-exported from `glam`.
pub use glam::DVec3 as Vec3;

/// A double-precision unit quaternion. Re-exported from `glam`.
pub use glam::DQuat as Quat;

/// An Oriented Bounding Box (OBB) that defines a shape's coordinate frame.
///
/// Every CGA operation transforms a parent `Scope` into one or more child `Scope`s.
/// - `position`: world-space location of the scope's local-space origin —
///   the `(0, 0, 0)` corner of the box, *not* its centre. World corners are
///   obtained as `position + rotation * (u·sx, v·sy, w·sz)` with each
///   coordinate in `[0, 1]` (see [`Scope::world_point`]).
/// - `rotation`: local-to-world orientation (unit quaternion required by
///   [`Scope::validate`]).
/// - `size`: non-negative extents along the local X, Y, Z axes. Zero on a
///   given axis is allowed (footprint scopes have `size.y = 0` until
///   `Extrude` runs; face scopes from `Comp(Faces)` carry `size.z = 0`).
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Scope {
    pub position: Vec3,
    pub rotation: Quat,
    pub size: Vec3,
}

impl Scope {
    /// Creates a unit scope at the origin with identity rotation.
    pub fn unit() -> Self {
        Self {
            position: Vec3::ZERO,
            rotation: Quat::IDENTITY,
            size: Vec3::ONE,
        }
    }

    pub fn new(position: Vec3, rotation: Quat, size: Vec3) -> Self {
        Self {
            position,
            rotation,
            size,
        }
    }

    /// Returns `Ok(())` if all fields are finite, the rotation is a unit quaternion,
    /// and no size component is negative.
    ///
    /// Zero size components are permitted: a root scope may have Y = 0 before
    /// `Extrude` sets its height, and face scopes produced by `Comp(Faces)` carry
    /// Z = 0 (they are 2-D canvases). Negative sizes, however, have no valid
    /// interpretation and are rejected here before they can silently corrupt
    /// downstream operations such as `Split`.
    ///
    /// Returns `Err(InvalidNumericValue)` on any violation.
    pub fn validate(&self) -> Result<(), ShapeError> {
        if !self.position.is_finite() || !self.rotation.is_finite() || !self.size.is_finite() {
            return Err(ShapeError::InvalidNumericValue);
        }
        if !self.rotation.is_normalized() {
            return Err(ShapeError::InvalidNumericValue);
        }
        if self.size.x < 0.0 || self.size.y < 0.0 || self.size.z < 0.0 {
            return Err(ShapeError::InvalidNumericValue);
        }
        Ok(())
    }

    /// Returns the world-space position of the local-space point `(u, v, w)`
    /// where each coordinate is in `[0, 1]` (relative to scope size).
    pub fn world_point(&self, u: f64, v: f64, w: f64) -> Vec3 {
        let local = Vec3::new(u * self.size.x, v * self.size.y, w * self.size.z);
        self.position + self.rotation * local
    }
}