Skip to main content

oximedia_edit/
blade_tool.rs

1#![allow(dead_code)]
2//! Blade/razor tool for cutting clips at specific frame positions.
3
4/// Controls which tracks the blade tool affects.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum BladeMode {
7    /// Cuts only the clip under the playhead on the active track.
8    Single,
9    /// Cuts all clips across every track at the playhead position.
10    AllTracks,
11    /// Cuts a clip and all clips linked to it (audio/video pairs).
12    Linked,
13}
14
15impl BladeMode {
16    /// Returns `true` if this mode cuts clips on every track.
17    #[must_use]
18    pub fn cuts_all(&self) -> bool {
19        matches!(self, BladeMode::AllTracks)
20    }
21
22    /// Human-readable label.
23    #[must_use]
24    pub fn label(&self) -> &'static str {
25        match self {
26            BladeMode::Single => "Single",
27            BladeMode::AllTracks => "All Tracks",
28            BladeMode::Linked => "Linked",
29        }
30    }
31}
32
33/// Describes where a single blade cut will occur.
34#[derive(Debug, Clone)]
35pub struct BladeCut {
36    /// Track index to cut on.
37    pub track_index: usize,
38    /// Clip id to cut.
39    pub clip_id: u64,
40    /// Frame position within the timeline at which to cut.
41    pub cut_frame: i64,
42}
43
44impl BladeCut {
45    /// Creates a new blade cut descriptor.
46    #[must_use]
47    pub fn new(track_index: usize, clip_id: u64, cut_frame: i64) -> Self {
48        Self {
49            track_index,
50            clip_id,
51            cut_frame,
52        }
53    }
54
55    /// Returns a cut snapped to the nearest frame boundary (integer).
56    /// In practice frames are already integers; this enforces the invariant.
57    #[must_use]
58    pub fn at_frame(mut self, frame: i64) -> Self {
59        self.cut_frame = frame;
60        self
61    }
62}
63
64/// The outcome produced after applying the blade tool.
65#[derive(Debug, Clone)]
66pub struct BladeResult {
67    /// All cuts that were applied during this operation.
68    pub cuts: Vec<BladeCut>,
69    /// Number of new clip segments created (= `cuts.len()` for single cuts each split = 1 new).
70    pub new_segments: usize,
71}
72
73impl BladeResult {
74    /// Creates a blade result from a list of applied cuts.
75    #[must_use]
76    pub fn new(cuts: Vec<BladeCut>) -> Self {
77        let new_segments = cuts.len();
78        Self { cuts, new_segments }
79    }
80
81    /// Number of cuts applied.
82    #[must_use]
83    pub fn cuts_applied(&self) -> usize {
84        self.cuts.len()
85    }
86}
87
88/// The blade/razor tool implementation.
89#[derive(Debug, Clone)]
90pub struct BladeTool {
91    /// Current operating mode.
92    pub mode: BladeMode,
93    /// Snap threshold in frames; 0 disables snapping.
94    pub snap_threshold: u32,
95}
96
97impl BladeTool {
98    /// Creates a blade tool with the given mode and snap threshold.
99    #[must_use]
100    pub fn new(mode: BladeMode, snap_threshold: u32) -> Self {
101        Self {
102            mode,
103            snap_threshold,
104        }
105    }
106
107    /// Performs a cut on the given set of (`track_index`, `clip_id`, `clip_start`, `clip_end`) tuples
108    /// at `frame`, returning a `BladeResult`.
109    ///
110    /// Only clips whose range `[clip_start, clip_end)` contains `frame` receive a cut.
111    #[must_use]
112    pub fn cut(&self, clips: &[(usize, u64, i64, i64)], frame: i64) -> BladeResult {
113        let cuts: Vec<BladeCut> = clips
114            .iter()
115            .filter(|(_, _, start, end)| frame > *start && frame < *end)
116            .map(|(track, id, _, _)| BladeCut::new(*track, *id, frame))
117            .collect();
118        BladeResult::new(cuts)
119    }
120
121    /// Returns the cuts that *would* be applied without actually applying them.
122    #[must_use]
123    pub fn preview_cut(&self, clips: &[(usize, u64, i64, i64)], frame: i64) -> Vec<BladeCut> {
124        self.cut(clips, frame).cuts
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_single_not_all() {
134        assert!(!BladeMode::Single.cuts_all());
135    }
136
137    #[test]
138    fn test_all_tracks_cuts_all() {
139        assert!(BladeMode::AllTracks.cuts_all());
140    }
141
142    #[test]
143    fn test_linked_not_all() {
144        assert!(!BladeMode::Linked.cuts_all());
145    }
146
147    #[test]
148    fn test_blade_mode_labels() {
149        assert_eq!(BladeMode::Single.label(), "Single");
150        assert_eq!(BladeMode::AllTracks.label(), "All Tracks");
151        assert_eq!(BladeMode::Linked.label(), "Linked");
152    }
153
154    #[test]
155    fn test_blade_cut_at_frame() {
156        let cut = BladeCut::new(0, 1, 50).at_frame(75);
157        assert_eq!(cut.cut_frame, 75);
158    }
159
160    #[test]
161    fn test_blade_result_cuts_applied() {
162        let cuts = vec![BladeCut::new(0, 1, 50), BladeCut::new(1, 2, 50)];
163        let result = BladeResult::new(cuts);
164        assert_eq!(result.cuts_applied(), 2);
165        assert_eq!(result.new_segments, 2);
166    }
167
168    #[test]
169    fn test_blade_tool_cut_single_clip() {
170        let tool = BladeTool::new(BladeMode::Single, 2);
171        let clips = vec![(0usize, 1u64, 0i64, 100i64)];
172        let result = tool.cut(&clips, 50);
173        assert_eq!(result.cuts_applied(), 1);
174        assert_eq!(result.cuts[0].cut_frame, 50);
175    }
176
177    #[test]
178    fn test_blade_tool_no_cut_outside_range() {
179        let tool = BladeTool::new(BladeMode::Single, 0);
180        let clips = vec![(0usize, 1u64, 0i64, 100i64)];
181        // Frame 150 is outside the clip
182        let result = tool.cut(&clips, 150);
183        assert_eq!(result.cuts_applied(), 0);
184    }
185
186    #[test]
187    fn test_blade_tool_no_cut_at_boundary() {
188        let tool = BladeTool::new(BladeMode::Single, 0);
189        let clips = vec![(0usize, 1u64, 0i64, 100i64)];
190        // Cutting exactly at start (0) should NOT cut (frame must be strictly inside)
191        let result = tool.cut(&clips, 0);
192        assert_eq!(result.cuts_applied(), 0);
193    }
194
195    #[test]
196    fn test_blade_tool_all_tracks_cut_multiple() {
197        let tool = BladeTool::new(BladeMode::AllTracks, 0);
198        let clips = vec![
199            (0usize, 10u64, 0i64, 200i64),
200            (1usize, 20u64, 0i64, 200i64),
201            (2usize, 30u64, 50i64, 150i64),
202        ];
203        let result = tool.cut(&clips, 100);
204        assert_eq!(result.cuts_applied(), 3);
205    }
206
207    #[test]
208    fn test_preview_cut_matches_cut() {
209        let tool = BladeTool::new(BladeMode::Single, 0);
210        let clips = vec![(0usize, 5u64, 10i64, 90i64)];
211        let preview = tool.preview_cut(&clips, 40);
212        let actual = tool.cut(&clips, 40).cuts;
213        assert_eq!(preview.len(), actual.len());
214    }
215
216    #[test]
217    fn test_blade_result_empty() {
218        let result = BladeResult::new(vec![]);
219        assert_eq!(result.cuts_applied(), 0);
220    }
221
222    #[test]
223    fn test_blade_tool_default_fields() {
224        let tool = BladeTool::new(BladeMode::Linked, 5);
225        assert_eq!(tool.snap_threshold, 5);
226        assert_eq!(tool.mode, BladeMode::Linked);
227    }
228}