Skip to main content

figif_core/
selector.rs

1//! Fluent segment selector API for building operations.
2//!
3//! This module provides a chainable, ergonomic API for selecting and
4//! manipulating segments in an analyzed GIF.
5//!
6//! # Example
7//!
8//! ```ignore
9//! use figif_core::prelude::*;
10//!
11//! let analysis = Figif::new().analyze_file("demo.gif")?;
12//!
13//! // Cap all pauses to 300ms
14//! let ops = analysis.pauses().cap(300);
15//!
16//! // Collapse only long pauses
17//! let ops = analysis.pauses()
18//!     .longer_than(500)
19//!     .collapse(200);
20//!
21//! // Speed up motion segments
22//! let ops = analysis.motion().speed_up(1.5);
23//!
24//! // Combine operations
25//! let ops = analysis.pauses().cap(300)
26//!     .merge(&analysis.motion().speed_up(1.2));
27//! ```
28
29use crate::types::{Segment, SegmentOp, SegmentOps};
30
31/// A selector over a subset of segments, supporting filtering and operations.
32///
33/// Created via methods on [`Analysis`](crate::Analysis) like `pauses()`, `motion()`, etc.
34/// Filters can be chained, and terminal operations produce [`SegmentOps`].
35#[derive(Debug, Clone)]
36pub struct SegmentSelector<'a> {
37    segments: Vec<&'a Segment>,
38}
39
40impl<'a> SegmentSelector<'a> {
41    /// Create a new selector from a list of segment references.
42    pub(crate) fn new(segments: Vec<&'a Segment>) -> Self {
43        Self { segments }
44    }
45
46    /// Get the number of selected segments.
47    pub fn count(&self) -> usize {
48        self.segments.len()
49    }
50
51    /// Check if any segments are selected.
52    pub fn is_empty(&self) -> bool {
53        self.segments.is_empty()
54    }
55
56    /// Get the selected segments.
57    pub fn segments(&self) -> &[&'a Segment] {
58        &self.segments
59    }
60
61    /// Get total duration of selected segments in milliseconds.
62    pub fn total_duration_ms(&self) -> u64 {
63        self.segments.iter().map(|s| s.duration_ms() as u64).sum()
64    }
65
66    /// Get total frame count of selected segments.
67    pub fn total_frames(&self) -> usize {
68        self.segments.iter().map(|s| s.frame_count()).sum()
69    }
70
71    // =========================================================================
72    // Filters - return Self for chaining
73    // =========================================================================
74
75    /// Filter to segments longer than the specified duration.
76    pub fn longer_than(self, ms: u32) -> Self {
77        let cs = (ms / 10) as u16;
78        Self {
79            segments: self
80                .segments
81                .into_iter()
82                .filter(|s| s.total_duration_cs > cs)
83                .collect(),
84        }
85    }
86
87    /// Filter to segments shorter than the specified duration.
88    pub fn shorter_than(self, ms: u32) -> Self {
89        let cs = (ms / 10) as u16;
90        Self {
91            segments: self
92                .segments
93                .into_iter()
94                .filter(|s| s.total_duration_cs < cs)
95                .collect(),
96        }
97    }
98
99    /// Filter to segments with duration in the specified range (inclusive).
100    pub fn duration_between(self, min_ms: u32, max_ms: u32) -> Self {
101        let min_cs = (min_ms / 10) as u16;
102        let max_cs = (max_ms / 10) as u16;
103        Self {
104            segments: self
105                .segments
106                .into_iter()
107                .filter(|s| s.total_duration_cs >= min_cs && s.total_duration_cs <= max_cs)
108                .collect(),
109        }
110    }
111
112    /// Filter to segments with more than N frames.
113    pub fn frames_gt(self, count: usize) -> Self {
114        Self {
115            segments: self
116                .segments
117                .into_iter()
118                .filter(|s| s.frame_count() > count)
119                .collect(),
120        }
121    }
122
123    /// Filter to segments with fewer than N frames.
124    pub fn frames_lt(self, count: usize) -> Self {
125        Self {
126            segments: self
127                .segments
128                .into_iter()
129                .filter(|s| s.frame_count() < count)
130                .collect(),
131        }
132    }
133
134    /// Filter to segments with exactly N frames.
135    pub fn frames_eq(self, count: usize) -> Self {
136        Self {
137            segments: self
138                .segments
139                .into_iter()
140                .filter(|s| s.frame_count() == count)
141                .collect(),
142        }
143    }
144
145    /// Filter using a custom predicate.
146    pub fn filter<F>(self, predicate: F) -> Self
147    where
148        F: Fn(&Segment) -> bool,
149    {
150        Self {
151            segments: self.segments.into_iter().filter(|s| predicate(s)).collect(),
152        }
153    }
154
155    /// Take only the first N segments.
156    pub fn take(self, n: usize) -> Self {
157        Self {
158            segments: self.segments.into_iter().take(n).collect(),
159        }
160    }
161
162    /// Skip the first N segments.
163    pub fn skip(self, n: usize) -> Self {
164        Self {
165            segments: self.segments.into_iter().skip(n).collect(),
166        }
167    }
168
169    /// Take the first segment only.
170    pub fn first(self) -> Self {
171        self.take(1)
172    }
173
174    /// Take the last segment only.
175    pub fn last(self) -> Self {
176        Self {
177            segments: self.segments.into_iter().last().into_iter().collect(),
178        }
179    }
180
181    // =========================================================================
182    // Terminal operations - return SegmentOps
183    // =========================================================================
184
185    /// Cap duration to a maximum value.
186    ///
187    /// Segments longer than `max_ms` are collapsed to a single frame
188    /// with that duration. Shorter segments are unchanged.
189    pub fn cap(&self, max_ms: u32) -> SegmentOps {
190        let max_cs = (max_ms / 10) as u16;
191        let mut ops = SegmentOps::new();
192
193        for segment in &self.segments {
194            if segment.total_duration_cs > max_cs {
195                ops.insert(segment.id, SegmentOp::Collapse { delay_cs: max_cs });
196            }
197        }
198
199        ops
200    }
201
202    /// Collapse each segment to a single frame with the specified duration.
203    pub fn collapse(&self, duration_ms: u32) -> SegmentOps {
204        let delay_cs = (duration_ms / 10) as u16;
205        let mut ops = SegmentOps::new();
206
207        for segment in &self.segments {
208            ops.insert(segment.id, SegmentOp::Collapse { delay_cs });
209        }
210
211        ops
212    }
213
214    /// Remove all selected segments entirely.
215    pub fn remove(&self) -> SegmentOps {
216        let mut ops = SegmentOps::new();
217
218        for segment in &self.segments {
219            ops.insert(segment.id, SegmentOp::Remove);
220        }
221
222        ops
223    }
224
225    /// Speed up selected segments by a factor.
226    ///
227    /// A factor of 2.0 makes segments play 2x faster (half duration).
228    pub fn speed_up(&self, factor: f64) -> SegmentOps {
229        let mut ops = SegmentOps::new();
230
231        for segment in &self.segments {
232            ops.insert(
233                segment.id,
234                SegmentOp::Scale {
235                    factor: 1.0 / factor,
236                },
237            );
238        }
239
240        ops
241    }
242
243    /// Slow down selected segments by a factor.
244    ///
245    /// A factor of 2.0 makes segments play 2x slower (double duration).
246    pub fn slow_down(&self, factor: f64) -> SegmentOps {
247        let mut ops = SegmentOps::new();
248
249        for segment in &self.segments {
250            ops.insert(segment.id, SegmentOp::Scale { factor });
251        }
252
253        ops
254    }
255
256    /// Set the total duration for each selected segment.
257    ///
258    /// The duration is distributed evenly across all frames in the segment.
259    pub fn set_duration(&self, ms: u32) -> SegmentOps {
260        let total_cs = (ms / 10) as u16;
261        let mut ops = SegmentOps::new();
262
263        for segment in &self.segments {
264            ops.insert(segment.id, SegmentOp::SetDuration { total_cs });
265        }
266
267        ops
268    }
269
270    /// Set a fixed delay for each frame in the selected segments.
271    pub fn set_frame_delay(&self, ms: u32) -> SegmentOps {
272        let delay_cs = (ms / 10) as u16;
273        let mut ops = SegmentOps::new();
274
275        for segment in &self.segments {
276            ops.insert(segment.id, SegmentOp::SetFrameDelay { delay_cs });
277        }
278
279        ops
280    }
281
282    /// Explicitly keep selected segments unchanged.
283    ///
284    /// This is useful when merging with other operations to ensure
285    /// certain segments are not modified.
286    pub fn keep(&self) -> SegmentOps {
287        let mut ops = SegmentOps::new();
288
289        for segment in &self.segments {
290            ops.insert(segment.id, SegmentOp::Keep);
291        }
292
293        ops
294    }
295
296    /// Scale timing by a raw factor.
297    ///
298    /// Factor < 1.0 speeds up, factor > 1.0 slows down.
299    pub fn scale(&self, factor: f64) -> SegmentOps {
300        let mut ops = SegmentOps::new();
301
302        for segment in &self.segments {
303            ops.insert(segment.id, SegmentOp::Scale { factor });
304        }
305
306        ops
307    }
308}
309
310// =========================================================================
311// Extension trait for SegmentOps to enable fluent merging
312// =========================================================================
313
314/// Extension methods for [`SegmentOps`].
315pub trait SegmentOpsExt {
316    /// Merge with another set of operations.
317    ///
318    /// Operations from `other` override operations in `self` for the same segment.
319    fn merge(&self, other: &SegmentOps) -> SegmentOps;
320
321    /// Merge with another set, consuming self.
322    fn and(self, other: SegmentOps) -> SegmentOps;
323
324    /// Merge multiple operation sets.
325    fn merge_all(sets: &[&SegmentOps]) -> SegmentOps;
326}
327
328impl SegmentOpsExt for SegmentOps {
329    fn merge(&self, other: &SegmentOps) -> SegmentOps {
330        let mut merged = self.clone();
331        merged.extend(other.iter().map(|(k, v)| (*k, v.clone())));
332        merged
333    }
334
335    fn and(mut self, other: SegmentOps) -> SegmentOps {
336        self.extend(other);
337        self
338    }
339
340    fn merge_all(sets: &[&SegmentOps]) -> SegmentOps {
341        let mut merged = SegmentOps::new();
342        for ops in sets {
343            merged.extend(ops.iter().map(|(k, v)| (*k, v.clone())));
344        }
345        merged
346    }
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352    use std::ops::Range;
353
354    fn make_segment(id: usize, duration_cs: u16, frames: usize, is_static: bool) -> Segment {
355        Segment {
356            id,
357            frame_range: Range {
358                start: 0,
359                end: frames,
360            },
361            total_duration_cs: duration_cs,
362            avg_distance: if is_static { 0.0 } else { 5.0 },
363            is_static,
364        }
365    }
366
367    #[test]
368    fn test_filter_longer_than() {
369        let segments = [
370            make_segment(0, 100, 10, true),
371            make_segment(1, 50, 5, true),
372            make_segment(2, 200, 20, true),
373        ];
374        let refs: Vec<_> = segments.iter().collect();
375        let selector = SegmentSelector::new(refs);
376
377        let filtered = selector.longer_than(600); // 60cs = 600ms
378        assert_eq!(filtered.count(), 2);
379    }
380
381    #[test]
382    fn test_cap_operation() {
383        let segments = [
384            make_segment(0, 100, 10, true), // 1000ms
385            make_segment(1, 50, 5, true),   // 500ms
386            make_segment(2, 200, 20, true), // 2000ms
387        ];
388        let refs: Vec<_> = segments.iter().collect();
389        let selector = SegmentSelector::new(refs);
390
391        let ops = selector.cap(800); // Cap to 800ms
392
393        // Only segments 0 and 2 should be capped
394        assert_eq!(ops.len(), 2);
395        assert!(matches!(
396            ops.get(&0),
397            Some(SegmentOp::Collapse { delay_cs: 80 })
398        ));
399        assert!(matches!(
400            ops.get(&2),
401            Some(SegmentOp::Collapse { delay_cs: 80 })
402        ));
403    }
404
405    #[test]
406    fn test_merge_ops() {
407        let mut ops1 = SegmentOps::new();
408        ops1.insert(0, SegmentOp::Keep);
409        ops1.insert(1, SegmentOp::Remove);
410
411        let mut ops2 = SegmentOps::new();
412        ops2.insert(1, SegmentOp::Collapse { delay_cs: 50 }); // Override
413        ops2.insert(2, SegmentOp::Remove);
414
415        let merged = ops1.merge(&ops2);
416
417        assert_eq!(merged.len(), 3);
418        assert!(matches!(merged.get(&0), Some(SegmentOp::Keep)));
419        assert!(matches!(
420            merged.get(&1),
421            Some(SegmentOp::Collapse { delay_cs: 50 })
422        ));
423        assert!(matches!(merged.get(&2), Some(SegmentOp::Remove)));
424    }
425}