Skip to main content

ass_renderer/layout/positioning/
info.rs

1use crate::pipeline::tag_processor::ProcessedTags;
2
3use super::{BoundingBox, PositionConfig};
4
5/// Calculated position with anchor and origin points
6#[derive(Debug, Clone)]
7pub struct PositionInfo {
8    /// Actual render position (top-left of bounding box)
9    pub render_x: f32,
10    pub render_y: f32,
11    /// Anchor point for alignment (based on alignment value)
12    pub anchor_x: f32,
13    pub anchor_y: f32,
14    /// Rotation origin point
15    pub origin_x: f32,
16    pub origin_y: f32,
17    /// Whether position was explicitly set
18    pub explicit_position: bool,
19}
20
21impl PositionInfo {
22    /// Calculate position based on alignment and tags
23    pub fn calculate(tags: &ProcessedTags, bbox: &BoundingBox, config: &PositionConfig) -> Self {
24        // Get effective alignment
25        let alignment = tags
26            .formatting
27            .alignment
28            .unwrap_or(config.default_alignment);
29
30        // Calculate anchor point based on alignment
31        let (anchor_x, anchor_y) = Self::get_anchor_point(
32            alignment,
33            bbox,
34            config.screen_width,
35            config.screen_height,
36            config.margin_left,
37            config.margin_right,
38            config.margin_vertical,
39        );
40
41        // Check for explicit positioning
42        let (render_x, render_y, explicit) = if let Some((pos_x, pos_y)) = tags.position {
43            // Explicit \pos tag
44            let render_x = pos_x - Self::get_alignment_offset_x(alignment, bbox);
45            let render_y = pos_y - Self::get_alignment_offset_y(alignment, bbox);
46            (render_x, render_y, true)
47        } else if let Some((x1, y1, _x2, _y2, _t1, _t2)) = tags.movement {
48            // \move tag (simplified - should interpolate based on time)
49            // For now just use start position
50            let render_x = x1 - Self::get_alignment_offset_x(alignment, bbox);
51            let render_y = y1 - Self::get_alignment_offset_y(alignment, bbox);
52            (render_x, render_y, true)
53        } else {
54            // Use alignment-based positioning
55            (anchor_x, anchor_y, false)
56        };
57
58        // Calculate rotation origin
59        let (origin_x, origin_y) = if let Some((org_x, org_y)) = tags.origin {
60            // Explicit \org tag
61            (org_x, org_y)
62        } else {
63            // Default to anchor point (libass behavior)
64            (anchor_x + bbox.width / 2.0, anchor_y + bbox.height / 2.0)
65        };
66
67        Self {
68            render_x,
69            render_y,
70            anchor_x,
71            anchor_y,
72            origin_x,
73            origin_y,
74            explicit_position: explicit,
75        }
76    }
77
78    /// Get anchor point based on alignment value
79    fn get_anchor_point(
80        alignment: u8,
81        bbox: &BoundingBox,
82        screen_width: f32,
83        screen_height: f32,
84        margin_left: f32,
85        margin_right: f32,
86        margin_vertical: f32,
87    ) -> (f32, f32) {
88        // Horizontal position based on alignment
89        let x = match alignment % 3 {
90            1 => {
91                // Left alignment (1, 4, 7)
92                margin_left
93            }
94            2 | 0 => {
95                // Center alignment (2, 5, 8)
96                (screen_width - bbox.width) / 2.0
97            }
98            _ => {
99                // Right alignment (3, 6, 9)
100                screen_width - margin_right - bbox.width
101            }
102        };
103
104        // Vertical position based on alignment
105        let y = match alignment {
106            1..=3 => {
107                // Bottom alignment
108                screen_height - margin_vertical - bbox.height
109            }
110            4..=6 => {
111                // Middle alignment
112                (screen_height - bbox.height) / 2.0
113            }
114            7..=9 => {
115                // Top alignment
116                margin_vertical
117            }
118            _ => {
119                // Default to bottom
120                screen_height - margin_vertical - bbox.height
121            }
122        };
123
124        (x, y)
125    }
126
127    /// Get horizontal offset for alignment anchor
128    fn get_alignment_offset_x(alignment: u8, bbox: &BoundingBox) -> f32 {
129        match alignment % 3 {
130            1 => 0.0,                  // Left
131            2 | 0 => bbox.width / 2.0, // Center
132            _ => bbox.width,           // Right
133        }
134    }
135
136    /// Get vertical offset for alignment anchor
137    fn get_alignment_offset_y(alignment: u8, bbox: &BoundingBox) -> f32 {
138        match alignment {
139            1..=3 => bbox.height,       // Bottom
140            4..=6 => bbox.height / 2.0, // Middle
141            7..=9 => 0.0,               // Top
142            _ => bbox.height,           // Default to bottom
143        }
144    }
145
146    /// Calculate position with movement interpolation
147    pub fn calculate_with_movement(
148        tags: &ProcessedTags,
149        bbox: &BoundingBox,
150        config: &PositionConfig,
151        current_time_ms: u32,
152        event_start_ms: u32,
153    ) -> Self {
154        // Get base position
155        let mut pos = Self::calculate(tags, bbox, config);
156
157        // Apply movement if present
158        if let Some((x1, y1, x2, y2, t1, t2)) = tags.movement {
159            let event_time = current_time_ms.saturating_sub(event_start_ms);
160
161            // Calculate interpolation factor
162            let factor = if t2 > t1 {
163                let progress = (event_time.saturating_sub(t1)) as f32 / (t2 - t1) as f32;
164                progress.clamp(0.0, 1.0)
165            } else {
166                1.0 // Instant movement
167            };
168
169            // Interpolate position
170            let current_x = x1 + (x2 - x1) * factor;
171            let current_y = y1 + (y2 - y1) * factor;
172
173            // Apply alignment offset
174            let alignment = tags
175                .formatting
176                .alignment
177                .unwrap_or(config.default_alignment);
178            pos.render_x = current_x - Self::get_alignment_offset_x(alignment, bbox);
179            pos.render_y = current_y - Self::get_alignment_offset_y(alignment, bbox);
180            pos.explicit_position = true;
181        }
182
183        pos
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn test_alignment_positions() {
193        let bbox = BoundingBox {
194            x: 0.0,
195            y: 0.0,
196            width: 100.0,
197            height: 50.0,
198        };
199
200        let tags = ProcessedTags::default();
201
202        // Test bottom-center alignment (default)
203        let config = PositionConfig {
204            screen_width: 1920.0,
205            screen_height: 1080.0,
206            margin_left: 0.0,
207            margin_right: 0.0,
208            margin_vertical: 0.0,
209            default_alignment: 2,
210        };
211        let pos = PositionInfo::calculate(&tags, &bbox, &config);
212
213        assert_eq!(pos.anchor_x, 910.0); // (1920 - 100) / 2
214        assert_eq!(pos.anchor_y, 1030.0); // 1080 - 0 - 50
215    }
216
217    #[test]
218    fn test_explicit_position() {
219        let bbox = BoundingBox {
220            x: 0.0,
221            y: 0.0,
222            width: 100.0,
223            height: 50.0,
224        };
225
226        let tags = ProcessedTags {
227            position: Some((500.0, 300.0)),
228            ..ProcessedTags::default()
229        };
230
231        let config = PositionConfig {
232            screen_width: 1920.0,
233            screen_height: 1080.0,
234            margin_left: 0.0,
235            margin_right: 0.0,
236            margin_vertical: 0.0,
237            default_alignment: 2,
238        };
239        let pos = PositionInfo::calculate(&tags, &bbox, &config);
240
241        assert!(pos.explicit_position);
242        assert_eq!(pos.render_x, 450.0); // 500 - 50 (center offset)
243        assert_eq!(pos.render_y, 250.0); // 300 - 50 (bottom offset)
244    }
245
246    #[test]
247    fn test_movement_interpolation() {
248        let bbox = BoundingBox {
249            x: 0.0,
250            y: 0.0,
251            width: 100.0,
252            height: 50.0,
253        };
254
255        let tags = ProcessedTags {
256            movement: Some((100.0, 100.0, 500.0, 300.0, 0, 1000)),
257            ..ProcessedTags::default()
258        };
259
260        // Test at halfway point (500ms)
261        let config = PositionConfig {
262            screen_width: 1920.0,
263            screen_height: 1080.0,
264            margin_left: 0.0,
265            margin_right: 0.0,
266            margin_vertical: 0.0,
267            default_alignment: 2,
268        };
269        let pos = PositionInfo::calculate_with_movement(&tags, &bbox, &config, 500, 0);
270
271        // Position should be interpolated halfway
272        assert_eq!(pos.render_x, 250.0); // 300 - 50 (center offset)
273        assert_eq!(pos.render_y, 150.0); // 200 - 50 (bottom offset)
274    }
275}