Skip to main content

symbios_shape/
scope.rs

1use serde::{Deserialize, Serialize};
2
3use crate::error::ShapeError;
4
5/// A 3-component double-precision vector. Re-exported from `glam`.
6pub use glam::DVec3 as Vec3;
7
8/// A double-precision unit quaternion. Re-exported from `glam`.
9pub use glam::DQuat as Quat;
10
11/// An Oriented Bounding Box (OBB) that defines a shape's coordinate frame.
12///
13/// Every CGA operation transforms a parent `Scope` into one or more child `Scope`s.
14/// - `position`: world-space origin of the scope's corner (min point in local space).
15/// - `rotation`: local-to-world orientation.
16/// - `size`: extents along the local X, Y, Z axes.
17#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
18pub struct Scope {
19    pub position: Vec3,
20    pub rotation: Quat,
21    pub size: Vec3,
22}
23
24impl Scope {
25    /// Creates a unit scope at the origin with identity rotation.
26    pub fn unit() -> Self {
27        Self {
28            position: Vec3::ZERO,
29            rotation: Quat::IDENTITY,
30            size: Vec3::ONE,
31        }
32    }
33
34    pub fn new(position: Vec3, rotation: Quat, size: Vec3) -> Self {
35        Self {
36            position,
37            rotation,
38            size,
39        }
40    }
41
42    /// Returns `Ok(())` if all fields are finite, the rotation is a unit quaternion,
43    /// and no size component is negative.
44    ///
45    /// Zero size components are permitted: a root scope may have Y = 0 before
46    /// `Extrude` sets its height, and face scopes produced by `Comp(Faces)` carry
47    /// Z = 0 (they are 2-D canvases). Negative sizes, however, have no valid
48    /// interpretation and are rejected here before they can silently corrupt
49    /// downstream operations such as `Split`.
50    ///
51    /// Returns `Err(InvalidNumericValue)` on any violation.
52    pub fn validate(&self) -> Result<(), ShapeError> {
53        if !self.position.is_finite() || !self.rotation.is_finite() || !self.size.is_finite() {
54            return Err(ShapeError::InvalidNumericValue);
55        }
56        if !self.rotation.is_normalized() {
57            return Err(ShapeError::InvalidNumericValue);
58        }
59        if self.size.x < 0.0 || self.size.y < 0.0 || self.size.z < 0.0 {
60            return Err(ShapeError::InvalidNumericValue);
61        }
62        Ok(())
63    }
64
65    /// Returns the world-space position of the local-space point `(u, v, w)`
66    /// where each coordinate is in `[0, 1]` (relative to scope size).
67    pub fn world_point(&self, u: f64, v: f64, w: f64) -> Vec3 {
68        let local = Vec3::new(u * self.size.x, v * self.size.y, w * self.size.z);
69        self.position + self.rotation * local
70    }
71}