Skip to main content

motion_core/geometry/
bounds.rs

1//! Double-precision authoring dimensions, bounds and anchors.
2
3use super::Point;
4
5/// Double-precision dimensions without implicit clamping or pixel conversion.
6///
7/// Raw values, including negative animation overshoot, are preserved. Consumers
8/// decide which dimensions are valid for their operation.
9///
10/// ```
11/// use motion_core::prelude::*;
12/// use motion_core::{
13///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
14///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
15/// };
16/// let size = Size::new(40.0, 30.0);
17/// assert_eq!(size.to_rect(), Rect::new(0.0, 0.0, 40.0, 30.0));
18/// assert_eq!(size.width, 40.0_f64);
19/// ```
20#[derive(Clone, Copy, Debug, Default, PartialEq)]
21pub struct Size {
22    /// Horizontal extent in design coordinates.
23    pub width: f64,
24    /// Vertical extent in design coordinates.
25    pub height: f64,
26}
27
28impl Size {
29    /// Preserves the supplied dimensions without normalization.
30    pub const fn new(width: f64, height: f64) -> Self {
31        Self { width, height }
32    }
33
34    /// Returns bounds starting at the origin with these dimensions.
35    pub fn to_rect(self) -> Rect {
36        Rect::from_size(self)
37    }
38}
39
40/// A local-space anchor used by authoring placement and transforms.
41///
42/// Relative coordinates are not clamped; explicit points need not lie inside
43/// bounds. Baseline falls back to BottomLeft without text baseline metrics.
44#[derive(Clone, Copy, Debug, Default, PartialEq)]
45pub enum Pivot {
46    /// Upper-left corner.
47    TopLeft,
48    /// Midpoint of the upper edge.
49    Top,
50    /// Upper-right corner.
51    TopRight,
52    /// Midpoint of the left edge.
53    Left,
54    /// Bounds midpoint; the default authoring pivot.
55    #[default]
56    Center,
57    /// Midpoint of the right edge.
58    Right,
59    /// Lower-left corner.
60    BottomLeft,
61    /// Midpoint of the lower edge.
62    Bottom,
63    /// Lower-right corner.
64    BottomRight,
65    /// Text baseline origin, or BottomLeft without baseline metrics.
66    Baseline,
67    /// Fractions of the horizontal and vertical bounds, without clamping.
68    Relative(f64, f64),
69    /// An explicit point in local coordinates.
70    Point(Point),
71}
72
73/// Double-precision bounds specified by opposite corners, not width and height.
74///
75/// Coordinates are preserved, not sorted or clamped. Inverted or zero-area bounds
76/// are empty. Positive inset shrinks; excessive inset produces inverted bounds.
77/// Containment is half-open: left/top are included and right/bottom are excluded.
78///
79/// ```
80/// use motion_core::prelude::*;
81/// use motion_core::{
82///     DisplayList, RasterLimits, ResourcePool, ValidatedDisplayList, MAX_PATH_VERBS,
83///     MAX_STROKE_VERBS, MAX_SVG_PATH_BYTES, sample_offsets,
84/// };
85/// let bounds = Rect::new(10.0, 20.0, 40.0, 30.0);
86/// assert_eq!(bounds.size(), Size::new(30.0, 10.0));
87/// assert_eq!(bounds.center(), Point::new(25.0, 25.0));
88/// assert!(bounds.contains(Point::new(10.0, 20.0)));
89/// assert!(!bounds.contains(Point::new(40.0, 30.0)));
90/// ```
91#[derive(Clone, Copy, Debug, Default, PartialEq)]
92pub struct Rect {
93    /// Horizontal coordinate of the first corner.
94    pub left: f64,
95    /// Vertical coordinate of the first corner.
96    pub top: f64,
97    /// Horizontal coordinate of the opposite corner.
98    pub right: f64,
99    /// Vertical coordinate of the opposite corner.
100    pub bottom: f64,
101}
102
103impl Rect {
104    /// Preserves two corners without sorting, clamping or changing their meaning.
105    pub const fn new(left: f64, top: f64, right: f64, bottom: f64) -> Self {
106        Self {
107            left,
108            top,
109            right,
110            bottom,
111        }
112    }
113
114    /// Creates bounds from a top-left position and signed dimensions.
115    pub fn from_origin_size(origin: Point, size: Size) -> Self {
116        Self::new(
117            origin.x,
118            origin.y,
119            origin.x + size.width,
120            origin.y + size.height,
121        )
122    }
123
124    /// Creates bounds around a center with signed dimensions.
125    pub fn from_center_size(center: Point, size: Size) -> Self {
126        Self::new(
127            center.x - size.width * 0.5,
128            center.y - size.height * 0.5,
129            center.x + size.width * 0.5,
130            center.y + size.height * 0.5,
131        )
132    }
133
134    /// Creates bounds at the coordinate origin with these dimensions.
135    pub fn from_size(size: Size) -> Self {
136        Self::from_origin_size(Point::ORIGIN, size)
137    }
138
139    /// Returns the signed horizontal span; subtraction can overflow for extremes.
140    pub fn width(self) -> f64 {
141        self.right - self.left
142    }
143
144    /// Returns the signed vertical span; subtraction can overflow for extremes.
145    pub fn height(self) -> f64 {
146        self.bottom - self.top
147    }
148
149    /// Returns signed dimensions without normalization.
150    pub fn size(self) -> Size {
151        Size::new(self.width(), self.height())
152    }
153
154    /// Returns the midpoint without adding two unscaled large coordinates.
155    pub fn center(self) -> Point {
156        Point::new(
157            self.left.midpoint(self.right),
158            self.top.midpoint(self.bottom),
159        )
160    }
161
162    /// Resolves an anchor without normalizing or clamping the stored bounds.
163    ///
164    /// Baseline uses the non-text BottomLeft fallback. Relative coordinates can
165    /// extrapolate; invalid or unrepresentable arithmetic remains observable.
166    pub fn anchor(self, pivot: Pivot) -> Point {
167        fn relative(start: f64, end: f64, fraction: f64) -> f64 {
168            match fraction {
169                0.0 => start,
170                1.0 => end,
171                0.5 => start.midpoint(end),
172                _ if start.is_sign_negative() != end.is_sign_negative() => {
173                    let middle = start.midpoint(end);
174                    if fraction < 0.5 {
175                        start + (middle - start) * (fraction * 2.0)
176                    } else {
177                        end + (middle - end) * ((1.0 - fraction) * 2.0)
178                    }
179                }
180                _ => start + (end - start) * fraction,
181            }
182        }
183        let center = self.center();
184        match pivot {
185            Pivot::TopLeft => Point::new(self.left, self.top),
186            Pivot::Top => Point::new(center.x, self.top),
187            Pivot::TopRight => Point::new(self.right, self.top),
188            Pivot::Left => Point::new(self.left, center.y),
189            Pivot::Center => center,
190            Pivot::Right => Point::new(self.right, center.y),
191            Pivot::BottomLeft | Pivot::Baseline => Point::new(self.left, self.bottom),
192            Pivot::Bottom => Point::new(center.x, self.bottom),
193            Pivot::BottomRight => Point::new(self.right, self.bottom),
194            Pivot::Relative(horizontal, vertical) => Point::new(
195                relative(self.left, self.right, horizontal),
196                relative(self.top, self.bottom, vertical),
197            ),
198            Pivot::Point(point) => point,
199        }
200    }
201
202    /// Whether all four stored coordinates are finite; spans may still overflow.
203    pub fn is_finite(self) -> bool {
204        [self.left, self.top, self.right, self.bottom]
205            .iter()
206            .all(|value| value.is_finite())
207    }
208
209    /// Whether a finite rectangle has no positive area, including inverted bounds.
210    /// Nonfinite coordinates are invalid, not a usable empty rectangle.
211    pub fn is_empty(self) -> bool {
212        self.is_finite() && (self.right <= self.left || self.bottom <= self.top)
213    }
214
215    /// Tests a finite point with half-open edge ownership. Invalid bounds reject it.
216    pub fn contains(self, point: Point) -> bool {
217        self.is_finite()
218            && point.x.is_finite()
219            && point.y.is_finite()
220            && point.x >= self.left
221            && point.x < self.right
222            && point.y >= self.top
223            && point.y < self.bottom
224    }
225
226    /// Returns enclosing bounds, ignoring empty operands. If both are empty,
227    /// returns the second operand. A nonfinite operand is propagated unchanged.
228    pub fn union(self, other: Self) -> Self {
229        if !self.is_finite() {
230            return self;
231        }
232        if !other.is_finite() {
233            return other;
234        }
235        if self.is_empty() {
236            return other;
237        }
238        if other.is_empty() {
239            return self;
240        }
241        Self::new(
242            self.left.min(other.left),
243            self.top.min(other.top),
244            self.right.max(other.right),
245            self.bottom.max(other.bottom),
246        )
247    }
248
249    /// Returns overlapping bounds, clamping each disjoint span to zero at the
250    /// greater origin. A nonfinite operand is propagated unchanged.
251    pub fn intersect(self, other: Self) -> Self {
252        if !self.is_finite() {
253            return self;
254        }
255        if !other.is_finite() {
256            return other;
257        }
258        let left = self.left.max(other.left);
259        let top = self.top.max(other.top);
260        Self::new(
261            left,
262            top,
263            self.right.min(other.right).max(left),
264            self.bottom.min(other.bottom).max(top),
265        )
266    }
267
268    /// Moves every edge inward by a signed amount; negative values expand.
269    /// Excessive positive amounts preserve inverted, empty bounds.
270    pub fn inset(self, amount: f64) -> Self {
271        Self::new(
272            self.left + amount,
273            self.top + amount,
274            self.right - amount,
275            self.bottom - amount,
276        )
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    #[test]
283    fn pivot_anchors_cover_corners_edges_center_and_nontext_baseline() {
284        use super::{Pivot, Rect};
285        use crate::Point;
286        let bounds = Rect::new(10.0, 20.0, 50.0, 80.0);
287        for (pivot, expected) in [
288            (Pivot::TopLeft, (10.0, 20.0)),
289            (Pivot::Top, (30.0, 20.0)),
290            (Pivot::TopRight, (50.0, 20.0)),
291            (Pivot::Left, (10.0, 50.0)),
292            (Pivot::Center, (30.0, 50.0)),
293            (Pivot::Right, (50.0, 50.0)),
294            (Pivot::BottomLeft, (10.0, 80.0)),
295            (Pivot::Bottom, (30.0, 80.0)),
296            (Pivot::BottomRight, (50.0, 80.0)),
297            (Pivot::Baseline, (10.0, 80.0)),
298        ] {
299            assert_eq!(bounds.anchor(pivot), Point::new(expected.0, expected.1));
300        }
301        assert_eq!(Pivot::default(), Pivot::Center);
302        assert_eq!(
303            bounds.anchor(Pivot::Relative(-0.25, 1.5)),
304            Point::new(0.0, 110.0)
305        );
306        assert_eq!(
307            bounds.anchor(Pivot::Point(Point::new(-8.0, 5.0))),
308            Point::new(-8.0, 5.0)
309        );
310    }
311
312    #[test]
313    fn pivot_relative_positions_preserve_extreme_midpoints_and_subnormals() {
314        use super::{Pivot, Rect};
315        use crate::Point;
316        let wide = Rect::new(-f64::MAX, 0.0, f64::MAX, f64::from_bits(1));
317        assert_eq!(
318            wide.anchor(Pivot::Relative(0.5, 1.0)),
319            Point::new(0.0, f64::from_bits(1))
320        );
321        assert_eq!(wide.anchor(Pivot::Relative(0.25, 0.0)).x, -f64::MAX * 0.5);
322        let tiny = f64::from_bits(1);
323        let equal = Rect::new(tiny, tiny, tiny, tiny);
324        assert_eq!(
325            equal.anchor(Pivot::Relative(0.5, 0.5)),
326            Point::new(tiny, tiny)
327        );
328        let inverted = Rect::new(50.0, 80.0, 10.0, 20.0);
329        assert_eq!(inverted.anchor(Pivot::TopLeft), Point::new(50.0, 80.0));
330        assert_eq!(
331            inverted.anchor(Pivot::Relative(0.25, 0.5)),
332            Point::new(40.0, 50.0)
333        );
334    }
335
336    use super::*;
337    use crate::{Interpolate, Rect, Size};
338
339    #[test]
340    fn constructors_use_opposite_corners_and_preserve_double_precision() {
341        let bounds = Rect::new(10.0, 20.0, 40.0, 30.0);
342        assert_eq!(bounds.size(), Size::new(30.0, 10.0));
343        assert_eq!(
344            Rect::from_origin_size(Point::new(10.0, 20.0), bounds.size()),
345            bounds
346        );
347        assert_eq!(
348            Rect::from_center_size(Point::new(25.0, 25.0), bounds.size()),
349            bounds
350        );
351        assert_eq!(bounds.size().to_rect(), Rect::new(0.0, 0.0, 30.0, 10.0));
352        let origin = 1_000_000_000.0;
353        let precise = Rect::new(origin, 0.0, origin + 0.25, 0.5);
354        assert_eq!(precise.width(), 0.25);
355        assert_eq!(precise.center(), Point::new(origin + 0.125, 0.25));
356        assert_eq!(precise.left as f32, precise.right as f32);
357        assert_eq!(Size::new(-1.0, 2.0).to_rect().width(), -1.0);
358    }
359
360    #[test]
361    fn containment_intersection_union_and_inset_have_explicit_empty_edges() {
362        let bounds = Rect::new(10.0, 20.0, 40.0, 30.0);
363        assert!(bounds.contains(Point::new(10.0, 20.0)));
364        assert!(!bounds.contains(Point::new(40.0, 20.0)));
365        assert!(!bounds.contains(Point::new(10.0, 30.0)));
366        assert!(!bounds.contains(Point::new(f64::NAN, 25.0)));
367        let adjacent = Rect::new(40.0, 20.0, 50.0, 30.0);
368        assert_eq!(
369            bounds.intersect(adjacent),
370            Rect::new(40.0, 20.0, 40.0, 30.0)
371        );
372        assert!(bounds.intersect(adjacent).is_empty());
373        assert_eq!(bounds.union(adjacent), Rect::new(10.0, 20.0, 50.0, 30.0));
374        assert_eq!(bounds.union(Rect::default()), bounds);
375        assert_eq!(Rect::default().union(bounds), bounds);
376        assert_eq!(bounds.inset(2.0), Rect::new(12.0, 22.0, 38.0, 28.0));
377        assert_eq!(bounds.inset(-2.0), Rect::new(8.0, 18.0, 42.0, 32.0));
378        let inverted = bounds.inset(20.0);
379        assert!(inverted.is_empty());
380        assert!(!inverted.contains(inverted.center()));
381        assert!(bounds.intersect(inverted).is_empty());
382        assert_eq!(bounds.union(inverted), bounds);
383        assert_eq!(
384            bounds.intersect(Rect::new(50.0, 50.0, 60.0, 60.0)),
385            Rect::new(50.0, 50.0, 50.0, 50.0)
386        );
387    }
388
389    #[test]
390    fn extreme_midpoints_remain_finite_and_nonfinite_bounds_are_not_silently_discarded() {
391        let large = Rect::new(1e308, -1.4e308, 1.4e308, -1e308);
392        assert_eq!(large.center(), Point::new(1.2e308, -1.2e308));
393        assert_eq!(
394            Rect::new(-f64::MAX, 0.0, f64::MAX, 2.0).center(),
395            Point::new(0.0, 1.0)
396        );
397        let smallest = f64::from_bits(1);
398        assert_eq!(
399            Rect::new(smallest, smallest, smallest, smallest).center(),
400            Point::new(smallest, smallest)
401        );
402        assert_eq!(
403            Rect::new(smallest, smallest, smallest * 2.0, smallest * 2.0).center(),
404            Point::new(smallest * 2.0, smallest * 2.0)
405        );
406        let valid = Rect::new(0.0, 0.0, 10.0, 10.0);
407        for invalid in [
408            Rect::new(f64::NAN, 0.0, 10.0, 10.0),
409            Rect::new(0.0, 0.0, f64::INFINITY, 10.0),
410        ] {
411            assert!(!invalid.is_finite());
412            assert!(!invalid.is_empty());
413            assert!(!invalid.contains(Point::ORIGIN));
414            assert!(!valid.union(invalid).is_finite());
415            assert!(!invalid.union(valid).is_finite());
416            assert!(!valid.intersect(invalid).is_finite());
417            assert!(!invalid.intersect(valid).is_finite());
418        }
419    }
420
421    #[test]
422    fn interpolation_retains_f64_progress_and_signed_extrapolated_values() {
423        let progress = 0.500_000_000_1;
424        let size = Size::default().interpolate(&Size::new(1.0, 2.0), progress);
425        assert_eq!(size, Size::new(progress, 2.0 * progress));
426        assert_ne!(size.width, f64::from(progress as f32));
427        let bounds = Rect::default().interpolate(&Rect::new(1.0, 2.0, 3.0, 4.0), progress);
428        assert_eq!(
429            bounds,
430            Rect::new(progress, 2.0 * progress, 3.0 * progress, 4.0 * progress)
431        );
432        let inverted = Rect::new(0.0, 0.0, 10.0, 20.0).interpolate(&Rect::default(), 2.0);
433        assert_eq!(inverted.size(), Size::new(-10.0, -20.0));
434        assert!(inverted.is_empty());
435        assert_eq!(
436            Size::new(10.0, 20.0).interpolate(&Size::default(), 2.0),
437            Size::new(-10.0, -20.0)
438        );
439    }
440}