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}