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}