Skip to main content

fret_core/scene/
image_object_fit.rs

1use crate::ViewportFit;
2use crate::geometry::{Point, Px, Rect, Size};
3use crate::scene::UvRect;
4
5#[derive(Debug, Clone, Copy, PartialEq)]
6pub struct ImageObjectFitMapped {
7    pub draw_rect: Rect,
8    pub uv: UvRect,
9}
10
11/// Maps a destination rect + source image size into a draw rect and a normalized UV rect,
12/// following the `SceneOp::Image` v1 `object-fit` contract (ADR 0231).
13///
14/// Returns `None` for degenerate inputs (zero/negative destination size or zero source size).
15pub fn map_image_object_fit(
16    destination_rect: Rect,
17    source_px_size: (u32, u32),
18    fit: ViewportFit,
19) -> Option<ImageObjectFitMapped> {
20    let (sw, sh) = source_px_size;
21    if sw == 0 || sh == 0 {
22        return None;
23    }
24
25    let dw = destination_rect.size.width.0.max(0.0);
26    let dh = destination_rect.size.height.0.max(0.0);
27    if dw <= 0.0 || dh <= 0.0 || !dw.is_finite() || !dh.is_finite() {
28        return None;
29    }
30
31    let sw = sw as f32;
32    let sh = sh as f32;
33
34    match fit {
35        ViewportFit::Stretch => Some(ImageObjectFitMapped {
36            draw_rect: destination_rect,
37            uv: UvRect::FULL,
38        }),
39        ViewportFit::Contain => {
40            let s = (dw / sw).min(dh / sh);
41            if !s.is_finite() || s <= 0.0 {
42                return None;
43            }
44
45            let draw_w = sw * s;
46            let draw_h = sh * s;
47            let x = destination_rect.origin.x.0 + (dw - draw_w) * 0.5;
48            let y = destination_rect.origin.y.0 + (dh - draw_h) * 0.5;
49
50            Some(ImageObjectFitMapped {
51                draw_rect: Rect::new(Point::new(Px(x), Px(y)), Size::new(Px(draw_w), Px(draw_h))),
52                uv: UvRect::FULL,
53            })
54        }
55        ViewportFit::Cover => {
56            let s = (dw / sw).max(dh / sh);
57            if !s.is_finite() || s <= 0.0 {
58                return None;
59            }
60
61            let cover_w = sw * s;
62            let cover_h = sh * s;
63            if cover_w <= 0.0 || cover_h <= 0.0 || !cover_w.is_finite() || !cover_h.is_finite() {
64                return None;
65            }
66
67            let mut u0 = ((cover_w - dw) * 0.5) / cover_w;
68            let mut v0 = ((cover_h - dh) * 0.5) / cover_h;
69            let mut u1 = 1.0 - u0;
70            let mut v1 = 1.0 - v0;
71
72            if !(u0.is_finite() && v0.is_finite() && u1.is_finite() && v1.is_finite()) {
73                return None;
74            }
75
76            u0 = u0.clamp(0.0, 1.0);
77            v0 = v0.clamp(0.0, 1.0);
78            u1 = u1.clamp(0.0, 1.0);
79            v1 = v1.clamp(0.0, 1.0);
80
81            if u1 < u0 {
82                u1 = u0;
83            }
84            if v1 < v0 {
85                v1 = v0;
86            }
87
88            Some(ImageObjectFitMapped {
89                draw_rect: destination_rect,
90                uv: UvRect { u0, v0, u1, v1 },
91            })
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
101        Rect::new(Point::new(Px(x), Px(y)), Size::new(Px(w), Px(h)))
102    }
103
104    #[test]
105    fn stretch_maps_to_full_uv_and_dest_rect() {
106        let mapped = map_image_object_fit(
107            rect(10.0, 20.0, 100.0, 80.0),
108            (200, 100),
109            ViewportFit::Stretch,
110        )
111        .unwrap();
112        assert_eq!(mapped.draw_rect, rect(10.0, 20.0, 100.0, 80.0));
113        assert_eq!(mapped.uv, UvRect::FULL);
114    }
115
116    #[test]
117    fn contain_letterboxes_by_shrinking_draw_rect() {
118        let mapped = map_image_object_fit(
119            rect(0.0, 0.0, 100.0, 100.0),
120            (200, 100),
121            ViewportFit::Contain,
122        )
123        .unwrap();
124        assert_eq!(mapped.draw_rect, rect(0.0, 25.0, 100.0, 50.0));
125        assert_eq!(mapped.uv, UvRect::FULL);
126    }
127
128    #[test]
129    fn contain_pillarboxes_when_source_is_tall() {
130        let mapped = map_image_object_fit(
131            rect(0.0, 0.0, 200.0, 100.0),
132            (100, 200),
133            ViewportFit::Contain,
134        )
135        .unwrap();
136        assert_eq!(mapped.draw_rect, rect(75.0, 0.0, 50.0, 100.0));
137        assert_eq!(mapped.uv, UvRect::FULL);
138    }
139
140    #[test]
141    fn cover_center_crops_by_adjusting_uv() {
142        let mapped =
143            map_image_object_fit(rect(0.0, 0.0, 100.0, 100.0), (200, 100), ViewportFit::Cover)
144                .unwrap();
145        assert_eq!(mapped.draw_rect, rect(0.0, 0.0, 100.0, 100.0));
146        assert!((mapped.uv.u0 - 0.25).abs() <= 1.0e-6);
147        assert!((mapped.uv.u1 - 0.75).abs() <= 1.0e-6);
148        assert!((mapped.uv.v0 - 0.0).abs() <= 1.0e-6);
149        assert!((mapped.uv.v1 - 1.0).abs() <= 1.0e-6);
150    }
151
152    #[test]
153    fn cover_handles_tall_images() {
154        let mapped =
155            map_image_object_fit(rect(0.0, 0.0, 200.0, 100.0), (100, 200), ViewportFit::Cover)
156                .unwrap();
157        assert_eq!(mapped.draw_rect, rect(0.0, 0.0, 200.0, 100.0));
158        assert!((mapped.uv.u0 - 0.0).abs() <= 1.0e-6);
159        assert!((mapped.uv.u1 - 1.0).abs() <= 1.0e-6);
160        assert!((mapped.uv.v0 - 0.375).abs() <= 1.0e-6);
161        assert!((mapped.uv.v1 - 0.625).abs() <= 1.0e-6);
162    }
163
164    #[test]
165    fn cover_uv_is_clamped_and_monotonic() {
166        let mapped =
167            map_image_object_fit(rect(0.0, 0.0, 100.0, 100.0), (1, 1), ViewportFit::Cover).unwrap();
168
169        assert!((0.0..=1.0).contains(&mapped.uv.u0));
170        assert!((0.0..=1.0).contains(&mapped.uv.v0));
171        assert!((0.0..=1.0).contains(&mapped.uv.u1));
172        assert!((0.0..=1.0).contains(&mapped.uv.v1));
173        assert!(mapped.uv.u0 <= mapped.uv.u1);
174        assert!(mapped.uv.v0 <= mapped.uv.v1);
175    }
176
177    #[test]
178    fn degenerate_inputs_return_none() {
179        assert!(
180            map_image_object_fit(rect(0.0, 0.0, 0.0, 10.0), (10, 10), ViewportFit::Cover).is_none()
181        );
182        assert!(
183            map_image_object_fit(rect(0.0, 0.0, 10.0, 10.0), (0, 10), ViewportFit::Cover).is_none()
184        );
185    }
186}