Skip to main content

optic_core/
coord.rs

1use std::ops::{Add, Mul, Neg, Sub};
2
3use crate::{componentwise_min, componentwise_max, Components, Size2D};
4
5/// A 2D point in continuous space.
6///
7/// `Coord2D` represents a position. Subtracting two points yields a
8/// [`CoordOffset`] (vector). Adding a vector to a point yields a new point.
9///
10/// ```
11/// use optic_core::*;
12///
13/// let a = Coord2D::from(100.0, 200.0);
14/// let b = Coord2D::from(150.0, 180.0);
15/// let d: CoordOffset = b - a;
16/// let mid = a.lerp(b, 0.5);
17/// ```
18///
19/// Implements [`Components<f64, 2>`].
20#[derive(Copy, Clone, Debug)]
21pub struct Coord2D {
22    pub x: f64,
23    pub y: f64,
24}
25
26impl Components<f64, 2> for Coord2D {
27    fn to_array(self) -> [f64; 2] { [self.x, self.y] }
28    fn from_array(a: [f64; 2]) -> Self { Coord2D { x: a[0], y: a[1] } }
29}
30
31impl From<[f64; 2]> for Coord2D { fn from(a: [f64; 2]) -> Self { Coord2D::from_array(a) } }
32impl From<Coord2D> for [f64; 2] { fn from(c: Coord2D) -> Self { c.to_array() } }
33impl From<(f64, f64)> for Coord2D { fn from(t: (f64, f64)) -> Self { Coord2D { x: t.0, y: t.1 } } }
34
35// Point - Point = Vector
36impl Sub for Coord2D {
37    type Output = CoordOffset;
38    fn sub(self, rhs: Coord2D) -> CoordOffset {
39        CoordOffset { x: self.x - rhs.x, y: self.y - rhs.y }
40    }
41}
42
43// Point + Vector = Point, Point - Vector = Point
44impl Add<CoordOffset> for Coord2D {
45    type Output = Coord2D;
46    fn add(self, rhs: CoordOffset) -> Coord2D {
47        Coord2D { x: self.x + rhs.x, y: self.y + rhs.y }
48    }
49}
50impl Sub<CoordOffset> for Coord2D {
51    type Output = Coord2D;
52    fn sub(self, rhs: CoordOffset) -> Coord2D {
53        Coord2D { x: self.x - rhs.x, y: self.y - rhs.y }
54    }
55}
56
57impl Coord2D {
58    /// The origin point (0, 0).
59    pub fn empty() -> Coord2D {
60        Coord2D { x: 0.0, y: 0.0 }
61    }
62    /// Construct from x, y coordinates.
63    pub fn from(x: f64, y: f64) -> Self {
64        Self { x, y }
65    }
66    /// Construct from a tuple.
67    pub fn from_tup((x, y): (f64, f64)) -> Self {
68        Self { x, y }
69    }
70    /// True if the point lies within the rectangle `(0, 0)` to `(size.w, size.h)`.
71    pub fn is_inside(&self, size: Size2D) -> bool {
72        let [w, h] = size.to_array();
73        self.x >= 0.0 && self.y >= 0.0 && self.x <= w as f64 && self.y <= h as f64
74    }
75    /// Euclidean distance to another point.
76    pub fn distance_to(&self, other: Coord2D) -> f64 {
77        (*self - other).length()
78    }
79    /// Midpoint between two points.
80    pub fn midpoint(&self, other: Coord2D) -> Coord2D {
81        Coord2D { x: (self.x + other.x) * 0.5, y: (self.y + other.y) * 0.5 }
82    }
83    /// Linearly interpolate toward `other` by factor `t` (clamped 0..1).
84    pub fn lerp(&self, other: Coord2D, t: f64) -> Coord2D {
85        *self + (other - *self) * t.clamp(0.0, 1.0)
86    }
87    /// Componentwise minimum.
88    pub fn min(&self, other: Coord2D) -> Coord2D {
89        componentwise_min(*self, other)
90    }
91    /// Componentwise maximum.
92    pub fn max(&self, other: Coord2D) -> Coord2D {
93        componentwise_max(*self, other)
94    }
95}
96
97/// A 2D vector/displacement.
98///
99/// `CoordOffset` represents the difference between two [`Coord2D`] points.
100/// It supports vector arithmetic: addition, subtraction, scalar multiplication,
101/// negation, normalization, and dot products.
102#[derive(Copy, Clone, Debug)]
103pub struct CoordOffset {
104    pub x: f64,
105    pub y: f64,
106}
107
108impl Components<f64, 2> for CoordOffset {
109    fn to_array(self) -> [f64; 2] { [self.x, self.y] }
110    fn from_array(a: [f64; 2]) -> Self { CoordOffset { x: a[0], y: a[1] } }
111}
112
113impl From<[f64; 2]> for CoordOffset { fn from(a: [f64; 2]) -> Self { CoordOffset::from_array(a) } }
114impl From<CoordOffset> for [f64; 2] { fn from(v: CoordOffset) -> Self { v.to_array() } }
115impl From<(f64, f64)> for CoordOffset { fn from(t: (f64, f64)) -> Self { CoordOffset { x: t.0, y: t.1 } } }
116
117impl Add for CoordOffset {
118    type Output = CoordOffset;
119    fn add(self, rhs: CoordOffset) -> CoordOffset {
120        CoordOffset { x: self.x + rhs.x, y: self.y + rhs.y }
121    }
122}
123impl Sub for CoordOffset {
124    type Output = CoordOffset;
125    fn sub(self, rhs: CoordOffset) -> CoordOffset {
126        CoordOffset { x: self.x - rhs.x, y: self.y - rhs.y }
127    }
128}
129impl Mul<f64> for CoordOffset {
130    type Output = CoordOffset;
131    fn mul(self, rhs: f64) -> CoordOffset {
132        CoordOffset { x: self.x * rhs, y: self.y * rhs }
133    }
134}
135impl Neg for CoordOffset {
136    type Output = CoordOffset;
137    fn neg(self) -> CoordOffset {
138        CoordOffset { x: -self.x, y: -self.y }
139    }
140}
141
142impl CoordOffset {
143    /// Zero vector (0, 0).
144    pub fn empty() -> CoordOffset {
145        CoordOffset { x: 0.0, y: 0.0 }
146    }
147    /// Construct from x, y components.
148    pub fn from(x: f64, y: f64) -> Self {
149        Self { x, y }
150    }
151    /// Construct from a tuple.
152    pub fn from_tup((x, y): (f64, f64)) -> Self {
153        Self { x, y }
154    }
155    /// True if both components are exactly zero.
156    pub fn is_zero(&self) -> bool {
157        self.x == 0.0 && self.y == 0.0
158    }
159    /// Euclidean length.
160    pub fn length(&self) -> f64 {
161        (self.x * self.x + self.y * self.y).sqrt()
162    }
163    /// Squared length (avoids the sqrt).
164    pub fn length_squared(&self) -> f64 {
165        self.x * self.x + self.y * self.y
166    }
167    /// Unit vector in the same direction. Returns zero if length is near-zero.
168    pub fn normalize(&self) -> CoordOffset {
169        let len = self.length();
170        if len < f64::EPSILON { return CoordOffset { x: 0.0, y: 0.0 }; }
171        CoordOffset { x: self.x / len, y: self.y / len }
172    }
173    /// Dot product with another vector.
174    pub fn dot(&self, other: CoordOffset) -> f64 {
175        self.x * other.x + self.y * other.y
176    }
177    /// Linearly interpolate toward `other` by factor `t` (clamped 0..1).
178    pub fn lerp(&self, other: CoordOffset, t: f64) -> CoordOffset {
179        *self + (other - *self) * t.clamp(0.0, 1.0)
180    }
181    /// Componentwise minimum.
182    pub fn min(&self, other: CoordOffset) -> CoordOffset {
183        componentwise_min(*self, other)
184    }
185    /// Componentwise maximum.
186    pub fn max(&self, other: CoordOffset) -> CoordOffset {
187        componentwise_max(*self, other)
188    }
189}