Skip to main content

oximedia_edit/
group.rs

1//! Clip grouping and linking.
2//!
3//! Groups allow multiple clips to be treated as a single unit, while links
4//! maintain relationships between clips (e.g., video and audio from the same source).
5
6use std::collections::{HashMap, HashSet};
7
8use crate::clip::ClipId;
9use crate::error::{EditError, EditResult};
10
11/// Unique identifier for groups.
12pub type GroupId = u64;
13
14/// A group of clips that move together.
15#[derive(Clone, Debug)]
16pub struct ClipGroup {
17    /// Unique group identifier.
18    pub id: GroupId,
19    /// Clips in this group.
20    pub clips: HashSet<ClipId>,
21    /// Group name.
22    pub name: Option<String>,
23    /// Group color (for UI).
24    pub color: Option<[u8; 3]>,
25    /// Group is locked.
26    pub locked: bool,
27}
28
29impl ClipGroup {
30    /// Create a new group.
31    #[must_use]
32    pub fn new(id: GroupId) -> Self {
33        Self {
34            id,
35            clips: HashSet::new(),
36            name: None,
37            color: None,
38            locked: false,
39        }
40    }
41
42    /// Add a clip to the group.
43    pub fn add_clip(&mut self, clip_id: ClipId) {
44        self.clips.insert(clip_id);
45    }
46
47    /// Remove a clip from the group.
48    pub fn remove_clip(&mut self, clip_id: ClipId) -> bool {
49        self.clips.remove(&clip_id)
50    }
51
52    /// Check if group contains a clip.
53    #[must_use]
54    pub fn contains(&self, clip_id: ClipId) -> bool {
55        self.clips.contains(&clip_id)
56    }
57
58    /// Check if group is empty.
59    #[must_use]
60    pub fn is_empty(&self) -> bool {
61        self.clips.is_empty()
62    }
63
64    /// Get clip count.
65    #[must_use]
66    pub fn len(&self) -> usize {
67        self.clips.len()
68    }
69
70    /// Get all clip IDs.
71    #[must_use]
72    pub fn clip_ids(&self) -> Vec<ClipId> {
73        self.clips.iter().copied().collect()
74    }
75}
76
77/// Manager for clip groups.
78#[derive(Debug, Default)]
79pub struct GroupManager {
80    /// All groups.
81    groups: HashMap<GroupId, ClipGroup>,
82    /// Clip to group mapping.
83    clip_to_group: HashMap<ClipId, GroupId>,
84    /// Next group ID.
85    next_id: GroupId,
86}
87
88impl GroupManager {
89    /// Create a new group manager.
90    #[must_use]
91    pub fn new() -> Self {
92        Self {
93            groups: HashMap::new(),
94            clip_to_group: HashMap::new(),
95            next_id: 1,
96        }
97    }
98
99    /// Create a new group.
100    pub fn create_group(&mut self) -> GroupId {
101        let id = self.next_id;
102        self.next_id += 1;
103        self.groups.insert(id, ClipGroup::new(id));
104        id
105    }
106
107    /// Create a group with clips.
108    pub fn create_group_with_clips(&mut self, clips: Vec<ClipId>) -> EditResult<GroupId> {
109        let id = self.create_group();
110        for clip_id in clips {
111            self.add_to_group(id, clip_id)?;
112        }
113        Ok(id)
114    }
115
116    /// Delete a group.
117    pub fn delete_group(&mut self, group_id: GroupId) -> Option<ClipGroup> {
118        if let Some(group) = self.groups.remove(&group_id) {
119            // Remove clip mappings
120            for clip_id in &group.clips {
121                self.clip_to_group.remove(clip_id);
122            }
123            Some(group)
124        } else {
125            None
126        }
127    }
128
129    /// Add a clip to a group.
130    pub fn add_to_group(&mut self, group_id: GroupId, clip_id: ClipId) -> EditResult<()> {
131        // Check if clip is already in another group
132        if self.clip_to_group.contains_key(&clip_id) {
133            return Err(EditError::InvalidEdit(
134                "Clip already in a group".to_string(),
135            ));
136        }
137
138        let group = self
139            .groups
140            .get_mut(&group_id)
141            .ok_or_else(|| EditError::InvalidEdit("Group not found".to_string()))?;
142
143        group.add_clip(clip_id);
144        self.clip_to_group.insert(clip_id, group_id);
145
146        Ok(())
147    }
148
149    /// Remove a clip from its group.
150    pub fn remove_from_group(&mut self, clip_id: ClipId) -> Option<GroupId> {
151        if let Some(&group_id) = self.clip_to_group.get(&clip_id) {
152            if let Some(group) = self.groups.get_mut(&group_id) {
153                group.remove_clip(clip_id);
154                self.clip_to_group.remove(&clip_id);
155                return Some(group_id);
156            }
157        }
158        None
159    }
160
161    /// Get the group containing a clip.
162    #[must_use]
163    pub fn get_clip_group(&self, clip_id: ClipId) -> Option<&ClipGroup> {
164        self.clip_to_group
165            .get(&clip_id)
166            .and_then(|&group_id| self.groups.get(&group_id))
167    }
168
169    /// Get a group by ID.
170    #[must_use]
171    pub fn get_group(&self, group_id: GroupId) -> Option<&ClipGroup> {
172        self.groups.get(&group_id)
173    }
174
175    /// Get mutable group by ID.
176    pub fn get_group_mut(&mut self, group_id: GroupId) -> Option<&mut ClipGroup> {
177        self.groups.get_mut(&group_id)
178    }
179
180    /// Get all groups.
181    #[must_use]
182    pub fn all_groups(&self) -> Vec<&ClipGroup> {
183        self.groups.values().collect()
184    }
185
186    /// Check if a clip is grouped.
187    #[must_use]
188    pub fn is_grouped(&self, clip_id: ClipId) -> bool {
189        self.clip_to_group.contains_key(&clip_id)
190    }
191
192    /// Get all clips in the same group as a clip.
193    #[must_use]
194    pub fn get_group_members(&self, clip_id: ClipId) -> Vec<ClipId> {
195        self.get_clip_group(clip_id)
196            .map(ClipGroup::clip_ids)
197            .unwrap_or_default()
198    }
199
200    /// Clear all groups.
201    pub fn clear(&mut self) {
202        self.groups.clear();
203        self.clip_to_group.clear();
204    }
205
206    /// Get total group count.
207    #[must_use]
208    pub fn len(&self) -> usize {
209        self.groups.len()
210    }
211
212    /// Check if there are no groups.
213    #[must_use]
214    pub fn is_empty(&self) -> bool {
215        self.groups.is_empty()
216    }
217}
218
219/// Type of link between clips.
220#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum LinkType {
222    /// Video and audio from the same source.
223    VideoAudio,
224    /// Synchronized clips (move together).
225    Synchronized,
226    /// Parent-child relationship.
227    ParentChild,
228    /// Custom link.
229    Custom,
230}
231
232/// A link between two clips.
233#[derive(Clone, Debug)]
234pub struct ClipLink {
235    /// Source clip.
236    pub clip_a: ClipId,
237    /// Destination clip.
238    pub clip_b: ClipId,
239    /// Link type.
240    pub link_type: LinkType,
241    /// Link is active.
242    pub active: bool,
243}
244
245impl ClipLink {
246    /// Create a new link.
247    #[must_use]
248    pub fn new(clip_a: ClipId, clip_b: ClipId, link_type: LinkType) -> Self {
249        Self {
250            clip_a,
251            clip_b,
252            link_type,
253            active: true,
254        }
255    }
256
257    /// Check if link involves a clip.
258    #[must_use]
259    pub fn involves(&self, clip_id: ClipId) -> bool {
260        self.clip_a == clip_id || self.clip_b == clip_id
261    }
262
263    /// Get the other clip in the link.
264    #[must_use]
265    pub fn other_clip(&self, clip_id: ClipId) -> Option<ClipId> {
266        if self.clip_a == clip_id {
267            Some(self.clip_b)
268        } else if self.clip_b == clip_id {
269            Some(self.clip_a)
270        } else {
271            None
272        }
273    }
274}
275
276/// Manager for clip links.
277#[derive(Debug, Default)]
278pub struct LinkManager {
279    /// All links.
280    links: Vec<ClipLink>,
281    /// Clip to links mapping.
282    clip_links: HashMap<ClipId, Vec<usize>>,
283}
284
285impl LinkManager {
286    /// Create a new link manager.
287    #[must_use]
288    pub fn new() -> Self {
289        Self {
290            links: Vec::new(),
291            clip_links: HashMap::new(),
292        }
293    }
294
295    /// Add a link between two clips.
296    pub fn add_link(&mut self, clip_a: ClipId, clip_b: ClipId, link_type: LinkType) -> usize {
297        let index = self.links.len();
298        let link = ClipLink::new(clip_a, clip_b, link_type);
299
300        self.links.push(link);
301        self.clip_links.entry(clip_a).or_default().push(index);
302        self.clip_links.entry(clip_b).or_default().push(index);
303
304        index
305    }
306
307    /// Add a video-audio link.
308    pub fn link_video_audio(&mut self, video_clip: ClipId, audio_clip: ClipId) -> usize {
309        self.add_link(video_clip, audio_clip, LinkType::VideoAudio)
310    }
311
312    /// Remove a link by index.
313    pub fn remove_link(&mut self, index: usize) -> Option<ClipLink> {
314        if index >= self.links.len() {
315            return None;
316        }
317
318        let link = self.links.remove(index);
319
320        // Update clip_links mappings
321        self.clip_links.values_mut().for_each(|links| {
322            links.retain(|&i| i != index);
323            // Adjust indices for links after the removed one
324            for link_idx in links.iter_mut() {
325                if *link_idx > index {
326                    *link_idx -= 1;
327                }
328            }
329        });
330
331        Some(link)
332    }
333
334    /// Remove all links involving a clip.
335    pub fn remove_clip_links(&mut self, clip_id: ClipId) -> Vec<ClipLink> {
336        let link_indices: Vec<usize> = self.clip_links.get(&clip_id).cloned().unwrap_or_default();
337
338        let mut removed = Vec::new();
339        // Remove in reverse order to maintain indices
340        for &index in link_indices.iter().rev() {
341            if let Some(link) = self.remove_link(index) {
342                removed.push(link);
343            }
344        }
345
346        self.clip_links.remove(&clip_id);
347        removed
348    }
349
350    /// Get all links involving a clip.
351    #[must_use]
352    pub fn get_clip_links(&self, clip_id: ClipId) -> Vec<&ClipLink> {
353        self.clip_links
354            .get(&clip_id)
355            .map(|indices| indices.iter().filter_map(|&i| self.links.get(i)).collect())
356            .unwrap_or_default()
357    }
358
359    /// Get linked clips of a specific type.
360    #[must_use]
361    pub fn get_linked_clips(&self, clip_id: ClipId, link_type: LinkType) -> Vec<ClipId> {
362        self.get_clip_links(clip_id)
363            .into_iter()
364            .filter(|link| link.link_type == link_type && link.active)
365            .filter_map(|link| link.other_clip(clip_id))
366            .collect()
367    }
368
369    /// Check if two clips are linked.
370    #[must_use]
371    pub fn are_linked(&self, clip_a: ClipId, clip_b: ClipId) -> bool {
372        self.get_clip_links(clip_a)
373            .iter()
374            .any(|link| link.involves(clip_b))
375    }
376
377    /// Get all active links.
378    #[must_use]
379    pub fn active_links(&self) -> Vec<&ClipLink> {
380        self.links.iter().filter(|link| link.active).collect()
381    }
382
383    /// Clear all links.
384    pub fn clear(&mut self) {
385        self.links.clear();
386        self.clip_links.clear();
387    }
388
389    /// Get total link count.
390    #[must_use]
391    pub fn len(&self) -> usize {
392        self.links.len()
393    }
394
395    /// Check if there are no links.
396    #[must_use]
397    pub fn is_empty(&self) -> bool {
398        self.links.is_empty()
399    }
400}
401
402/// Compound clip - a clip that contains other clips.
403#[derive(Clone, Debug)]
404pub struct CompoundClip {
405    /// Unique identifier.
406    pub id: ClipId,
407    /// Nested clips.
408    pub clips: Vec<ClipId>,
409    /// Compound clip name.
410    pub name: String,
411    /// Duration (maximum end time of nested clips).
412    pub duration: i64,
413}
414
415impl CompoundClip {
416    /// Create a new compound clip.
417    #[must_use]
418    pub fn new(id: ClipId, name: String) -> Self {
419        Self {
420            id,
421            clips: Vec::new(),
422            name,
423            duration: 0,
424        }
425    }
426
427    /// Add a clip to the compound clip.
428    pub fn add_clip(&mut self, clip_id: ClipId) {
429        self.clips.push(clip_id);
430    }
431
432    /// Remove a clip from the compound clip.
433    pub fn remove_clip(&mut self, clip_id: ClipId) -> bool {
434        if let Some(pos) = self.clips.iter().position(|&id| id == clip_id) {
435            self.clips.remove(pos);
436            true
437        } else {
438            false
439        }
440    }
441
442    /// Check if compound clip contains a clip.
443    #[must_use]
444    pub fn contains(&self, clip_id: ClipId) -> bool {
445        self.clips.contains(&clip_id)
446    }
447
448    /// Check if compound clip is empty.
449    #[must_use]
450    pub fn is_empty(&self) -> bool {
451        self.clips.is_empty()
452    }
453
454    /// Get clip count.
455    #[must_use]
456    pub fn len(&self) -> usize {
457        self.clips.len()
458    }
459}
460
461/// Manager for compound clips.
462#[derive(Debug, Default)]
463pub struct CompoundClipManager {
464    /// All compound clips.
465    compounds: HashMap<ClipId, CompoundClip>,
466    /// Clip to compound mapping.
467    clip_to_compound: HashMap<ClipId, ClipId>,
468}
469
470impl CompoundClipManager {
471    /// Create a new compound clip manager.
472    #[must_use]
473    pub fn new() -> Self {
474        Self {
475            compounds: HashMap::new(),
476            clip_to_compound: HashMap::new(),
477        }
478    }
479
480    /// Create a compound clip.
481    pub fn create(&mut self, id: ClipId, name: String) -> ClipId {
482        let compound = CompoundClip::new(id, name);
483        self.compounds.insert(id, compound);
484        id
485    }
486
487    /// Delete a compound clip.
488    pub fn delete(&mut self, id: ClipId) -> Option<CompoundClip> {
489        if let Some(compound) = self.compounds.remove(&id) {
490            // Remove clip mappings
491            for &clip_id in &compound.clips {
492                self.clip_to_compound.remove(&clip_id);
493            }
494            Some(compound)
495        } else {
496            None
497        }
498    }
499
500    /// Add a clip to a compound clip.
501    pub fn add_to_compound(&mut self, compound_id: ClipId, clip_id: ClipId) -> EditResult<()> {
502        // Check if clip is already in another compound
503        if self.clip_to_compound.contains_key(&clip_id) {
504            return Err(EditError::InvalidEdit(
505                "Clip already in a compound".to_string(),
506            ));
507        }
508
509        let compound = self
510            .compounds
511            .get_mut(&compound_id)
512            .ok_or_else(|| EditError::InvalidEdit("Compound clip not found".to_string()))?;
513
514        compound.add_clip(clip_id);
515        self.clip_to_compound.insert(clip_id, compound_id);
516
517        Ok(())
518    }
519
520    /// Remove a clip from its compound.
521    pub fn remove_from_compound(&mut self, clip_id: ClipId) -> Option<ClipId> {
522        if let Some(&compound_id) = self.clip_to_compound.get(&clip_id) {
523            if let Some(compound) = self.compounds.get_mut(&compound_id) {
524                compound.remove_clip(clip_id);
525                self.clip_to_compound.remove(&clip_id);
526                return Some(compound_id);
527            }
528        }
529        None
530    }
531
532    /// Get the compound containing a clip.
533    #[must_use]
534    pub fn get_compound_for_clip(&self, clip_id: ClipId) -> Option<&CompoundClip> {
535        self.clip_to_compound
536            .get(&clip_id)
537            .and_then(|&compound_id| self.compounds.get(&compound_id))
538    }
539
540    /// Get a compound clip by ID.
541    #[must_use]
542    pub fn get(&self, id: ClipId) -> Option<&CompoundClip> {
543        self.compounds.get(&id)
544    }
545
546    /// Get mutable compound clip by ID.
547    pub fn get_mut(&mut self, id: ClipId) -> Option<&mut CompoundClip> {
548        self.compounds.get_mut(&id)
549    }
550
551    /// Get all compound clips.
552    #[must_use]
553    pub fn all(&self) -> Vec<&CompoundClip> {
554        self.compounds.values().collect()
555    }
556
557    /// Clear all compound clips.
558    pub fn clear(&mut self) {
559        self.compounds.clear();
560        self.clip_to_compound.clear();
561    }
562
563    /// Get total compound clip count.
564    #[must_use]
565    pub fn len(&self) -> usize {
566        self.compounds.len()
567    }
568
569    /// Check if there are no compound clips.
570    #[must_use]
571    pub fn is_empty(&self) -> bool {
572        self.compounds.is_empty()
573    }
574}