Skip to main content

ferroui_pretext_layout/
obstacle_layout_helper.rs

1use crate::{Interval, RectObstacle};
2
3/// Geometry utilities for carving horizontal text slots from exclusion shapes.
4pub struct ObstacleLayoutHelper;
5
6impl ObstacleLayoutHelper {
7    /// Carve all blocked intervals, then discard slots narrower than the minimum.
8    /// The surviving intervals remain ordered from left to right.
9    pub fn carve_text_line_slots(
10        base_slot: Interval,
11        blocked: impl IntoIterator<Item = Interval>,
12        min_slot_width: f64,
13    ) -> Vec<Interval> {
14        if !base_slot.left.is_finite()
15            || !base_slot.right.is_finite()
16            || base_slot.width() <= 0.0
17            || !min_slot_width.is_finite()
18            || min_slot_width < 0.0
19        {
20            return Vec::new();
21        }
22        let mut slots = vec![base_slot];
23        let mut next = Vec::new();
24        for interval in blocked {
25            if !interval.left.is_finite() || !interval.right.is_finite() || interval.width() <= 0.0
26            {
27                continue;
28            }
29            next.clear();
30            for slot in &slots {
31                if interval.right <= slot.left || interval.left >= slot.right {
32                    next.push(*slot);
33                    continue;
34                }
35                if interval.left > slot.left {
36                    next.push(Interval {
37                        left: slot.left,
38                        right: interval.left,
39                    });
40                }
41                if interval.right < slot.right {
42                    next.push(Interval {
43                        left: interval.right,
44                        right: slot.right,
45                    });
46                }
47            }
48            std::mem::swap(&mut slots, &mut next);
49            if slots.is_empty() {
50                break;
51            }
52        }
53        slots.retain(|slot| slot.width() >= min_slot_width);
54        slots
55    }
56
57    /// The widest horizontal intersection of a circle with the entire line band.
58    /// A tangent band has no occupied area and yields `None`.
59    #[allow(clippy::too_many_arguments)]
60    pub fn circle_interval_for_band(
61        cx: f64,
62        cy: f64,
63        radius: f64,
64        band_top: f64,
65        band_bottom: f64,
66        horizontal_padding: f64,
67        vertical_padding: f64,
68    ) -> Option<Interval> {
69        Self::ellipse_interval_for_band(
70            cx,
71            cy,
72            radius,
73            radius,
74            band_top,
75            band_bottom,
76            horizontal_padding,
77            vertical_padding,
78        )
79    }
80
81    #[allow(clippy::too_many_arguments)]
82    pub fn ellipse_interval_for_band(
83        cx: f64,
84        cy: f64,
85        radius_x: f64,
86        radius_y: f64,
87        band_top: f64,
88        band_bottom: f64,
89        horizontal_padding: f64,
90        vertical_padding: f64,
91    ) -> Option<Interval> {
92        if ![
93            cx,
94            cy,
95            radius_x,
96            radius_y,
97            band_top,
98            band_bottom,
99            horizontal_padding,
100            vertical_padding,
101        ]
102        .iter()
103        .all(|value| value.is_finite())
104            || radius_x <= 0.0
105            || radius_y <= 0.0
106            || band_top > band_bottom
107            || horizontal_padding < 0.0
108            || vertical_padding < 0.0
109        {
110            return None;
111        }
112        let top = band_top - vertical_padding;
113        let bottom = band_bottom + vertical_padding;
114        if top >= cy + radius_y || bottom <= cy - radius_y {
115            return None;
116        }
117        let min_dy = if cy >= top && cy <= bottom {
118            0.0
119        } else if cy < top {
120            top - cy
121        } else {
122            cy - bottom
123        };
124        if min_dy >= radius_y {
125            return None;
126        }
127        let normalized_dy = min_dy / radius_y;
128        let max_dx = radius_x * (1.0 - normalized_dy * normalized_dy).max(0.0).sqrt();
129        let interval = Interval {
130            left: cx - max_dx - horizontal_padding,
131            right: cx + max_dx + horizontal_padding,
132        };
133        (interval.left.is_finite() && interval.right.is_finite()).then_some(interval)
134    }
135
136    pub fn get_rect_intervals_for_band(
137        rects: &[RectObstacle],
138        band_top: f64,
139        band_bottom: f64,
140        horizontal_padding: f64,
141        vertical_padding: f64,
142    ) -> Vec<Interval> {
143        if ![band_top, band_bottom, horizontal_padding, vertical_padding]
144            .iter()
145            .all(|value| value.is_finite())
146            || band_top > band_bottom
147            || horizontal_padding < 0.0
148            || vertical_padding < 0.0
149        {
150            return Vec::new();
151        }
152        let mut intervals = Vec::with_capacity(rects.len());
153        for rect in rects {
154            if rect.width <= 0.0
155                || rect.height <= 0.0
156                || ![
157                    rect.x,
158                    rect.y,
159                    rect.width,
160                    rect.height,
161                    rect.right(),
162                    rect.bottom(),
163                ]
164                .iter()
165                .all(|value| value.is_finite())
166                || band_bottom <= rect.y - vertical_padding
167                || band_top >= rect.bottom() + vertical_padding
168            {
169                continue;
170            }
171            let interval = Interval {
172                left: rect.x - horizontal_padding,
173                right: rect.right() + horizontal_padding,
174            };
175            if interval.left.is_finite() && interval.right.is_finite() {
176                intervals.push(interval);
177            }
178        }
179        intervals
180    }
181
182    /// Pick the widest usable slot and resolve equal widths by horizontal order.
183    /// Empty inputs return `None` instead of indexing an absent first interval.
184    pub fn pick_slot(slots: &[Interval], prefer_right_on_tie: bool) -> Option<Interval> {
185        let mut best = None;
186        for &candidate in slots {
187            if !candidate.left.is_finite()
188                || !candidate.right.is_finite()
189                || candidate.width() <= 0.0
190            {
191                continue;
192            }
193            let Some(current): Option<Interval> = best else {
194                best = Some(candidate);
195                continue;
196            };
197            if candidate.width() > current.width()
198                || (candidate.width() == current.width()
199                    && if prefer_right_on_tie {
200                        candidate.left > current.left
201                    } else {
202                        candidate.left < current.left
203                    })
204            {
205                best = Some(candidate);
206            }
207        }
208        best
209    }
210}