Skip to main content

optic_core/
geometry.rs

1use std::ops::{Add, Mul, Sub};
2
3/// Trait for types whose components can be accessed as a fixed-size array.
4///
5/// This is the geometric analogue of [`ChannelArray`] in `optic-color`. It
6/// enables generic componentwise operations (`min`, `max`) across different
7/// geometric types.
8///
9/// Implemented for [`Size2D`], [`Size3D`], [`Coord2D`], [`CoordOffset`].
10///
11/// [`ChannelArray`]: optic_color::ChannelArray
12pub trait Components<T, const N: usize>: Copy {
13    /// Convert to an array of length N.
14    fn to_array(self) -> [T; N];
15    /// Construct from an array of length N.
16    fn from_array(a: [T; N]) -> Self;
17}
18
19/// Componentwise minimum of two values.
20///
21/// Each component of the result is the minimum of the corresponding
22/// components of `a` and `b`.
23pub fn componentwise_min<T: PartialOrd + Copy, C: Components<T, N>, const N: usize>(a: C, b: C) -> C {
24    let (a, b) = (a.to_array(), b.to_array());
25    let mut out = a;
26    for i in 0..N {
27        if b[i] < out[i] {
28            out[i] = b[i];
29        }
30    }
31    C::from_array(out)
32}
33
34/// Componentwise maximum of two values.
35///
36/// Each component of the result is the maximum of the corresponding
37/// components of `a` and `b`.
38pub fn componentwise_max<T: PartialOrd + Copy, C: Components<T, N>, const N: usize>(a: C, b: C) -> C {
39    let (a, b) = (a.to_array(), b.to_array());
40    let mut out = a;
41    for i in 0..N {
42        if b[i] > out[i] {
43            out[i] = b[i];
44        }
45    }
46    C::from_array(out)
47}
48
49/// A 2D size with non-negative integer dimensions.
50///
51/// ```
52/// use optic_core::*;
53///
54/// let s = Size2D::from(1920, 1080);
55/// assert_eq!(s.aspect_ratio(), 16.0 / 9.0);
56/// ```
57///
58/// Supports [`Add`], [`Sub`] (saturating), and [`Mul<f32>`] componentwise.
59/// Conversion from/to `[u32; 2]` and `(u32, u32)` via [`Components`].
60#[derive(Copy, Clone, Debug, PartialEq)]
61pub struct Size2D {
62    pub w: u32,
63    pub h: u32,
64}
65
66impl Components<u32, 2> for Size2D {
67    fn to_array(self) -> [u32; 2] { [self.w, self.h] }
68    fn from_array(a: [u32; 2]) -> Self { Size2D { w: a[0], h: a[1] } }
69}
70
71impl From<[u32; 2]> for Size2D { fn from(a: [u32; 2]) -> Self { Size2D::from_array(a) } }
72impl From<Size2D> for [u32; 2] { fn from(s: Size2D) -> Self { s.to_array() } }
73impl From<(u32, u32)> for Size2D { fn from(t: (u32, u32)) -> Self { Size2D { w: t.0, h: t.1 } } }
74
75macro_rules! impl_size_ops {
76    ($ty:ty, $n:literal) => {
77        impl Add for $ty {
78            type Output = Self;
79            fn add(self, rhs: Self) -> Self {
80                let (a, b) = (self.to_array(), rhs.to_array());
81                let mut out = [0u32; $n];
82                for i in 0..$n { out[i] = a[i].saturating_add(b[i]); }
83                Self::from_array(out)
84            }
85        }
86        impl Sub for $ty {
87            type Output = Self;
88            fn sub(self, rhs: Self) -> Self {
89                let (a, b) = (self.to_array(), rhs.to_array());
90                let mut out = [0u32; $n];
91                for i in 0..$n { out[i] = a[i].saturating_sub(b[i]); }
92                Self::from_array(out)
93            }
94        }
95        impl Mul<f32> for $ty {
96            type Output = Self;
97            fn mul(self, rhs: f32) -> Self {
98                let a = self.to_array();
99                let mut out = [0u32; $n];
100                for i in 0..$n { out[i] = ((a[i] as f32) * rhs).round().max(0.0) as u32; }
101                Self::from_array(out)
102            }
103        }
104    };
105}
106
107impl_size_ops!(Size2D, 2);
108
109impl Size2D {
110    /// Zero-size (0, 0).
111    pub fn empty() -> Size2D {
112        Self { w: 0, h: 0 }
113    }
114    /// Construct from explicit width and height.
115    pub fn from(w: u32, h: u32) -> Self {
116        Self { w, h }
117    }
118    /// Reduce both dimensions by `n` (saturating).
119    pub fn shave(&self, n: u32) -> Size2D {
120        Size2D {
121            w: self.w.saturating_sub(n),
122            h: self.h.saturating_sub(n),
123        }
124    }
125    /// Aspect ratio as `w / h` (f32). Returns 0 if height is 0.
126    pub fn aspect_ratio(&self) -> f32 {
127        self.w as f32 / self.h.max(1) as f32
128    }
129    /// True if either dimension is zero.
130    pub fn is_empty(&self) -> bool {
131        self.w == 0 || self.h == 0
132    }
133    /// Area in pixels (`w * h` as u64).
134    pub fn area(&self) -> u64 {
135        self.w as u64 * self.h as u64
136    }
137    /// Componentwise minimum.
138    pub fn min(&self, other: Size2D) -> Size2D {
139        componentwise_min(*self, other)
140    }
141    /// Componentwise maximum.
142    pub fn max(&self, other: Size2D) -> Size2D {
143        componentwise_max(*self, other)
144    }
145    /// Scale down to fit within `max` while preserving aspect ratio.
146    ///
147    /// If already within bounds, returns unchanged.
148    pub fn fit_within(&self, max: Size2D) -> Size2D {
149        if self.w <= max.w && self.h <= max.h { return *self; }
150        let scale = (max.w as f32 / self.w as f32).min(max.h as f32 / self.h as f32);
151        *self * scale
152    }
153    /// Scale to a specific width, preserving aspect ratio.
154    pub fn scaled_to_width(&self, w: u32) -> Size2D {
155        let scale = w as f32 / self.w.max(1) as f32;
156        *self * scale
157    }
158    /// Scale to a specific height, preserving aspect ratio.
159    pub fn scaled_to_height(&self, h: u32) -> Size2D {
160        let scale = h as f32 / self.h.max(1) as f32;
161        *self * scale
162    }
163    /// Convert to a [`Size3D`] with the given depth.
164    pub fn to_size3d(&self, depth: u32) -> Size3D {
165        Size3D { w: self.w, h: self.h, d: depth }
166    }
167}
168
169/// A 3D size with non-negative integer dimensions.
170#[derive(Copy, Clone, Debug, PartialEq)]
171pub struct Size3D {
172    pub w: u32,
173    pub h: u32,
174    pub d: u32,
175}
176
177impl Components<u32, 3> for Size3D {
178    fn to_array(self) -> [u32; 3] { [self.w, self.h, self.d] }
179    fn from_array(a: [u32; 3]) -> Self { Size3D { w: a[0], h: a[1], d: a[2] } }
180}
181
182impl From<[u32; 3]> for Size3D { fn from(a: [u32; 3]) -> Self { Size3D::from_array(a) } }
183impl From<Size3D> for [u32; 3] { fn from(s: Size3D) -> Self { s.to_array() } }
184impl From<(u32, u32, u32)> for Size3D { fn from(t: (u32, u32, u32)) -> Self { Size3D { w: t.0, h: t.1, d: t.2 } } }
185
186impl_size_ops!(Size3D, 3);
187
188impl Size3D {
189    /// Zero-size (0, 0, 0).
190    pub fn empty() -> Size3D {
191        Self { w: 0, h: 0, d: 0 }
192    }
193    /// Construct from width, height, and depth.
194    pub fn from(w: u32, h: u32, d: u32) -> Self {
195        Self { w, h, d }
196    }
197    /// Reduce all three dimensions by `n` (saturating).
198    pub fn shave(&self, n: u32) -> Size3D {
199        Size3D {
200            w: self.w.saturating_sub(n),
201            h: self.h.saturating_sub(n),
202            d: self.d.saturating_sub(n),
203        }
204    }
205    /// True if any dimension is zero.
206    pub fn is_empty(&self) -> bool {
207        self.w == 0 || self.h == 0 || self.d == 0
208    }
209    /// Volume as u64 (`w * h * d`).
210    pub fn volume(&self) -> u64 {
211        self.w as u64 * self.h as u64 * self.d as u64
212    }
213    /// Componentwise minimum.
214    pub fn min(&self, other: Size3D) -> Size3D {
215        componentwise_min(*self, other)
216    }
217    /// Componentwise maximum.
218    pub fn max(&self, other: Size3D) -> Size3D {
219        componentwise_max(*self, other)
220    }
221    /// Drop the depth component, returning a [`Size2D`].
222    pub fn to_size2d(&self) -> Size2D {
223        Size2D { w: self.w, h: self.h }
224    }
225}
226
227/// Near/far clip plane distances for a camera.
228#[derive(Clone, Copy, Debug)]
229pub struct ClipDist {
230    pub near: f32,
231    pub far: f32,
232}
233
234impl Default for ClipDist {
235    fn default() -> Self {
236        ClipDist::from(0.01, 1000.0)
237    }
238}
239
240impl ClipDist {
241    pub fn from(near: f32, far: f32) -> ClipDist {
242        ClipDist { near, far }
243    }
244}
245
246/// Camera projection mode.
247#[derive(Clone, Copy, Debug, PartialEq)]
248pub enum CamProj {
249    Ortho,
250    Persp,
251}