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 location of the scope's local-space origin —
15///   the `(0, 0, 0)` corner of the box, *not* its centre. World corners are
16///   obtained as `position + rotation * (u·sx, v·sy, w·sz)` with each
17///   coordinate in `[0, 1]` (see [`Scope::world_point`]).
18/// - `rotation`: local-to-world orientation (unit quaternion required by
19///   [`Scope::validate`]).
20/// - `size`: non-negative extents along the local X, Y, Z axes. Zero on a
21///   given axis is allowed (footprint scopes have `size.y = 0` until
22///   `Extrude` runs; face scopes from `Comp(Faces)` carry `size.z = 0`).
23#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
24pub struct Scope {
25    pub position: Vec3,
26    pub rotation: Quat,
27    pub size: Vec3,
28}
29
30impl Scope {
31    /// Creates a unit scope at the origin with identity rotation.
32    pub fn unit() -> Self {
33        Self {
34            position: Vec3::ZERO,
35            rotation: Quat::IDENTITY,
36            size: Vec3::ONE,
37        }
38    }
39
40    pub fn new(position: Vec3, rotation: Quat, size: Vec3) -> Self {
41        Self {
42            position,
43            rotation,
44            size,
45        }
46    }
47
48    /// Returns `Ok(())` if all fields are finite, the rotation is a unit quaternion,
49    /// and no size component is negative.
50    ///
51    /// Zero size components are permitted: a root scope may have Y = 0 before
52    /// `Extrude` sets its height, and face scopes produced by `Comp(Faces)` carry
53    /// Z = 0 (they are 2-D canvases). Negative sizes, however, have no valid
54    /// interpretation and are rejected here before they can silently corrupt
55    /// downstream operations such as `Split`.
56    ///
57    /// Returns `Err(InvalidNumericValue)` on any violation.
58    pub fn validate(&self) -> Result<(), ShapeError> {
59        if !self.position.is_finite() || !self.rotation.is_finite() || !self.size.is_finite() {
60            return Err(ShapeError::InvalidNumericValue);
61        }
62        if !self.rotation.is_normalized() {
63            return Err(ShapeError::InvalidNumericValue);
64        }
65        if self.size.x < 0.0 || self.size.y < 0.0 || self.size.z < 0.0 {
66            return Err(ShapeError::InvalidNumericValue);
67        }
68        Ok(())
69    }
70
71    /// Returns the world-space position of the local-space point `(u, v, w)`
72    /// where each coordinate is in `[0, 1]` (relative to scope size).
73    pub fn world_point(&self, u: f64, v: f64, w: f64) -> Vec3 {
74        let local = Vec3::new(u * self.size.x, v * self.size.y, w * self.size.z);
75        self.position + self.rotation * local
76    }
77}