Skip to main content

ass_renderer/collision/
resolver.rs

1//! Collision resolvers for subtitle positioning
2
3#[cfg(feature = "nostd")]
4use alloc::vec::Vec;
5#[cfg(not(feature = "nostd"))]
6use std::vec::Vec;
7
8use super::{BoundingBox, CollisionDetector, PositionedEvent};
9
10/// Collision resolver for subtitle positioning
11pub struct CollisionResolver {
12    #[allow(dead_code)] // Used in constructor, may be needed for future collision algorithms
13    screen_width: f32,
14    screen_height: f32,
15    positioned_events: Vec<PositionedEvent>,
16    collision_margin: f32,
17}
18
19impl CollisionResolver {
20    /// Create a new collision resolver
21    pub fn new(screen_width: f32, screen_height: f32) -> Self {
22        Self {
23            screen_width,
24            screen_height,
25            positioned_events: Vec::new(),
26            collision_margin: 2.0, // Default 2px margin between subtitles
27        }
28    }
29
30    /// Clear all positioned events
31    pub fn clear(&mut self) {
32        self.positioned_events.clear();
33    }
34
35    /// Set collision margin
36    pub fn set_collision_margin(&mut self, margin: f32) {
37        self.collision_margin = margin;
38    }
39
40    /// Add a positioned event that won't be moved
41    pub fn add_fixed(&mut self, event: PositionedEvent) {
42        self.positioned_events.push(event);
43    }
44
45    /// Resolve collisions by stacking the event away from its alignment margin:
46    /// bottom-aligned events (1-3) move up, top/middle (4-9) move down, until the
47    /// event no longer overlaps any already-placed same-layer event. This matches
48    /// libass "Normal" collisions, where earlier events keep the margin position
49    /// and later events stack past them. The resolved box is recorded so that
50    /// subsequent events stack against it.
51    pub fn find_position(&mut self, mut event: PositionedEvent) -> BoundingBox {
52        let margin = self.collision_margin;
53        let push_down = !matches!(event.alignment, 1..=3);
54        let mut bbox = event.bbox;
55
56        // Each iteration clears at least one overlap, so the placed-event count
57        // bounds the number of repositions needed.
58        for _ in 0..=self.positioned_events.len() {
59            let overlapping: Vec<BoundingBox> = self
60                .positioned_events
61                .iter()
62                .filter(|e| {
63                    e.layer == event.layer && e.bbox.expand(margin).intersects(&bbox.expand(margin))
64                })
65                .map(|e| e.bbox)
66                .collect();
67            if overlapping.is_empty() {
68                break;
69            }
70            bbox.y = if push_down {
71                overlapping
72                    .iter()
73                    .map(|b| b.y + b.height)
74                    .fold(f32::MIN, f32::max)
75                    + margin * 2.0
76            } else {
77                overlapping.iter().map(|b| b.y).fold(f32::MAX, f32::min)
78                    - bbox.height
79                    - margin * 2.0
80            };
81        }
82
83        // Keep the event on-screen if the stack would overflow the frame edge.
84        let max_y = (self.screen_height - bbox.height).max(0.0);
85        bbox.y = bbox.y.clamp(0.0, max_y);
86
87        event.bbox = bbox;
88        self.positioned_events.push(event);
89        bbox
90    }
91
92    /// Get all positioned events
93    pub fn positioned_events(&self) -> &[PositionedEvent] {
94        &self.positioned_events
95    }
96}
97
98/// Smart collision system with priority-based resolution
99pub struct SmartCollisionResolver {
100    resolver: CollisionResolver,
101    priority_threshold: i32,
102}
103
104impl SmartCollisionResolver {
105    /// Create a new smart collision resolver
106    pub fn new(screen_width: f32, screen_height: f32) -> Self {
107        Self {
108            resolver: CollisionResolver::new(screen_width, screen_height),
109            priority_threshold: 100,
110        }
111    }
112
113    /// Process events with smart collision resolution
114    pub fn process_events(&mut self, events: &mut [PositionedEvent]) {
115        // Sort by priority (lower priority = can be moved)
116        events.sort_by_key(|e| -e.priority);
117
118        self.resolver.clear();
119
120        for event in events.iter_mut() {
121            if event.priority >= self.priority_threshold {
122                // High priority - don't move
123                self.resolver.add_fixed(event.clone());
124            } else {
125                // Low priority - can be repositioned
126                let new_bbox = self.resolver.find_position(event.clone());
127                event.bbox = new_bbox;
128            }
129        }
130    }
131
132    /// Set the priority threshold
133    pub fn set_priority_threshold(&mut self, threshold: i32) {
134        self.priority_threshold = threshold;
135    }
136}
137
138impl CollisionDetector for SmartCollisionResolver {
139    fn check_collision(&self, bbox: &BoundingBox, existing: &[BoundingBox]) -> bool {
140        existing.iter().any(|e| bbox.intersects(e))
141    }
142
143    fn find_free_position(
144        &self,
145        bbox: &BoundingBox,
146        existing: &[BoundingBox],
147        bounds: &BoundingBox,
148    ) -> Option<(f32, f32)> {
149        // Try multiple positions to find a free spot
150        let step = 10.0; // Search step size
151
152        // First try moving down
153        for y_offset in (0..20).map(|i| i as f32 * step) {
154            let test_y = bbox.y + y_offset;
155            if test_y + bbox.height <= bounds.height {
156                let test_bbox = BoundingBox::new(bbox.x, test_y, bbox.width, bbox.height);
157                if !self.check_collision(&test_bbox, existing) {
158                    return Some((bbox.x, test_y));
159                }
160            }
161        }
162
163        // Then try moving up
164        for y_offset in (1..20).map(|i| i as f32 * step) {
165            let test_y = bbox.y - y_offset;
166            if test_y >= 0.0 {
167                let test_bbox = BoundingBox::new(bbox.x, test_y, bbox.width, bbox.height);
168                if !self.check_collision(&test_bbox, existing) {
169                    return Some((bbox.x, test_y));
170                }
171            }
172        }
173
174        None
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn test_collision_resolver() {
184        let mut resolver = CollisionResolver::new(1920.0, 1080.0);
185
186        let event1 = PositionedEvent {
187            bbox: BoundingBox::new(100.0, 900.0, 200.0, 50.0),
188            layer: 0,
189            margin_v: 10,
190            margin_l: 10,
191            margin_r: 10,
192            alignment: 2,
193            priority: 100,
194        };
195
196        let event2 = PositionedEvent {
197            bbox: BoundingBox::new(100.0, 900.0, 200.0, 50.0),
198            layer: 0,
199            margin_v: 10,
200            margin_l: 10,
201            margin_r: 10,
202            alignment: 2,
203            priority: 50,
204        };
205
206        resolver.add_fixed(event1);
207        let new_pos = resolver.find_position(event2);
208
209        // Second event should be moved due to collision
210        assert_ne!(new_pos.y, 900.0);
211    }
212}