1use crate::metadata::{item_link_group_id, item_tellers_group_id, resolve_tellers_group_id};
2use crate::{
3 Clip, Gap, IdMetadataExt, InsertPolicy, Item, OverlapPolicy, Seconds, Stack, Track, TrackKind,
4 TrackInsertResult,
5};
6use std::collections::{HashMap, HashSet};
7
8mod stack_insert_propagate;
9mod stack_item_delete;
10mod stack_item_get;
11mod stack_item_insert;
12mod stack_item_link;
13mod stack_item_move;
14mod stack_item_replace;
15mod stack_item_split;
16mod stack_partner_tracks;
17#[cfg(test)]
18mod stack_partner_move_tests;
19mod stack_ripple;
20mod stack_snap;
21mod stack_track;
22
23pub use stack_snap::{SnapEdge, SnapMatch, SnapTarget};
24
25use stack_item_split::SyncSplitIdPolicy;
26
27const EPS: Seconds = 1e-9;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct SyncedInsertResult {
31 pub primary_clip_id: String,
32 pub audio_clips: Vec<(String, usize)>,
33 pub synced_video_clip_id: Option<String>,
34 pub sync_clips_id: Option<i64>,
35 pub created_track_indices: Vec<usize>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum InsertItemAtTimeResult {
40 ItemId(String),
41 Synced(SyncedInsertResult),
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SyncTrackInfo {
46 pub track_indices: Vec<usize>,
47 pub track_ids: Vec<Option<String>>,
48}
49
50#[derive(Debug, Clone)]
51struct SyncedInputs {
52 audio: Vec<Item>,
53 video: Option<Item>,
54}
55
56#[derive(Debug, Clone)]
57struct SyncedMoveItem {
58 track_index: usize,
59 item_index: usize,
60 track_kind: TrackKind,
61 item: Item,
62 is_selected: bool,
63}
64
65#[derive(Debug, Clone)]
66struct SyncedClipState {
67 track_index: usize,
68 start: Seconds,
69 duration: Seconds,
70 source_start: Seconds,
71 sync_clips_id: i64,
72}
73
74#[derive(Debug, Clone)]
75struct FlattenedBoundary {
76 track_indices: Vec<usize>,
77}
78
79fn shift_track_index_after_insert(track_index: &mut usize, inserted_track_index: usize) {
80 if inserted_track_index <= *track_index {
81 *track_index += 1;
82 }
83}
84
85fn shift_track_indices_after_insert(track_indices: &mut [usize], inserted_track_index: usize) {
86 for track_index in track_indices {
87 shift_track_index_after_insert(track_index, inserted_track_index);
88 }
89}
90
91fn shift_move_placements_after_insert(
92 placements: &mut [(usize, Item, bool)],
93 inserted_track_index: usize,
94) {
95 for (track_index, _, _) in placements {
96 shift_track_index_after_insert(track_index, inserted_track_index);
97 }
98}
99
100fn clamp_insertion_index(len: usize, index: isize) -> usize {
101 if index < 0 {
102 let pos = len as isize + index;
103 if pos <= 0 {
104 0
105 } else if pos >= len as isize {
106 len
107 } else {
108 pos as usize
109 }
110 } else {
111 let idx = index as usize;
112 if idx > len {
113 len
114 } else {
115 idx
116 }
117 }
118}
119
120impl Stack {
121 fn collect_timeline_ids(&self) -> HashSet<String> {
122 let mut ids = HashSet::new();
123 for track in &self.children {
124 if let Some(id) = track.get_id() {
125 if !id.is_empty() {
126 ids.insert(id);
127 }
128 }
129 for item in &track.items {
130 if let Some(id) = item.get_id() {
131 if !id.is_empty() {
132 ids.insert(id);
133 }
134 }
135 }
136 }
137 ids
138 }
139
140 fn ensure_unique_item_id(item: &mut Item, used_ids: &mut HashSet<String>) -> String {
141 let current = item.get_id().filter(|id| !id.is_empty());
142 if let Some(id) = current {
143 if used_ids.insert(id.clone()) {
144 return id;
145 }
146 }
147
148 loop {
149 let id = crate::types::gen_hex_id_12();
150 if used_ids.insert(id.clone()) {
151 item.set_id(Some(id.clone()));
152 return id;
153 }
154 }
155 }
156
157 fn next_sync_clips_id(&self) -> i64 {
158 self.children
159 .iter()
160 .flat_map(|track| track.items.iter())
161 .filter_map(|item| match item {
162 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
163 Item::Gap(_) => None,
164 })
165 .max()
166 .unwrap_or(0)
167 + 1
168 }
169
170 fn next_tellers_group_id(&self) -> i64 {
171 self.children
172 .iter()
173 .flat_map(|track| track.items.iter())
174 .filter_map(|item| match item {
175 Item::Clip(clip) => resolve_tellers_group_id(&clip.metadata),
176 Item::Gap(_) => None,
177 })
178 .max()
179 .unwrap_or(0)
180 + 1
181 }
182
183 fn insert_gap_only(&mut self, track_index: usize, dest_time: Seconds, item: Item) -> bool {
184 if track_index >= self.children.len() || item.duration() <= EPS {
185 return false;
186 }
187
188 let duration = item.duration().max(0.0);
189 let mut start = dest_time;
190 let total = self.children[track_index].total_duration();
191 if start < 0.0 {
192 start = total - start;
193 }
194 if start < 0.0 {
195 return false;
196 }
197 let end = start + duration;
198
199 if start >= total - EPS {
200 self.children[track_index].insert_at_time(
201 start,
202 item,
203 OverlapPolicy::Override,
204 InsertPolicy::InsertBefore,
205 );
206 return true;
207 }
208
209 if !range_is_gap_backed(&self.children[track_index], start, end) {
210 return false;
211 }
212
213 let track = &mut self.children[track_index];
214 split_gap_boundary(track, end);
215 split_gap_boundary(track, start);
216
217 self.children[track_index].insert_at_time(
218 start,
219 item,
220 OverlapPolicy::Override,
221 InsertPolicy::InsertBefore,
222 );
223 true
224 }
225
226 fn apply_sync_splits_for_column_insert(
227 &mut self,
228 start: Seconds,
229 duration: Seconds,
230 insert_policy: InsertPolicy,
231 overlap_policy: OverlapPolicy,
232 id_policy: SyncSplitIdPolicy,
233 scope_track_indices: Option<&[usize]>,
234 ) -> bool {
235 let mut split_times = Vec::new();
236 if matches!(insert_policy, InsertPolicy::SplitAndInsert) {
237 split_times.push(start);
238 }
239 if overlap_policy == OverlapPolicy::Override && duration > EPS {
240 if !split_times
241 .iter()
242 .any(|time| (*time - start).abs() <= EPS)
243 {
244 split_times.push(start);
245 }
246 if matches!(id_policy, SyncSplitIdPolicy::KeepShared) {
247 split_times.push(start + duration);
248 }
249 }
250 split_times.sort_by(|left, right| left.partial_cmp(right).unwrap_or(std::cmp::Ordering::Equal));
251 split_times.dedup_by(|left, right| (*left - *right).abs() <= EPS);
252 for split_time in split_times {
253 if !self.split_sync_clips_at_time(split_time, id_policy, scope_track_indices) {
254 return false;
255 }
256 }
257 true
258 }
259
260 fn insert_at_time_with_sync_splits(
261 &mut self,
262 track_index: usize,
263 insert_time: Seconds,
264 item: Item,
265 overlap_policy: OverlapPolicy,
266 insert_policy: InsertPolicy,
267 ) {
268 if overlap_policy == OverlapPolicy::Push {
269 let _ = self.children[track_index].insert_at_time(
270 insert_time,
271 item,
272 overlap_policy,
273 insert_policy,
274 );
275 return;
276 }
277
278 let prefer_cluster_with_video = self.children.get(track_index).is_some_and(|track| {
279 track.kind == TrackKind::Video
280 });
281 let cluster = self.preferred_cluster_indices(track_index, prefer_cluster_with_video);
282 let start = insertion_start_or_end_for_policy(
283 &self.children[track_index],
284 insert_time,
285 insert_policy,
286 )
287 .unwrap_or(insert_time);
288 let duration = item.duration().max(0.0);
289
290 let primary_result = self.children[track_index].insert_at_time(
291 insert_time,
292 item,
293 overlap_policy,
294 insert_policy,
295 );
296 if primary_result.success {
297 let updates = [(track_index, &primary_result)];
298 let _ = self.propagate_insert_to_cluster(
299 start,
300 duration,
301 overlap_policy,
302 &updates,
303 &cluster,
304 None,
305 );
306 }
307 }
308
309 fn nearest_free_video_tracks(
312 &self,
313 dest_track_index: usize,
314 start: Seconds,
315 end: Seconds,
316 exclude: &[usize],
317 ) -> Vec<usize> {
318 self.nearest_free_tracks_of_kind(TrackKind::Video, dest_track_index, start, end, exclude)
319 }
320
321 pub(super) fn nearest_free_tracks_of_kind(
322 &self,
323 kind: TrackKind,
324 dest_track_index: usize,
325 start: Seconds,
326 end: Seconds,
327 exclude: &[usize],
328 ) -> Vec<usize> {
329 let mut candidates: Vec<usize> = self
330 .children
331 .iter()
332 .enumerate()
333 .filter(|(track_index, track)| {
334 *track_index != dest_track_index
335 && track.kind == kind
336 && !exclude.contains(track_index)
337 && (track_is_empty_boundary(track) || range_is_gap_backed(track, start, end))
338 })
339 .map(|(track_index, _)| track_index)
340 .collect();
341 candidates.sort_by_key(|&track_index| {
342 (
343 !self.track_is_free(track_index),
344 track_index.abs_diff(dest_track_index),
345 track_index,
346 )
347 });
348 candidates
349 }
350
351 pub(crate) fn track_range_is_free(track: &Track, start: Seconds, end: Seconds) -> bool {
354 track_is_empty_boundary(track) || range_is_gap_backed(track, start, end)
355 }
356
357 pub(super) fn try_reuse_video_track_for_audio_move(
358 &self,
359 track_index: usize,
360 audio_track_index: usize,
361 dest_time: Seconds,
362 end_time: Seconds,
363 sync_clips_id: Option<i64>,
364 use_sync_backed_track: bool,
365 overlap_policy: OverlapPolicy,
366 insert_policy: InsertPolicy,
367 ) -> Option<usize> {
368 if self.children.get(track_index)?.kind != TrackKind::Video {
369 return None;
370 }
371 if range_is_gap_backed(&self.children[track_index], dest_time, end_time) {
372 return Some(track_index);
373 }
374 let has_blocking_clip = range_has_blocking_clip(
375 &self.children[track_index],
376 dest_time,
377 end_time,
378 sync_clips_id,
379 );
380 let can_push_existing_boundary = overlap_policy == OverlapPolicy::Push
381 && self.track_matches_primary_sync_boundary(audio_track_index, track_index);
382 let will_split_for_override = overlap_policy == OverlapPolicy::Override
386 && matches!(insert_policy, InsertPolicy::SplitAndInsert);
387 if !has_blocking_clip || can_push_existing_boundary || will_split_for_override {
388 return use_sync_backed_track.then_some(track_index);
389 }
390 None
391 }
392
393 pub(super) fn find_or_create_video_track_for_audio(
394 &mut self,
395 audio_track_index: usize,
396 dest_time: Seconds,
397 duration: Seconds,
398 created_track_indices: &mut Vec<usize>,
399 sync_clips_id: Option<i64>,
400 use_sync_backed_track: bool,
401 overlap_policy: OverlapPolicy,
402 insert_policy: InsertPolicy,
403 ) -> Option<usize> {
404 let end_time = dest_time + duration;
405
406 let cluster = self.boundary_group_indices(audio_track_index);
410 let mut cluster_video_tracks: Vec<usize> = cluster
411 .iter()
412 .copied()
413 .filter(|&track_index| {
414 self.children
415 .get(track_index)
416 .is_some_and(|track| track.kind == TrackKind::Video)
417 })
418 .collect();
419 cluster_video_tracks.sort_by_key(|track_index| {
422 if *track_index > audio_track_index {
423 0
424 } else {
425 1
426 }
427 });
428 for track_index in cluster_video_tracks {
429 if let Some(track_index) = self.try_reuse_video_track_for_audio_move(
430 track_index,
431 audio_track_index,
432 dest_time,
433 end_time,
434 sync_clips_id,
435 use_sync_backed_track,
436 overlap_policy,
437 insert_policy,
438 ) {
439 return Some(track_index);
440 }
441 }
442
443 if let Some(track_index) = self
446 .nearest_free_video_tracks(audio_track_index, dest_time, end_time, &[])
447 .into_iter()
448 .next()
449 {
450 return Some(track_index);
451 }
452
453 let mut audio_end = audio_track_index + 1;
454 while audio_end < self.children.len() && self.children[audio_end].kind == TrackKind::Audio {
455 audio_end += 1;
456 }
457
458 let insert_at = audio_end;
460 let track = self.new_numbered_track(TrackKind::Video);
461 self.children.insert(insert_at, track);
462 created_track_indices.push(insert_at);
463 Some(insert_at)
464 }
465
466 fn item_occupies_column(
467 &self,
468 track_index: usize,
469 item_index: usize,
470 column_start: Seconds,
471 column_duration: Seconds,
472 ) -> bool {
473 let Some(track) = self.children.get(track_index) else {
474 return false;
475 };
476 if item_index >= track.items.len() {
477 return false;
478 }
479 (track.start_time_of_item(item_index) - column_start).abs() <= EPS
480 && (track.items[item_index].duration() - column_duration).abs() <= EPS
481 }
482
483 fn delete_clips_at_indices(
484 &mut self,
485 mut targets: Vec<(usize, usize)>,
486 replace_with_gap: bool,
487 ) -> Vec<(usize, Item)> {
488 targets.sort_by(|a, b| b.cmp(a));
489 targets.dedup();
490 let mut removed = Vec::new();
491 let mut used_ids = replace_with_gap.then(|| self.collect_timeline_ids());
492 for (ti, ii) in targets {
493 if ti >= self.children.len() {
494 continue;
495 }
496 let track = &mut self.children[ti];
497 if ii >= track.items.len() {
498 continue;
499 }
500 if matches!(track.items[ii], Item::Gap(_)) && replace_with_gap {
501 continue;
502 }
503 let start = track.start_time_of_item(ii);
504 let end = start + track.items[ii].duration().max(0.0);
505 let mut removed_items = track.delete_range(start, end, replace_with_gap);
506 let Some(removed_item) = removed_items.pop() else {
507 continue;
508 };
509 if replace_with_gap {
510 if let (Some(used_ids), Some(item)) = (
511 &mut used_ids,
512 track
513 .get_item_at_time(start)
514 .and_then(|gap_index| track.items.get_mut(gap_index)),
515 ) {
516 if matches!(item, Item::Gap(_)) {
517 Self::ensure_unique_item_id(item, used_ids);
518 }
519 }
520 }
521 removed.push((ti, removed_item));
522 }
523 removed.reverse();
524 removed
525 }
526
527 fn delete_item_targets(&self, item_id: &str) -> Option<Vec<(usize, usize)>> {
528 let (track_index, item_index, item) = self.get_item(item_id)?;
529 if matches!(item, Item::Gap(_)) {
530 return None;
531 }
532 let sync_clips_id = match &item {
533 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
534 Item::Gap(_) => None,
535 };
536 if let Some(sync_id) = sync_clips_id {
537 Some(self.synced_clips_targets(sync_id))
538 } else {
539 Some(vec![(track_index, item_index)])
540 }
541 }
542
543 fn delete_item_group_targets(
550 &self,
551 item_id: &str,
552 with_sync_clips: bool,
553 ) -> Option<Vec<(usize, usize)>> {
554 let mut targets = if with_sync_clips {
555 self.delete_item_targets(item_id)?
556 } else {
557 vec![self.clip_target(item_id)?]
558 };
559 let (_, _, item) = self.get_item(item_id)?;
560 if let Item::Clip(clip) = item {
561 if let Some(group_id) = resolve_tellers_group_id(&clip.metadata) {
562 for member in self.tellers_group_targets(group_id) {
563 targets.push(member);
564 if !with_sync_clips {
565 continue;
566 }
567 if let Some(Item::Clip(member_clip)) = self
568 .children
569 .get(member.0)
570 .and_then(|track| track.items.get(member.1))
571 {
572 if let Some(sync_id) = resolve_sync_clips_id(&member_clip.metadata) {
573 targets.extend(self.synced_clips_targets(sync_id));
574 }
575 }
576 }
577 }
578 }
579 Some(targets)
580 }
581
582 fn delete_item_replace_with_gap(
583 &mut self,
584 item_id: &str,
585 with_sync_clips: bool,
586 ) -> Vec<(usize, Item)> {
587 let Some(targets) = self.delete_item_group_targets(item_id, with_sync_clips) else {
588 return Vec::new();
589 };
590 let removed = self.delete_clips_at_indices(targets, true);
591 if !removed.is_empty() {
592 self.sanitize();
593 }
594 removed
595 }
596
597 fn sync_groups_behind_clips(&self, clips_to_delete: &[(usize, usize)]) -> HashSet<i64> {
598 let mut sync_groups = HashSet::new();
599 for &(ti, ii) in clips_to_delete {
600 let Some(track) = self.children.get(ti) else {
601 continue;
602 };
603 for item in track.items.iter().skip(ii + 1) {
604 if let Item::Clip(clip) = item {
605 if let Some(sync_id) = resolve_sync_clips_id(&clip.metadata) {
606 sync_groups.insert(sync_id);
607 }
608 }
609 }
610 }
611 sync_groups
612 }
613
614 fn collapse_mutation_tracks(
615 &self,
616 clips_to_delete: &[(usize, usize)],
617 sync_groups_behind: &HashSet<i64>,
618 ) -> HashSet<usize> {
619 let mut tracks: HashSet<usize> = clips_to_delete.iter().map(|(ti, _)| *ti).collect();
620 for sync_id in sync_groups_behind {
621 for (ti, _) in self.synced_clips_targets(*sync_id) {
622 tracks.insert(ti);
623 }
624 }
625 tracks
626 }
627
628 fn delete_item_collapse(&mut self, item_id: &str) -> Vec<(usize, Item)> {
629 let Some((track_index, item_index, item)) = self.get_item(item_id) else {
630 return Vec::new();
631 };
632
633 if matches!(item, Item::Gap(_)) {
634 let removed = self.delete_clips_at_indices(vec![(track_index, item_index)], false);
635 if !removed.is_empty() {
636 self.sanitize();
637 }
638 return removed;
639 }
640
641 let Some(clips_to_delete) = self.delete_item_group_targets(item_id, true) else {
642 return Vec::new();
643 };
644 if clips_to_delete.is_empty() {
645 return Vec::new();
646 }
647
648 let column_start = self.children[track_index].start_time_of_item(item_index);
649 let column_end = column_start + item.duration().max(0.0);
650 let sync_groups_behind = self.sync_groups_behind_clips(&clips_to_delete);
651 let tracks_to_mutate =
652 self.collapse_mutation_tracks(&clips_to_delete, &sync_groups_behind);
653
654 let mut removed = self.delete_clips_at_indices(clips_to_delete, false);
655 let mutated_tracks: HashSet<usize> = removed.iter().map(|(ti, _)| *ti).collect();
656
657 let mut pending_range_tracks: Vec<_> = tracks_to_mutate
658 .into_iter()
659 .filter(|ti| !mutated_tracks.contains(ti))
660 .collect();
661 pending_range_tracks.sort_unstable();
662 pending_range_tracks.dedup();
663
664 for ti in pending_range_tracks {
665 let Some(track) = self.children.get_mut(ti) else {
666 continue;
667 };
668 for removed_item in track.delete_range(column_start, column_end, false) {
669 removed.push((ti, removed_item));
670 }
671 }
672
673 if !removed.is_empty() {
674 self.sanitize();
675 }
676 removed
677 }
678
679 fn delete_sync_clips(
680 &mut self,
681 sync_clips_id: i64,
682 replace_with_gap: bool,
683 ) -> Vec<(usize, Item)> {
684 self.delete_clips_at_indices(self.synced_clips_targets(sync_clips_id), replace_with_gap)
685 }
686
687 fn clip_target(&self, item_id: &str) -> Option<(usize, usize)> {
688 let (track_index, item_index, item) = self.get_item(item_id)?;
689 matches!(item, Item::Clip(_)).then_some((track_index, item_index))
690 }
691
692 fn synced_clips_targets(&self, sync_clips_id: i64) -> Vec<(usize, usize)> {
693 let mut targets = Vec::new();
694 for (ti, track) in self.children.iter().enumerate() {
695 for (ii, item) in track.items.iter().enumerate() {
696 if let Item::Clip(clip) = item {
697 if resolve_sync_clips_id(&clip.metadata) == Some(sync_clips_id) {
698 targets.push((ti, ii));
699 }
700 }
701 }
702 }
703 targets
704 }
705
706 fn tellers_group_targets(&self, group_id: i64) -> Vec<(usize, usize)> {
707 let mut targets = Vec::new();
708 for (ti, track) in self.children.iter().enumerate() {
709 for (ii, item) in track.items.iter().enumerate() {
710 if let Item::Clip(clip) = item {
711 if resolve_tellers_group_id(&clip.metadata) == Some(group_id) {
712 targets.push((ti, ii));
713 }
714 }
715 }
716 }
717 targets
718 }
719
720 fn tellers_group_move_plan(
732 &self,
733 item_id: &str,
734 dest_track_id: &str,
735 dest_time: Seconds,
736 ) -> Option<Vec<(String, String, Seconds)>> {
737 let (selected_track_index, selected_item_index, selected_item) = self.get_item(item_id)?;
738 item_tellers_group_id(selected_item)?;
739 let selected_start =
740 self.children[selected_track_index].start_time_of_item(selected_item_index);
741 let delta = dest_time - selected_start;
742
743 let column_ids = |id: &str| -> Vec<String> {
744 self.synced_move_items(id)
745 .map(|items| items.into_iter().filter_map(|m| m.item.get_id()).collect())
746 .unwrap_or_else(|| vec![id.to_string()])
747 };
748
749 let mut moves = vec![(
752 item_id.to_string(),
753 dest_track_id.to_string(),
754 dest_time,
755 )];
756 let mut seen_ids: HashSet<String> = column_ids(item_id).into_iter().collect();
757 for (track_index, item_index) in self.delete_item_group_targets(item_id, true)? {
758 let Some(Item::Clip(clip)) = self
759 .children
760 .get(track_index)
761 .and_then(|track| track.items.get(item_index))
762 else {
763 continue;
764 };
765 let Some(rep_id) = clip.get_id() else {
766 continue;
767 };
768 if seen_ids.contains(&rep_id) {
769 continue;
770 }
771 seen_ids.extend(column_ids(&rep_id));
772 let Some(track_id) = self.children[track_index].get_id() else {
773 continue;
774 };
775 let rep_start = self.children[track_index].start_time_of_item(item_index);
776 moves.push((rep_id, track_id, rep_start + delta));
777 }
778
779 Some(moves)
780 }
781
782 fn sync_column_targets_at(
783 &self,
784 sync_clips_id: i64,
785 column_start: Seconds,
786 column_duration: Seconds,
787 ) -> Vec<(usize, usize)> {
788 self.synced_clips_targets(sync_clips_id)
789 .into_iter()
790 .filter(|(track_index, item_index)| {
791 self.item_occupies_column(*track_index, *item_index, column_start, column_duration)
792 })
793 .collect()
794 }
795
796 fn is_independent_sync_column(
797 &self,
798 sync_clips_id: i64,
799 column_start: Seconds,
800 column_duration: Seconds,
801 ) -> bool {
802 self.sync_column_targets_at(sync_clips_id, column_start, column_duration)
803 .len()
804 > 1
805 }
806
807 fn item_is_unsynced(item: &Item) -> bool {
808 match item {
809 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata).is_none(),
810 Item::Gap(_) => true,
811 }
812 }
813
814 fn is_cluster_column_at(
817 &self,
818 cluster: &[usize],
819 column_start: Seconds,
820 duration: Seconds,
821 ) -> bool {
822 cluster.len() > 1
823 && cluster.iter().all(|&track_index| {
824 let Some(track) = self.children.get(track_index) else {
825 return false;
826 };
827 let Some(item_index) = track.get_item_at_time(column_start) else {
828 return false;
829 };
830 (track.start_time_of_item(item_index) - column_start).abs() <= EPS
831 && Self::item_is_unsynced(&track.items[item_index])
832 && (track.items[item_index].duration() - duration).abs() <= EPS
833 })
834 }
835
836 fn apply_cluster_column_duration_change(
839 &mut self,
840 item_id: &str,
841 cluster: &[usize],
842 column_start: Seconds,
843 old_duration: Seconds,
844 new_duration: Seconds,
845 source_start_time: Seconds,
846 clamp_to_media: bool,
847 ) -> bool {
848 for &track_index in cluster {
849 let Some(track) = self.children.get_mut(track_index) else {
850 return false;
851 };
852 let Some(item_index) = track.get_item_at_time(column_start) else {
853 return false;
854 };
855 if (track.start_time_of_item(item_index) - column_start).abs() > EPS {
856 return false;
857 }
858 let item = &mut track.items[item_index];
859 if !Self::item_is_unsynced(item) {
860 return false;
861 }
862 if (item.duration() - old_duration).abs() > EPS {
863 return false;
864 }
865 if item.get_id().as_deref() == Some(item_id) {
866 if let Item::Clip(clip) = item {
867 let _ = clip;
868 set_item_source_start(item, source_start_time);
869 }
870 }
871 item.set_duration(new_duration);
872 if clamp_to_media {
873 item.clamp_to_active_available_range();
874 }
875 track.sanitize_preserving_all_gap_track();
876 }
877
878 true
879 }
880
881 fn synced_clip_states(&self) -> HashMap<String, SyncedClipState> {
882 let mut states = HashMap::new();
883 for (track_index, track) in self.children.iter().enumerate() {
884 for (item_index, item) in track.items.iter().enumerate() {
885 let Item::Clip(clip) = item else {
886 continue;
887 };
888 let Some(sync_clips_id) = resolve_sync_clips_id(&clip.metadata) else {
889 continue;
890 };
891 let Some(id) = item.get_id() else {
892 continue;
893 };
894 states.insert(
895 id,
896 SyncedClipState {
897 track_index,
898 start: track.start_time_of_item(item_index),
899 duration: item.duration().max(0.0),
900 source_start: item_source_start(item),
901 sync_clips_id,
902 },
903 );
904 }
905 }
906 states
907 }
908
909 fn cleanup_singleton_sync_clips(&mut self, sync_clips_ids: &[i64]) -> usize {
910 let mut count = 0;
911 let mut seen = HashSet::new();
912 for sync_clips_id in sync_clips_ids {
913 if !seen.insert(*sync_clips_id) {
914 continue;
915 }
916 let targets = self.synced_clips_targets(*sync_clips_id);
917 if targets.len() > 1 {
918 continue;
919 }
920 for (track_index, item_index) in targets {
921 let Some(Item::Clip(clip)) = self
922 .children
923 .get_mut(track_index)
924 .and_then(|track| track.items.get_mut(item_index))
925 else {
926 continue;
927 };
928 if remove_resolve_sync_clips_id(&mut clip.metadata) {
929 count += 1;
930 }
931 }
932 }
933 count
934 }
935
936 fn set_item_sync_clips(item: &mut Item, sync_clips_id: Option<i64>) {
937 let Item::Clip(clip) = item else {
938 return;
939 };
940 if let Some(sync_clips_id) = sync_clips_id {
941 set_resolve_sync_clips_id(&mut clip.metadata, sync_clips_id);
942 } else {
943 remove_resolve_sync_clips_id(&mut clip.metadata);
944 }
945 }
946
947 pub(super) fn new_numbered_track(&self, kind: TrackKind) -> Track {
948 let prefix = match kind {
949 TrackKind::Audio => "A",
950 TrackKind::Video => "V",
951 TrackKind::Other => "T",
952 };
953 let used: HashSet<_> = self
954 .children
955 .iter()
956 .flat_map(|track| [track.get_id(), track.name.clone()])
957 .flatten()
958 .collect();
959 let id = (1..=99)
960 .map(|index| format!("{prefix}{index}"))
961 .find(|candidate| !used.contains(candidate))
962 .unwrap_or_else(crate::types::gen_hex_id_12);
963 let mut track = Track::new(kind, Some(id.clone()));
964 track.name = Some(id);
965 track.set_auto_created(true);
966 track
967 }
968
969 fn prepare_synced_item(
970 item: Item,
971 duration: Seconds,
972 sync_clips_id: Option<i64>,
973 used_ids: &mut HashSet<String>,
974 ) -> Option<(Item, String)> {
975 let Item::Clip(mut clip) = item else {
976 return None;
977 };
978 clip.source_range.duration.set_from_seconds(duration);
979 clamp_clip_to_active_available_range(&mut clip);
980 let clamped_duration = clip.source_range.duration.to_seconds();
981 if clamped_duration + EPS < duration || clamped_duration <= EPS
982 {
983 return None;
984 }
985
986 let mut item = Item::Clip(clip);
987 Self::set_item_sync_clips(&mut item, sync_clips_id);
988 let id = Self::ensure_unique_item_id(&mut item, used_ids);
989 Some((item, id))
990 }
991
992 fn prepare_synced_item_preserve_duration(
993 item: Item,
994 sync_clips_id: Option<i64>,
995 used_ids: &mut HashSet<String>,
996 ) -> Option<(Item, String)> {
997 let Item::Clip(mut clip) = item else {
998 return None;
999 };
1000 clamp_clip_to_active_available_range(&mut clip);
1001 if clip.source_range.duration.to_seconds() <= EPS {
1002 return None;
1003 }
1004
1005 let mut item = Item::Clip(clip);
1006 Self::set_item_sync_clips(&mut item, sync_clips_id);
1007 let id = Self::ensure_unique_item_id(&mut item, used_ids);
1008 Some((item, id))
1009 }
1010
1011 fn synced_insert_column_span(primary_duration: Seconds, inputs: &SyncedInputs) -> Seconds {
1012 let mut span = primary_duration;
1013 for item in &inputs.audio {
1014 if let Some(duration) = Self::sanitized_clip_duration(item) {
1015 span = span.max(duration);
1016 }
1017 }
1018 if let Some(video_item) = &inputs.video {
1019 if let Some(duration) = Self::sanitized_clip_duration(video_item) {
1020 span = span.max(duration);
1021 }
1022 }
1023 span
1024 }
1025
1026 fn sanitized_clip_duration(item: &Item) -> Option<Seconds> {
1027 let Item::Clip(mut clip) = item.clone() else {
1028 return None;
1029 };
1030 clamp_clip_to_active_available_range(&mut clip);
1031 let duration = clip.source_range.duration.to_seconds().max(0.0);
1032 (duration > EPS).then_some(duration)
1033 }
1034
1035 fn synced_inputs_match_duration(duration: Seconds, inputs: &SyncedInputs) -> bool {
1036 for audio_item in &inputs.audio {
1037 let Some(audio_duration) = Self::sanitized_clip_duration(audio_item) else {
1038 return false;
1039 };
1040 if (audio_duration - duration).abs() > EPS {
1041 return false;
1042 }
1043 }
1044 if let Some(video_item) = &inputs.video {
1045 let Some(video_duration) = Self::sanitized_clip_duration(video_item) else {
1046 return false;
1047 };
1048 if (video_duration - duration).abs() > EPS {
1049 return false;
1050 }
1051 }
1052
1053 true
1054 }
1055
1056 fn normalize_synced_inputs(
1057 synced_audio_clips: Option<Vec<Item>>,
1058 synced_video_clip: Option<Item>,
1059 ) -> SyncedInputs {
1060 let mut inputs = SyncedInputs {
1061 audio: synced_audio_clips.unwrap_or_default(),
1062 video: synced_video_clip,
1063 };
1064 for item in &mut inputs.audio {
1065 item.clamp_to_active_available_range();
1066 }
1067 if let Some(video) = &mut inputs.video {
1068 video.clamp_to_active_available_range();
1069 }
1070 inputs
1071 }
1072
1073 pub(super) fn shift_insert_track_indices_after_create(
1074 inserted_at: usize,
1075 dest_track_index: &mut usize,
1076 cluster: &mut [usize],
1077 audio_slots: &mut [usize],
1078 created_track_indices: &mut [usize],
1079 ) {
1080 if inserted_at <= *dest_track_index {
1081 *dest_track_index += 1;
1082 }
1083 for index in cluster.iter_mut() {
1084 if *index >= inserted_at {
1085 *index += 1;
1086 }
1087 }
1088 for index in audio_slots.iter_mut() {
1089 if *index >= inserted_at {
1090 *index += 1;
1091 }
1092 }
1093 for index in created_track_indices.iter_mut() {
1094 if *index >= inserted_at {
1095 *index += 1;
1096 }
1097 }
1098 }
1099
1100 pub(super) fn sync_clips_ids_in_range(
1102 &self,
1103 track_index: usize,
1104 start: Seconds,
1105 end: Seconds,
1106 ) -> HashSet<i64> {
1107 let mut ids = HashSet::new();
1108 let Some(track) = self.children.get(track_index) else {
1109 return ids;
1110 };
1111 let mut pos: Seconds = 0.0;
1112 for item in &track.items {
1113 let item_start = pos;
1114 let item_end = pos + item.duration().max(0.0);
1115 pos = item_end;
1116 if item_end <= start + EPS || item_start >= end - EPS {
1117 continue;
1118 }
1119 if let Item::Clip(clip) = item {
1120 if let Some(sync_clips_id) = resolve_sync_clips_id(&clip.metadata) {
1121 ids.insert(sync_clips_id);
1122 }
1123 }
1124 }
1125 ids
1126 }
1127
1128 pub(super) fn range_only_holds_sync_clips(
1131 &self,
1132 track_index: usize,
1133 start: Seconds,
1134 end: Seconds,
1135 sync_clips_ids: &HashSet<i64>,
1136 ) -> bool {
1137 let Some(track) = self.children.get(track_index) else {
1138 return false;
1139 };
1140 let mut pos: Seconds = 0.0;
1141 for item in &track.items {
1142 let item_start = pos;
1143 let item_end = pos + item.duration().max(0.0);
1144 pos = item_end;
1145 if item_end <= start + EPS || item_start >= end - EPS {
1146 continue;
1147 }
1148 if let Item::Clip(clip) = item {
1149 if !resolve_sync_clips_id(&clip.metadata)
1150 .is_some_and(|sync_clips_id| sync_clips_ids.contains(&sync_clips_id))
1151 {
1152 return false;
1153 }
1154 }
1155 }
1156 true
1157 }
1158
1159 pub(super) fn track_sync_clips_ids(&self, track_index: usize) -> HashSet<i64> {
1160 let Some(track) = self.children.get(track_index) else {
1161 return HashSet::new();
1162 };
1163 track
1164 .items
1165 .iter()
1166 .filter_map(|item| match item {
1167 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
1168 Item::Gap(_) => None,
1169 })
1170 .collect()
1171 }
1172
1173 pub(super) fn tracks_share_sync_clips(
1174 &self,
1175 left_track_index: usize,
1176 right_track_index: usize,
1177 ) -> bool {
1178 if left_track_index == right_track_index {
1179 return true;
1180 }
1181 let left_groups = self.track_sync_clips_ids(left_track_index);
1182 !left_groups.is_empty()
1183 && self
1184 .track_sync_clips_ids(right_track_index)
1185 .iter()
1186 .any(|sync_clips_id| left_groups.contains(sync_clips_id))
1187 }
1188
1189 fn flatten_boundary_for_sync_clips(
1190 &self,
1191 anchor_track_index: usize,
1192 sync_clips: &[i64],
1193 ) -> FlattenedBoundary {
1194 if anchor_track_index >= self.children.len() {
1195 return FlattenedBoundary {
1196 track_indices: Vec::new(),
1197 };
1198 }
1199
1200 let mut track_indices = Vec::new();
1201 match self.children[anchor_track_index].kind {
1202 TrackKind::Video => {
1203 for track_index in (0..anchor_track_index).rev() {
1204 if self.children[track_index].kind != TrackKind::Audio {
1205 break;
1206 }
1207 if track_is_empty_boundary(&self.children[track_index]) {
1208 track_indices.push(track_index);
1209 break;
1210 }
1211 if !self
1212 .track_matches_primary_sync_boundary(anchor_track_index, track_index)
1213 {
1214 break;
1215 }
1216 track_indices.push(track_index);
1217 }
1218 track_indices.reverse();
1219 track_indices.push(anchor_track_index);
1220 if !sync_clips.is_empty() {
1221 for track_index in (anchor_track_index + 1)..self.children.len() {
1222 if self.children[track_index].kind != TrackKind::Audio {
1223 break;
1224 }
1225 if track_is_empty_boundary(&self.children[track_index]) {
1226 track_indices.push(track_index);
1227 break;
1228 }
1229 if !self
1230 .track_matches_primary_sync_boundary(anchor_track_index, track_index)
1231 {
1232 break;
1233 }
1234 track_indices.push(track_index);
1235 }
1236 }
1237 }
1238 TrackKind::Audio => {
1239 let mut audio_start = anchor_track_index;
1240 while audio_start > 0 && self.children[audio_start - 1].kind == TrackKind::Audio {
1241 let previous = audio_start - 1;
1242 if track_is_empty_boundary(&self.children[previous])
1243 || !self.track_matches_primary_sync_boundary(anchor_track_index, previous)
1244 {
1245 break;
1246 }
1247 audio_start = previous;
1248 }
1249 if !sync_clips.is_empty() {
1250 let previous_video_index = audio_start.checked_sub(1);
1251 if let Some(video_index) = previous_video_index {
1252 if self.children[video_index].kind == TrackKind::Video
1253 && self
1254 .track_matches_primary_sync_boundary(anchor_track_index, video_index)
1255 {
1256 track_indices.push(video_index);
1257 }
1258 }
1259 }
1260 for track_index in audio_start..self.children.len() {
1261 if self.children[track_index].kind != TrackKind::Audio {
1262 break;
1263 }
1264 if track_index != anchor_track_index
1265 && !track_is_empty_boundary(&self.children[track_index])
1266 && !self.track_matches_primary_sync_boundary(
1267 anchor_track_index,
1268 track_index,
1269 )
1270 {
1271 break;
1272 }
1273 track_indices.push(track_index);
1274 if track_index != anchor_track_index
1275 && track_is_empty_boundary(&self.children[track_index])
1276 {
1277 break;
1278 }
1279 }
1280 let video_index = track_indices.last().copied().unwrap_or(anchor_track_index) + 1;
1281 if video_index < self.children.len()
1282 && self.children[video_index].kind == TrackKind::Video
1283 && self.track_matches_primary_sync_boundary(anchor_track_index, video_index)
1284 {
1285 track_indices.push(video_index);
1286 }
1287 }
1288 TrackKind::Other => track_indices.push(anchor_track_index),
1289 }
1290
1291 FlattenedBoundary { track_indices }
1292 }
1293
1294 fn track_matches_primary_sync_boundary(
1295 &self,
1296 primary_track_index: usize,
1297 candidate_track_index: usize,
1298 ) -> bool {
1299 self.tracks_share_sync_clips(primary_track_index, candidate_track_index)
1300 }
1301
1302 fn boundary_track_indices_for_anchors(
1303 &self,
1304 sync_clips: &[i64],
1305 anchor_track_indices: &[usize],
1306 excluded_track_indices: &[usize],
1307 ) -> Vec<usize> {
1308 let mut track_indices = Vec::new();
1309 for anchor_track_index in anchor_track_indices {
1310 for track_index in &self
1311 .flatten_boundary_for_sync_clips(*anchor_track_index, sync_clips)
1312 .track_indices
1313 {
1314 if excluded_track_indices.contains(track_index) {
1315 continue;
1316 }
1317 if !track_indices.contains(track_index) {
1318 track_indices.push(*track_index);
1319 }
1320 }
1321 }
1322 track_indices
1323 }
1324
1325 fn sync_changed_groups_after_resize(
1340 &mut self,
1341 before_states: &HashMap<String, SyncedClipState>,
1342 modified_track_indices: &[usize],
1343 excluded_ids: &HashSet<String>,
1344 _overlap_policy: OverlapPolicy,
1345 ) -> bool {
1346 struct GroupChange {
1347 sync_clips_id: i64,
1348 primary_start: Seconds,
1349 start_delta: Seconds,
1350 old_duration: Seconds,
1351 new_duration: Seconds,
1352 source_delta: Seconds,
1353 }
1354 let mut seen = HashSet::new();
1355 let mut changes: Vec<GroupChange> = Vec::new();
1356 let mut removed_groups: Vec<i64> = Vec::new();
1357 for (id, before) in before_states {
1358 if excluded_ids.contains(id) || !modified_track_indices.contains(&before.track_index) {
1359 continue;
1360 }
1361 let Some((track_index, item_index, item)) = self
1362 .get_item(id)
1363 .or_else(|| self.surviving_sync_fragment(before))
1364 else {
1365 if seen.insert(before.sync_clips_id) {
1367 removed_groups.push(before.sync_clips_id);
1368 }
1369 continue;
1370 };
1371 let start = self.children[track_index].start_time_of_item(item_index);
1372 let duration = item.duration().max(0.0);
1373 let start_delta = start - before.start;
1374 if (start_delta.abs() > EPS || (duration - before.duration).abs() > EPS)
1375 && seen.insert(before.sync_clips_id)
1376 {
1377 changes.push(GroupChange {
1378 sync_clips_id: before.sync_clips_id,
1379 primary_start: before.start,
1380 start_delta,
1381 old_duration: before.duration,
1382 new_duration: duration,
1383 source_delta: item_source_start(item) - before.source_start,
1384 });
1385 }
1386 }
1387 changes.sort_by(|a, b| a.primary_start.total_cmp(&b.primary_start));
1388
1389 for sync_clips_id in removed_groups {
1392 let partner_ids: Vec<String> = before_states
1393 .iter()
1394 .filter(|(_, state)| {
1395 state.sync_clips_id == sync_clips_id
1396 && !modified_track_indices.contains(&state.track_index)
1397 })
1398 .map(|(id, _)| id.clone())
1399 .collect();
1400 for partner_id in partner_ids {
1401 let Some((track_index, item_index, _)) = self.get_item(&partner_id) else {
1402 continue;
1403 };
1404 self.children[track_index].delete_clip(item_index, true);
1405 self.children[track_index].sanitize_preserving_all_gap_track();
1406 }
1407 }
1408
1409 let mut applied: HashMap<usize, Vec<(Seconds, Seconds)>> = HashMap::new();
1411 for change in changes {
1412 let partners: Vec<(String, SyncedClipState)> = before_states
1413 .iter()
1414 .filter(|(_, state)| {
1415 state.sync_clips_id == change.sync_clips_id
1416 && !modified_track_indices.contains(&state.track_index)
1417 })
1418 .map(|(id, state)| (id.clone(), state.clone()))
1419 .collect();
1420 for (partner_id, before) in partners {
1421 let Some((track_index, _, _)) = self.get_item(&partner_id) else {
1422 continue;
1423 };
1424 let old_start = before.start
1425 + applied
1426 .get(&track_index)
1427 .map(|gaps| {
1428 gaps.iter()
1429 .filter(|(from, _)| *from <= before.start + EPS)
1430 .map(|(_, delta)| *delta)
1431 .sum::<Seconds>()
1432 })
1433 .unwrap_or(0.0);
1434 let old_end = old_start + before.duration;
1435 let new_end_rel = change.start_delta + change.new_duration;
1436 let trimmed_in_place = change.start_delta >= -EPS
1437 && new_end_rel <= change.old_duration + EPS
1438 && change.new_duration < change.old_duration - EPS;
1439
1440 if trimmed_in_place {
1441 let Some((_, item_index, _)) = self.get_item(&partner_id) else {
1442 continue;
1443 };
1444 if change.start_delta > EPS {
1445 self.resize_synced_clips_with_leading_gap(
1446 vec![(track_index, item_index)],
1447 change.start_delta,
1448 change.new_duration,
1449 );
1450 let trailing = change.old_duration - new_end_rel;
1452 if trailing > EPS {
1453 self.resize_synced_clips_with_trailing_gap(
1454 vec![(track_index, item_index + 1)],
1455 change.new_duration,
1456 );
1457 self.children[track_index]
1461 .items
1462 .insert(item_index + 2, Item::Gap(Gap::make_gap(trailing)));
1463 }
1464 } else {
1465 self.resize_synced_clips_with_trailing_gap(
1466 vec![(track_index, item_index)],
1467 change.new_duration,
1468 );
1469 }
1470 if change.source_delta.abs() > EPS && change.start_delta <= EPS {
1471 let Some((t, i, _)) = self.get_item(&partner_id) else {
1472 continue;
1473 };
1474 let shifted = item_source_start(&self.children[t].items[i]) + change.source_delta;
1475 set_item_source_start(&mut self.children[t].items[i], shifted);
1476 }
1477 self.children[track_index].sanitize_preserving_all_gap_track();
1478 continue;
1479 }
1480
1481 if change.start_delta.abs() > EPS {
1484 let delta = change.start_delta;
1485 let track = &mut self.children[track_index];
1486 if delta > 0.0 {
1487 track.insert_at_time(
1488 old_start,
1489 Item::Gap(Gap::make_gap(delta)),
1490 OverlapPolicy::Push,
1491 InsertPolicy::SplitAndInsert,
1492 );
1493 } else {
1494 let from = (old_start + delta).max(0.0);
1495 track.delete_range(from, old_start, false);
1496 }
1497 applied
1498 .entry(track_index)
1499 .or_default()
1500 .push((before.start, delta));
1501 }
1502
1503 let Some((t, i, item)) = self.get_item(&partner_id) else {
1505 continue;
1506 };
1507 let current_start = self.children[t].start_time_of_item(i);
1508 if change.new_duration < change.old_duration - EPS {
1509 self.resize_synced_clips_with_trailing_gap(vec![(t, i)], change.new_duration);
1510 } else if change.new_duration > change.old_duration + EPS {
1511 let mut item = item.clone();
1512 item.set_duration(change.new_duration);
1513 replace_track_range_with_item(
1514 &mut self.children[t],
1515 current_start,
1516 current_start + change.new_duration,
1517 item,
1518 );
1519 }
1520 if change.source_delta.abs() > EPS {
1521 let Some((t, i, _)) = self.get_item(&partner_id) else {
1522 continue;
1523 };
1524 let shifted = item_source_start(&self.children[t].items[i]) + change.source_delta;
1525 set_item_source_start(&mut self.children[t].items[i], shifted);
1526 }
1527 let _ = old_end;
1528 self.children[track_index].sanitize_preserving_all_gap_track();
1529 }
1530 }
1531 true
1532 }
1533
1534 fn surviving_sync_fragment(&self, before: &SyncedClipState) -> Option<(usize, usize, &Item)> {
1538 let track = self.children.get(before.track_index)?;
1539 let old_end = before.start + before.duration;
1540 let mut position: Seconds = 0.0;
1541 for (item_index, item) in track.items.iter().enumerate() {
1542 let item_start = position;
1543 let item_end = position + item.duration().max(0.0);
1544 position = item_end;
1545 let Item::Clip(clip) = item else {
1546 continue;
1547 };
1548 if resolve_sync_clips_id(&clip.metadata) != Some(before.sync_clips_id) {
1549 continue;
1550 }
1551 if item_end > before.start + EPS && item_start < old_end - EPS {
1552 return Some((before.track_index, item_index, item));
1553 }
1554 }
1555 None
1556 }
1557
1558 fn insert_synced_item_at_time(
1559 &mut self,
1560 dest_track_index: usize,
1561 dest_time: Seconds,
1562 dest_index: Option<usize>,
1563 item: Item,
1564 overlap_policy: OverlapPolicy,
1565 insert_policy: InsertPolicy,
1566 synced_audio_clips: Option<Vec<Item>>,
1567 synced_video_clip: Option<Item>,
1568 column_video_placed_separately: bool,
1569 preferred_video_track_id: Option<&str>,
1570 preferred_audio_track_indices: Option<&[usize]>,
1571 move_source_track_indices: Option<&[usize]>,
1572 ) -> Option<InsertItemAtTimeResult> {
1573 let mut primary_item = item;
1574 primary_item.clamp_to_active_available_range();
1575 let synced_inputs =
1576 Self::normalize_synced_inputs(synced_audio_clips, synced_video_clip);
1577 let has_synced_clips =
1578 !synced_inputs.audio.is_empty() || synced_inputs.video.is_some();
1579 if has_synced_clips && !matches!(primary_item, Item::Clip(_)) {
1580 return None;
1581 }
1582 if matches!(synced_inputs.video, Some(Item::Gap(_))) {
1583 return None;
1584 }
1585 if !self.column_may_land_on(
1586 dest_track_index,
1587 matches!(primary_item, Item::Gap(_)),
1588 synced_inputs.video.is_some() || column_video_placed_separately,
1589 ) {
1590 return None;
1591 }
1592
1593 let modified_duration = primary_item.duration().max(0.0);
1594 if modified_duration <= EPS {
1595 return None;
1596 }
1597 let column_span = Self::synced_insert_column_span(modified_duration, &synced_inputs);
1598
1599 let start = if let Some(dest_index) = dest_index {
1601 self.children[dest_track_index].start_time_of_item(dest_index)
1602 } else {
1603 insertion_start_or_end_for_policy(
1604 &self.children[dest_track_index],
1605 dest_time,
1606 insert_policy,
1607 )?
1608 };
1609
1610 let backup = self.clone();
1611 let mut used_ids = self.collect_timeline_ids();
1612 let sync_clips_id = has_synced_clips.then(|| self.next_sync_clips_id());
1613 let mut created_track_indices = Vec::new();
1614
1615 let prefer_cluster_with_video = self.children.get(dest_track_index).is_some_and(|track| {
1618 track.kind == TrackKind::Video
1619 }) || synced_inputs.video.is_some();
1620 let mut dest_track_index = dest_track_index;
1621 let mut cluster = self.resolve_insert_cluster(
1622 dest_track_index,
1623 prefer_cluster_with_video,
1624 if synced_inputs.video.is_some() {
1625 preferred_video_track_id
1626 } else {
1627 None
1628 },
1629 );
1630
1631 let mut audio_slots: Vec<usize> = Vec::new();
1635 let mut synced_video_clip_id = None;
1636 let mut column_video = None;
1637 let mut owner_track_index = self.group_owner_of(dest_track_index);
1641 if let Some(video_item) = synced_inputs.video {
1642 let video_span = Self::sanitized_clip_duration(&video_item).unwrap_or(column_span);
1643 let track_count_before = self.children.len();
1644 let video_track_index = self.pick_video_partner_track(
1645 dest_track_index,
1646 start,
1647 start + video_span,
1648 preferred_video_track_id,
1649 &[],
1650 sync_clips_id,
1651 overlap_policy,
1652 insert_policy,
1653 &mut created_track_indices,
1654 );
1655 let Some(video_track_index) = video_track_index else {
1656 *self = backup;
1657 return None;
1658 };
1659 if self.children.len() > track_count_before {
1660 Self::shift_insert_track_indices_after_create(
1661 video_track_index,
1662 &mut dest_track_index,
1663 &mut cluster,
1664 &mut audio_slots,
1665 &mut [],
1666 );
1667 }
1668 let Some((video_item, video_id)) = Self::prepare_synced_item_preserve_duration(
1669 video_item,
1670 sync_clips_id,
1671 &mut used_ids,
1672 ) else {
1673 *self = backup;
1674 return None;
1675 };
1676 synced_video_clip_id = Some(video_id);
1677 column_video = Some((video_track_index, video_item));
1678 owner_track_index = video_track_index;
1679 }
1680
1681 if has_synced_clips {
1684 for track_index in self
1685 .associated_track_indices(dest_track_index)
1686 .into_iter()
1687 .chain(self.associated_track_indices(owner_track_index))
1688 .chain(std::iter::once(owner_track_index))
1689 {
1690 if !cluster.contains(&track_index) {
1691 cluster.push(track_index);
1692 }
1693 }
1694 cluster.sort_unstable();
1695 }
1696
1697 let audio_durations: Vec<Seconds> = synced_inputs
1704 .audio
1705 .iter()
1706 .map(|item| Self::sanitized_clip_duration(item).unwrap_or(modified_duration))
1707 .collect();
1708 let column_end = start + column_span;
1709 let overridden_sync_ids: HashSet<i64> = if overlap_policy == OverlapPolicy::Override {
1710 self.sync_clips_ids_in_range(dest_track_index, start, column_end)
1711 } else {
1712 HashSet::new()
1713 };
1714 audio_slots = self.assign_audio_partner_slots(
1715 &mut dest_track_index,
1716 &mut owner_track_index,
1717 &mut cluster,
1718 &mut created_track_indices,
1719 start,
1720 &audio_durations,
1721 preferred_audio_track_indices,
1722 move_source_track_indices.unwrap_or(&[]),
1723 overlap_policy,
1724 &overridden_sync_ids,
1725 );
1726 if let Some((video_track_index, _)) = column_video.as_mut() {
1727 *video_track_index = owner_track_index;
1729 }
1730
1731 let (primary_item, primary_id) = match primary_item {
1733 Item::Clip(_) => Self::prepare_synced_item(
1734 primary_item,
1735 modified_duration,
1736 sync_clips_id,
1737 &mut used_ids,
1738 )?,
1739 Item::Gap(mut gap) => {
1740 gap.source_range.duration.set_from_seconds(modified_duration);
1741 let mut item = Item::Gap(gap);
1742 let id = Self::ensure_unique_item_id(&mut item, &mut used_ids);
1743 (item, id)
1744 }
1745 };
1746 let mut column = Vec::new();
1747
1748 let mut audio_clips = Vec::new();
1750 for (audio_item, &audio_track_index) in synced_inputs.audio.into_iter().zip(&audio_slots) {
1751 let Some((audio_item, audio_id)) = Self::prepare_synced_item_preserve_duration(
1752 audio_item,
1753 sync_clips_id,
1754 &mut used_ids,
1755 ) else {
1756 *self = backup;
1757 return None;
1758 };
1759 column.push((audio_track_index, audio_item));
1760 audio_clips.push((audio_id, audio_track_index));
1761 }
1762 if let Some(video_placement) = column_video {
1763 column.push(video_placement);
1764 }
1765 column.sort_by_key(|(track_index, _)| *track_index);
1766
1767 let mut full_column = vec![(dest_track_index, primary_item)];
1768 full_column.extend(column);
1769 if overlap_policy == OverlapPolicy::Push {
1770 full_column.sort_by_key(|(track_index, _)| *track_index);
1771 } else {
1772 let (mut dest_items, mut partners): (Vec<_>, Vec<_>) = full_column
1773 .into_iter()
1774 .partition(|(track_index, _)| *track_index == dest_track_index);
1775 partners.sort_by_key(|(track_index, _)| *track_index);
1776 dest_items.extend(partners);
1777 full_column = dest_items;
1778 }
1779
1780 let mut insert_updates: Vec<(usize, TrackInsertResult)> = Vec::new();
1781 for (track_index, item) in full_column {
1782 let result = self.children[track_index].insert_at_time(
1783 start,
1784 item,
1785 overlap_policy,
1786 InsertPolicy::SplitAndInsert,
1787 );
1788 if !result.success {
1789 *self = backup;
1790 return None;
1791 }
1792 insert_updates.push((track_index, result));
1793 }
1794
1795 let update_refs: Vec<_> = insert_updates
1796 .iter()
1797 .map(|(track_index, result)| (*track_index, result))
1798 .collect();
1799 if !self.propagate_insert_to_cluster(
1800 start,
1801 modified_duration,
1802 overlap_policy,
1803 &update_refs,
1804 &cluster,
1805 sync_clips_id,
1806 ) {
1807 *self = backup;
1808 return None;
1809 }
1810 if overlap_policy == OverlapPolicy::Push
1811 && !self.propagate_push_to_cluster(start, modified_duration, &update_refs)
1812 {
1813 *self = backup;
1814 return None;
1815 }
1816
1817 self.sanitize_preserving_all_gap_tracks();
1818
1819 if sync_clips_id.is_some()
1820 && matches!(self.get_item(&primary_id), Some((_, _, Item::Clip(_))))
1821 {
1822 Some(InsertItemAtTimeResult::Synced(SyncedInsertResult {
1823 primary_clip_id: primary_id,
1824 audio_clips,
1825 synced_video_clip_id,
1826 sync_clips_id,
1827 created_track_indices,
1828 }))
1829 } else {
1830 Some(InsertItemAtTimeResult::ItemId(primary_id))
1831 }
1832 }
1833
1834 pub fn resize_item(
1835 &mut self,
1836 item_id: &str,
1837 new_start_time: Seconds,
1838 new_duration: Seconds,
1839 overlap_policy: OverlapPolicy,
1840 clamp_to_media: bool,
1841 ) -> bool {
1842 let Some((selected_track_index, selected_item_index, selected_item)) =
1843 self.get_item(item_id)
1844 else {
1845 return false;
1846 };
1847 let before_states = self.synced_clip_states();
1848 let target_ids: Vec<String> = match selected_item {
1849 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata)
1850 .map(|sync_clips_id| {
1851 self.synced_clips_targets(sync_clips_id)
1852 .into_iter()
1853 .filter_map(|(track_index, item_index)| {
1854 self.children
1855 .get(track_index)
1856 .and_then(|track| track.items.get(item_index))
1857 .and_then(Item::get_id)
1858 })
1859 .collect()
1860 })
1861 .filter(|ids: &Vec<String>| ids.len() > 1)
1862 .unwrap_or_else(|| vec![item_id.to_string()]),
1863 Item::Gap(_) => vec![item_id.to_string()],
1864 };
1865 if target_ids.is_empty() {
1866 return false;
1867 }
1868 if matches!(selected_item, Item::Gap(_)) {
1869 let backup = self.clone();
1870 let Some(track) = self.children.get_mut(selected_track_index) else {
1871 return false;
1872 };
1873 let Some(item) = track.items.get_mut(selected_item_index) else {
1874 return false;
1875 };
1876 item.set_duration(new_duration.max(0.0));
1877 track.sanitize_preserving_all_gap_track();
1878 if !self.sync_changed_groups_after_resize(
1879 &before_states,
1880 &[selected_track_index],
1881 &HashSet::new(),
1882 overlap_policy,
1883 ) {
1884 *self = backup;
1885 return false;
1886 }
1887 self.sanitize_preserving_all_gap_tracks();
1888 return true;
1889 }
1890 let excluded_ids: HashSet<_> = target_ids.iter().cloned().collect();
1891 let selected_start =
1892 self.children[selected_track_index].start_time_of_item(selected_item_index);
1893 let start_delta = new_start_time - selected_start;
1894
1895 let effective_duration = target_ids
1896 .iter()
1897 .filter_map(|id| {
1898 self.get_item(id).map(|(_, _, item)| {
1899 resize_effective_duration(item, new_duration, clamp_to_media)
1900 })
1901 })
1902 .fold(new_duration.max(0.0), Seconds::min);
1903
1904 let backup = self.clone();
1905 if start_delta.abs() <= EPS {
1906 if overlap_policy != OverlapPolicy::Push {
1907 let mut resized_items = Vec::new();
1908 let mut modified_track_indices = Vec::new();
1909
1910 for id in &target_ids {
1911 let Some((track_index, item_index, item)) = self.get_item(id) else {
1912 *self = backup;
1913 return false;
1914 };
1915 if matches!(item, Item::Gap(_)) {
1916 resized_items.clear();
1917 modified_track_indices.clear();
1918 break;
1919 }
1920
1921 let old_start = self.children[track_index].start_time_of_item(item_index);
1922 let old_duration = item.duration().max(0.0);
1923 if effective_duration <= old_duration + EPS {
1924 resized_items.clear();
1925 modified_track_indices.clear();
1926 break;
1927 }
1928
1929 let mut item = item.clone();
1930 item.set_duration(effective_duration);
1931 if clamp_to_media {
1932 item.clamp_to_active_available_range();
1933 }
1934 resized_items.push((
1935 track_index,
1936 old_start,
1937 old_start + effective_duration,
1938 item,
1939 ));
1940 modified_track_indices.push(track_index);
1941 }
1942
1943 if !resized_items.is_empty() {
1944 for (track_index, range_start, range_end, item) in resized_items {
1945 let Some(track) = self.children.get_mut(track_index) else {
1946 *self = backup;
1947 return false;
1948 };
1949 replace_track_range_with_item(track, range_start, range_end, item);
1950 }
1951 modified_track_indices.sort_unstable();
1952 modified_track_indices.dedup();
1953 if !self.sync_changed_groups_after_resize(
1954 &before_states,
1955 &modified_track_indices,
1956 &excluded_ids,
1957 overlap_policy,
1958 ) {
1959 *self = backup;
1960 return false;
1961 }
1962 self.sanitize_preserving_all_gap_tracks();
1963 return true;
1964 }
1965 }
1966
1967 let mut modified_track_indices = Vec::new();
1968 for id in &target_ids {
1969 let Some((track_index, item_index, _)) = self.get_item(id) else {
1970 *self = backup;
1971 return false;
1972 };
1973 let Some(track) = self.children.get_mut(track_index) else {
1974 *self = backup;
1975 return false;
1976 };
1977 let Some(item) = track.items.get_mut(item_index) else {
1978 *self = backup;
1979 return false;
1980 };
1981 item.set_duration(effective_duration);
1982 if clamp_to_media {
1983 item.clamp_to_active_available_range();
1984 }
1985 track.sanitize_preserving_all_gap_track();
1986 modified_track_indices.push(track_index);
1987 }
1988 modified_track_indices.sort_unstable();
1989 modified_track_indices.dedup();
1990 if !self.sync_changed_groups_after_resize(
1991 &before_states,
1992 &modified_track_indices,
1993 &excluded_ids,
1994 overlap_policy,
1995 ) {
1996 *self = backup;
1997 return false;
1998 }
1999 self.sanitize_preserving_all_gap_tracks();
2000 return true;
2001 }
2002
2003 if start_delta < -EPS {
2004 let mut resized_items = Vec::new();
2005 let mut modified_track_indices = Vec::new();
2006
2007 for id in &target_ids {
2008 let Some((track_index, item_index, item)) = self.get_item(id) else {
2009 *self = backup;
2010 return false;
2011 };
2012 let old_start = self.children[track_index].start_time_of_item(item_index);
2013 let target_start = old_start + start_delta;
2014 let old_duration = item.duration().max(0.0);
2015 let old_end = old_start + old_duration;
2016
2017 modified_track_indices.push(track_index);
2018 let mut item = item.clone();
2019 item.set_duration(effective_duration);
2020 if clamp_to_media {
2021 item.clamp_to_active_available_range();
2022 }
2023 resized_items.push((
2024 track_index,
2025 item_index,
2026 target_start,
2027 old_duration,
2028 old_end,
2029 item,
2030 ));
2031 }
2032
2033 let mut removals = resized_items
2034 .iter()
2035 .map(|(track_index, item_index, _, old_duration, _, _)| {
2036 (*track_index, *item_index, *old_duration)
2037 })
2038 .collect::<Vec<_>>();
2039 removals.sort_unstable_by(|a, b| (b.0, b.1).cmp(&(a.0, a.1)));
2040
2041 for (track_index, item_index, old_duration) in removals {
2042 let Some(track) = self.children.get_mut(track_index) else {
2043 *self = backup;
2044 return false;
2045 };
2046 if item_index >= track.items.len() {
2047 *self = backup;
2048 return false;
2049 }
2050 track.items.remove(item_index);
2051 track
2052 .items
2053 .insert(item_index, Item::Gap(Gap::make_gap(old_duration)));
2054 }
2055
2056 for (track_index, _, target_start, _, old_end, item) in resized_items {
2057 let Some(track) = self.children.get_mut(track_index) else {
2058 *self = backup;
2059 return false;
2060 };
2061 replace_track_range_with_item(track, target_start, old_end, item);
2062 }
2063 modified_track_indices.sort_unstable();
2064 modified_track_indices.dedup();
2065 if !self.sync_changed_groups_after_resize(
2066 &before_states,
2067 &modified_track_indices,
2068 &excluded_ids,
2069 overlap_policy,
2070 ) {
2071 *self = backup;
2072 return false;
2073 }
2074 self.sanitize_preserving_all_gap_tracks();
2075 return true;
2076 }
2077
2078 let mut resized_items = Vec::new();
2084 let mut modified_track_indices = Vec::new();
2085 for id in &target_ids {
2086 let Some((track_index, item_index, item)) = self.get_item(id) else {
2087 *self = backup;
2088 return false;
2089 };
2090 modified_track_indices.push(track_index);
2091 let old_start = self.children[track_index].start_time_of_item(item_index);
2092 let old_end = old_start + item.duration().max(0.0);
2093 let target_start = old_start + start_delta;
2094 let mut item = item.clone();
2095 item.set_duration(effective_duration);
2096 if clamp_to_media {
2097 item.clamp_to_active_available_range();
2098 }
2099 resized_items.push((track_index, old_start, old_end, target_start, item));
2100 }
2101
2102 for (track_index, old_start, old_end, target_start, item) in resized_items {
2103 let Some(id) = item.get_id() else {
2104 *self = backup;
2105 return false;
2106 };
2107 let Some((_, item_index, _)) = self.get_item(&id) else {
2108 *self = backup;
2109 return false;
2110 };
2111 let new_end = target_start + item.duration().max(0.0);
2112 let Some(track) = self.children.get_mut(track_index) else {
2113 *self = backup;
2114 return false;
2115 };
2116 if new_end <= old_end + EPS {
2117 track.items.remove(item_index);
2118 let trailing = old_end - new_end;
2119 if trailing > EPS {
2120 track
2121 .items
2122 .insert(item_index, Item::Gap(Gap::make_gap(trailing)));
2123 }
2124 track.items.insert(item_index, item);
2125 track
2126 .items
2127 .insert(item_index, Item::Gap(Gap::make_gap(start_delta)));
2128 track.sanitize_preserving_all_gap_track();
2129 } else if overlap_policy == OverlapPolicy::Override {
2130 track.items.remove(item_index);
2131 track
2132 .items
2133 .insert(item_index, Item::Gap(Gap::make_gap(old_end - old_start)));
2134 replace_track_range_with_item(track, target_start, new_end, item);
2135 } else {
2136 track.items.remove(item_index);
2137 track.sanitize_preserving_all_gap_track();
2138 track.insert_at_time(
2139 target_start,
2140 item,
2141 overlap_policy,
2142 InsertPolicy::SplitAndInsert,
2143 );
2144 }
2145 }
2146 modified_track_indices.sort_unstable();
2147 modified_track_indices.dedup();
2148 if !self.sync_changed_groups_after_resize(
2149 &before_states,
2150 &modified_track_indices,
2151 &excluded_ids,
2152 overlap_policy,
2153 ) {
2154 *self = backup;
2155 return false;
2156 }
2157 self.sanitize_preserving_all_gap_tracks();
2158 true
2159 }
2160
2161 pub fn modify_item(
2162 &mut self,
2163 item_id: &str,
2164 source_start_time: Seconds,
2165 duration: Seconds,
2166 clamp_to_media: bool,
2167 resize_from_start: bool,
2168 push_following: bool,
2169 ) -> bool {
2170 let Some((track_index, item_index, _)) = self.get_item(item_id) else {
2171 return false;
2172 };
2173
2174 let mut effective_source_start = source_start_time;
2175 let mut effective_duration = duration;
2176 if effective_source_start < 0.0 {
2177 effective_duration += effective_source_start;
2178 effective_source_start = 0.0;
2179 }
2180
2181 if effective_duration < 0.0 {
2182 let replace_with_gap =
2183 !matches!(self.children[track_index].items[item_index], Item::Gap(_));
2184 self.delete_item(item_id, replace_with_gap, true);
2185 self.sanitize_preserving_all_gap_tracks();
2186 return true;
2187 }
2188
2189 let old_timeline_start = self.children[track_index].start_time_of_item(item_index);
2190 let old_source_start = match &self.children[track_index].items[item_index] {
2191 Item::Clip(clip) => clip.source_range.start_time.to_seconds(),
2192 Item::Gap(_) => 0.0,
2193 };
2194 let old_duration = self.children[track_index].items[item_index].duration();
2195 let new_timeline_start =
2196 (old_timeline_start + effective_source_start - old_source_start).max(0.0);
2197 let is_gap = matches!(self.children[track_index].items[item_index], Item::Gap(_));
2198 let is_clip = matches!(self.children[track_index].items[item_index], Item::Clip(_));
2199 let mut source_delta = effective_source_start - old_source_start;
2200 if is_clip && resize_from_start && !push_following {
2201 let unclamped_timeline_start = old_timeline_start + source_delta;
2202 if unclamped_timeline_start < 0.0 {
2203 source_delta = -old_timeline_start;
2204 effective_source_start = old_source_start + source_delta;
2205 effective_duration = old_duration - source_delta;
2206 }
2207 }
2208 let effective_push_following =
2209 push_following || (is_gap && effective_duration < old_duration);
2210
2211 let cluster = self.boundary_group_indices(track_index);
2212 let is_cluster_column = self.is_cluster_column_at(
2213 &cluster,
2214 old_timeline_start,
2215 old_duration,
2216 );
2217 if is_cluster_column
2218 && (new_timeline_start - old_timeline_start).abs() <= EPS
2219 && (effective_duration - old_duration).abs() > EPS
2220 {
2221 let backup = self.clone();
2222 let before_states = self.synced_clip_states();
2223 if !self.apply_cluster_column_duration_change(
2224 item_id,
2225 &cluster,
2226 old_timeline_start,
2227 old_duration,
2228 effective_duration,
2229 effective_source_start,
2230 clamp_to_media,
2231 ) {
2232 return false;
2233 }
2234 self.sanitize_preserving_all_gap_tracks();
2235 let overlap_policy = if effective_push_following {
2236 OverlapPolicy::Push
2237 } else {
2238 OverlapPolicy::Override
2239 };
2240 if !self.sync_changed_groups_after_resize(
2241 &before_states,
2242 &cluster,
2243 &HashSet::new(),
2244 overlap_policy,
2245 ) {
2246 *self = backup;
2247 return false;
2248 }
2249 return true;
2250 }
2251
2252 if is_gap && effective_duration < old_duration {
2253 let backup = self.clone();
2254 let before_states = self.synced_clip_states();
2255 self.children[track_index].items[item_index].set_duration(effective_duration);
2256 self.sanitize_preserving_all_gap_tracks();
2257 if !self.sync_changed_groups_after_resize(
2258 &before_states,
2259 &[track_index],
2260 &HashSet::new(),
2261 OverlapPolicy::Push,
2262 ) {
2263 *self = backup;
2264 return false;
2265 }
2266 return true;
2267 }
2268
2269 if is_clip && !effective_push_following {
2270 if resize_from_start && source_delta > 0.0 && effective_duration < old_duration {
2271 let targets = self.synced_clip_targets_for_item(item_id);
2272 self.resize_synced_clips_with_leading_gap(
2273 targets,
2274 source_delta,
2275 effective_duration,
2276 );
2277 self.sanitize_preserving_all_gap_tracks();
2278 return true;
2279 }
2280
2281 let duration_delta = old_duration - effective_duration;
2282 if !resize_from_start && duration_delta > 0.0 {
2283 let targets = self.synced_clip_targets_for_item(item_id);
2284 self.resize_synced_clips_with_trailing_gap(targets, effective_duration);
2285 self.sanitize_preserving_all_gap_tracks();
2286 return true;
2287 }
2288 }
2289
2290 let resize_timeline_start = if is_clip && effective_push_following && resize_from_start {
2291 old_timeline_start
2292 } else {
2293 new_timeline_start
2294 };
2295
2296 let resized = self.resize_item_with_source_start(
2297 item_id,
2298 resize_timeline_start,
2299 effective_source_start,
2300 effective_duration,
2301 if effective_push_following {
2302 OverlapPolicy::Push
2303 } else {
2304 OverlapPolicy::Override
2305 },
2306 clamp_to_media,
2307 );
2308 self.sanitize_preserving_all_gap_tracks();
2309 resized
2310 }
2311
2312 pub fn resize_item_with_source_start(
2313 &mut self,
2314 item_id: &str,
2315 new_start_time: Seconds,
2316 source_start_time: Seconds,
2317 new_duration: Seconds,
2318 overlap_policy: OverlapPolicy,
2319 clamp_to_media: bool,
2320 ) -> bool {
2321 let Some((_, _, item)) = self.get_item(item_id) else {
2322 return false;
2323 };
2324 let source_delta = match item {
2325 Item::Clip(clip) => source_start_time - clip.source_range.start_time.to_seconds(),
2326 Item::Gap(_) => 0.0,
2327 };
2328 if source_delta.abs() > EPS {
2329 self.offset_synced_clip_source_starts(item_id, source_delta);
2330 }
2331 self.resize_item(
2332 item_id,
2333 new_start_time,
2334 new_duration,
2335 overlap_policy,
2336 clamp_to_media,
2337 )
2338 }
2339
2340 fn resize_synced_clips_with_leading_gap(
2341 &mut self,
2342 mut targets: Vec<(usize, usize)>,
2343 source_delta: Seconds,
2344 duration: Seconds,
2345 ) {
2346 targets.sort_unstable_by(|a, b| b.cmp(a));
2347 for (track_index, item_index) in targets {
2348 let Some(track) = self.children.get_mut(track_index) else {
2349 continue;
2350 };
2351 if item_index >= track.items.len() {
2352 continue;
2353 }
2354 let source_start_time = item_source_start(&track.items[item_index]) + source_delta;
2355 set_item_source_start(&mut track.items[item_index], source_start_time);
2356 track.items[item_index].set_duration(duration);
2357 track
2358 .items
2359 .insert(item_index, Item::Gap(Gap::make_gap(source_delta)));
2360 }
2361 }
2362
2363 fn resize_synced_clips_with_trailing_gap(
2364 &mut self,
2365 mut targets: Vec<(usize, usize)>,
2366 duration: Seconds,
2367 ) {
2368 targets.sort_unstable_by(|a, b| b.cmp(a));
2369 for (track_index, item_index) in targets {
2370 let Some(track) = self.children.get_mut(track_index) else {
2371 continue;
2372 };
2373 if item_index >= track.items.len() {
2374 continue;
2375 }
2376 let gap_duration = (track.items[item_index].duration() - duration).max(0.0);
2377 track.items[item_index].set_duration(duration);
2378 if gap_duration > 0.0 {
2379 track
2380 .items
2381 .insert(item_index + 1, Item::Gap(Gap::make_gap(gap_duration)));
2382 }
2383 }
2384 }
2385
2386 fn synced_clip_targets_for_item(&self, item_id: &str) -> Vec<(usize, usize)> {
2387 let Some((selected_track_index, selected_item_index, selected_item)) =
2388 self.get_item(item_id)
2389 else {
2390 return Vec::new();
2391 };
2392 let Some(sync_clips_id) = (match selected_item {
2393 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
2394 Item::Gap(_) => None,
2395 }) else {
2396 return vec![(selected_track_index, selected_item_index)];
2397 };
2398
2399 let targets = self.synced_clips_targets(sync_clips_id);
2400 if targets.len() > 1 {
2401 targets
2402 } else {
2403 vec![(selected_track_index, selected_item_index)]
2404 }
2405 }
2406
2407 fn offset_synced_clip_source_starts(&mut self, item_id: &str, source_delta: Seconds) {
2408 for (track_index, item_index) in self.synced_clip_targets_for_item(item_id) {
2409 let Some(item) = self
2410 .children
2411 .get_mut(track_index)
2412 .and_then(|track| track.items.get_mut(item_index))
2413 else {
2414 continue;
2415 };
2416 let source_start_time = (item_source_start(item) + source_delta).max(0.0);
2417 set_item_source_start(item, source_start_time);
2418 }
2419 }
2420
2421 fn delete_one_item(&mut self, item_id: &str, replace_with_gap: bool) -> Option<(usize, Item)> {
2422 for ti in 0..self.children.len() {
2423 if let Some((ii, item)) = self.children[ti].get_item_by_id(item_id) {
2424 if !replace_with_gap
2427 && matches!(item, Item::Clip(clip) if clip.sync_clips_id().is_none())
2428 {
2429 let (mut removed, _) = self.ripple_remove_items(&[(ti, ii)])?;
2430 return removed.pop();
2431 }
2432 let backup = self.clone();
2433 let before_states = self.synced_clip_states();
2434 let Some(removed) = self.children[ti].delete_clip(ii, replace_with_gap) else {
2436 return None;
2437 };
2438 if !replace_with_gap {
2439 let excluded_ids = removed.get_id().into_iter().collect();
2440 if !self.sync_changed_groups_after_resize(
2441 &before_states,
2442 &[ti],
2443 &excluded_ids,
2444 OverlapPolicy::Override,
2445 ) {
2446 *self = backup;
2447 return None;
2448 }
2449 }
2450 return Some((ti, removed));
2451 }
2452 }
2453 None
2454 }
2455
2456 fn remove_item_at_for_move(
2457 &mut self,
2458 track_index: usize,
2459 item_index: usize,
2460 replace_with_gap: bool,
2461 used_ids: &mut HashSet<String>,
2462 ) -> bool {
2463 let Some(track) = self.children.get_mut(track_index) else {
2464 return false;
2465 };
2466 if item_index >= track.items.len() {
2467 return false;
2468 }
2469 let removed = track.items.remove(item_index);
2470 let duration = removed.duration().max(0.0);
2471 if replace_with_gap && duration > EPS {
2472 let mut gap = Item::Gap(crate::Gap::make_gap(duration));
2473 Self::ensure_unique_item_id(&mut gap, used_ids);
2474 track.items.insert(item_index.min(track.items.len()), gap);
2475 track.merge_adjacent_gaps();
2476 }
2477 true
2478 }
2479
2480 fn synced_move_items(&self, item_id: &str) -> Option<Vec<SyncedMoveItem>> {
2481 let (selected_track_index, selected_item_index, selected_item) = self.get_item(item_id)?;
2482 let sync_clips_id = match selected_item {
2483 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
2484 Item::Gap(_) => None,
2485 }?;
2486 let selected_start = self.children[selected_track_index]
2487 .start_time_of_item(selected_item_index);
2488 let selected_duration = selected_item.duration().max(0.0);
2489 if selected_duration <= EPS {
2490 return None;
2491 }
2492 let targets =
2493 self.sync_column_targets_at(sync_clips_id, selected_start, selected_duration);
2494 if targets.len() <= 1 {
2495 return None;
2496 }
2497
2498 let has_misaligned_video = self
2501 .synced_clips_targets(sync_clips_id)
2502 .into_iter()
2503 .any(|(track_index, item_index)| {
2504 self.children
2505 .get(track_index)
2506 .is_some_and(|track| track.kind == TrackKind::Video)
2507 && !targets
2508 .iter()
2509 .any(|(ti, ii)| ti == &track_index && ii == &item_index)
2510 });
2511 if has_misaligned_video {
2512 return None;
2513 }
2514
2515 let mut items = Vec::new();
2516 for (track_index, item_index) in targets {
2517 let track_kind = self.children.get(track_index)?.kind.clone();
2518 let item = self
2519 .children
2520 .get(track_index)?
2521 .items
2522 .get(item_index)?
2523 .clone();
2524 let is_selected =
2525 track_index == selected_track_index && item_index == selected_item_index;
2526 items.push(SyncedMoveItem {
2527 track_index,
2528 item_index,
2529 track_kind,
2530 item,
2531 is_selected,
2532 });
2533 }
2534
2535 Some(items)
2536 }
2537
2538 fn partner_move_target_allowed(
2546 &self,
2547 selected: &SyncedMoveItem,
2548 column_has_video: bool,
2549 dest_track_index: usize,
2550 start: Seconds,
2551 end: Seconds,
2552 overlap_policy: OverlapPolicy,
2553 ) -> bool {
2554 let Some(dest) = self.children.get(dest_track_index) else {
2555 return false;
2556 };
2557 if selected.track_kind == TrackKind::Video {
2558 return dest.kind == TrackKind::Video;
2559 }
2560 if selected.track_kind != TrackKind::Audio || dest.kind != TrackKind::Audio {
2561 return selected.track_kind == dest.kind;
2562 }
2563 if dest_track_index == selected.track_index {
2564 return true;
2565 }
2566 let sync_clips_id = match &selected.item {
2567 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
2568 Item::Gap(_) => None,
2569 };
2570 let source_owner = self.group_owner_of(selected.track_index);
2571 let dest_owner = self.group_owner_of(dest_track_index);
2572 if source_owner == dest_owner {
2573 if overlap_policy == OverlapPolicy::Push {
2574 return true;
2575 }
2576 let allowed: HashSet<i64> = sync_clips_id.into_iter().collect();
2577 return self.range_only_holds_sync_clips(dest_track_index, start, end, &allowed);
2578 }
2579 if column_has_video {
2580 return false;
2583 }
2584 !self.track_in_video_group(dest_track_index)
2586 }
2587
2588 fn is_intra_cluster_sync_move(
2589 &self,
2590 dest_track_index: usize,
2591 items_to_move: &[SyncedMoveItem],
2592 ) -> bool {
2593 let cluster: HashSet<usize> = self
2594 .boundary_group_indices(dest_track_index)
2595 .into_iter()
2596 .collect();
2597 cluster.contains(&dest_track_index)
2598 && items_to_move
2599 .iter()
2600 .all(|item| cluster.contains(&item.track_index))
2601 }
2602
2603 fn move_landing_is_free(
2606 &self,
2607 track_index: usize,
2608 start: Seconds,
2609 duration: Seconds,
2610 moving_ids: &[String],
2611 ) -> bool {
2612 let Some(track) = self.children.get(track_index) else {
2613 return true;
2614 };
2615 let end = start + duration.max(0.0);
2616 let mut cursor = 0.0;
2617 track.items.iter().all(|item| {
2618 let (item_start, item_end) = (cursor, cursor + item.duration());
2619 cursor = item_end;
2620 !matches!(item, Item::Clip(_))
2621 || item_end <= start + EPS
2622 || item_start >= end - EPS
2623 || item.get_id().is_some_and(|id| moving_ids.contains(&id))
2624 })
2625 }
2626
2627 fn stack_item_start_time(&self, item_id: &str) -> Option<Seconds> {
2628 let (track_index, item_index, _) = self.get_item(item_id)?;
2629 Some(self.children[track_index].start_time_of_item(item_index))
2630 }
2631
2632 fn move_snap_to_clip_at(
2636 &self,
2637 track_index: usize,
2638 time: Seconds,
2639 insert_policy: InsertPolicy,
2640 moving_ids: &[String],
2641 ) -> Option<Seconds> {
2642 let track = self.children.get(track_index)?;
2643 let mut cursor = 0.0;
2644 track.items.iter().find_map(|item| {
2645 let (start, end) = (cursor, cursor + item.duration());
2646 cursor = end;
2647 let contains = matches!(item, Item::Clip(_))
2648 && start + EPS < time
2649 && time < end - EPS
2650 && !item.get_id().is_some_and(|id| moving_ids.contains(&id));
2651 if !contains {
2652 return None;
2653 }
2654 match insert_policy {
2655 InsertPolicy::SplitAndInsert => None,
2656 InsertPolicy::InsertBefore => Some(start),
2657 InsertPolicy::InsertAfter => Some(end),
2658 InsertPolicy::InsertBeforeOrAfter => {
2659 Some(if time - start <= end - time { start } else { end })
2660 }
2661 }
2662 })
2663 }
2664
2665 fn effective_move_insert_policy(insert_policy: InsertPolicy, landing_is_free: bool) -> InsertPolicy {
2669 if landing_is_free {
2670 InsertPolicy::SplitAndInsert
2671 } else {
2672 insert_policy
2673 }
2674 }
2675
2676 fn effective_move_overlap_policy(
2681 overlap_policy: OverlapPolicy,
2682 previous_start: Option<Seconds>,
2683 dest_time: Seconds,
2684 landing_is_free: bool,
2685 ) -> OverlapPolicy {
2686 if overlap_policy == OverlapPolicy::Push
2687 && landing_is_free
2688 && previous_start.is_some_and(|previous| (dest_time - previous).abs() > EPS)
2689 {
2690 OverlapPolicy::Override
2691 } else {
2692 overlap_policy
2693 }
2694 }
2695
2696 fn linked_item_ids_for_move(&self, item_timeline_id: &str, item_to_move: &Item) -> Vec<String> {
2697 let Some((primary_track_index, primary_item_index, _)) = self.get_item(item_timeline_id)
2698 else {
2699 return Vec::new();
2700 };
2701 let primary_start = self.children[primary_track_index]
2702 .start_time_of_item(primary_item_index);
2703 let primary_duration = item_to_move.duration().max(0.0);
2704 let primary_sync_id = match item_to_move {
2705 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
2706 Item::Gap(_) => None,
2707 };
2708
2709 let mut selected_ids = HashSet::from([item_timeline_id.to_string()]);
2710 let mut selected_link_group_ids = HashSet::<i64>::new();
2711 if let Some(link_group_id) = item_link_group_id(item_to_move) {
2712 selected_link_group_ids.insert(link_group_id);
2713 }
2714 if selected_link_group_ids.is_empty() {
2715 return Vec::new();
2716 }
2717
2718 loop {
2719 let mut changed = false;
2720 for (track_index, track) in self.children.iter().enumerate() {
2721 let mut pos = 0.0;
2722 for (item_index, item) in track.items.iter().enumerate() {
2723 if !matches!(item, Item::Clip(_)) {
2724 pos += item.duration().max(0.0);
2725 continue;
2726 }
2727 let linked = item_link_group_id(item)
2728 .is_some_and(|id| selected_link_group_ids.contains(&id));
2729 if !linked {
2730 pos += item.duration().max(0.0);
2731 continue;
2732 }
2733 if let Some(sync_id) = primary_sync_id {
2734 if resolve_sync_clips_id(match item {
2735 Item::Clip(clip) => &clip.metadata,
2736 Item::Gap(_) => {
2737 pos += item.duration().max(0.0);
2738 continue;
2739 }
2740 }) == Some(sync_id)
2741 && !self.item_occupies_column(
2742 track_index,
2743 item_index,
2744 primary_start,
2745 primary_duration,
2746 )
2747 && self.is_independent_sync_column(
2748 sync_id,
2749 pos,
2750 item.duration().max(0.0),
2751 )
2752 {
2753 pos += item.duration().max(0.0);
2754 continue;
2755 }
2756 }
2757 let Some(item_id) = item.get_id() else {
2758 pos += item.duration().max(0.0);
2759 continue;
2760 };
2761 if selected_ids.insert(item_id) {
2762 changed = true;
2763 }
2764 if let Some(link_group_id) = item_link_group_id(item) {
2765 changed |= selected_link_group_ids.insert(link_group_id);
2766 }
2767 pos += item.duration().max(0.0);
2768 }
2769 }
2770 if !changed {
2771 break;
2772 }
2773 }
2774
2775 let mut linked_ids = Vec::new();
2776 for track in &self.children {
2777 for item in &track.items {
2778 let Some(item_id) = item.get_id() else {
2779 continue;
2780 };
2781 if item_id != item_timeline_id && selected_ids.contains(&item_id) {
2782 linked_ids.push(item_id);
2783 }
2784 }
2785 }
2786 linked_ids
2787 }
2788
2789 fn sync_clips_id_from_insert_result(
2793 &self,
2794 result: &InsertItemAtTimeResult,
2795 ) -> Option<i64> {
2796 match result {
2797 InsertItemAtTimeResult::Synced(synced) => synced.sync_clips_id,
2798 InsertItemAtTimeResult::ItemId(id) => self
2799 .get_item(id)
2800 .and_then(|(_, _, item)| item_link_group_id(item)),
2801 }
2802 }
2803
2804 fn place_link_group_video_partner(
2805 &mut self,
2806 dest_audio_track_index: usize,
2807 dest_time: Seconds,
2808 video_item: Item,
2809 sync_clips_id: Option<i64>,
2810 preferred_cluster_video_id: Option<&str>,
2811 insert_policy: InsertPolicy,
2812 overlap_policy: OverlapPolicy,
2813 ) -> bool {
2814 let video_duration = video_item.duration().max(0.0);
2815 if video_duration <= EPS {
2816 return false;
2817 }
2818 let Some(start) = insertion_start_or_end_for_policy(
2819 &self.children[dest_audio_track_index],
2820 dest_time,
2821 insert_policy,
2822 ) else {
2823 return false;
2824 };
2825 let end_time = start + video_duration;
2826
2827 let mut created_track_indices = Vec::new();
2828 let Some(video_track_index) = self.pick_video_partner_track(
2829 dest_audio_track_index,
2830 start,
2831 end_time,
2832 preferred_cluster_video_id,
2833 &[],
2834 sync_clips_id,
2835 overlap_policy,
2836 insert_policy,
2837 &mut created_track_indices,
2838 ) else {
2839 return false;
2840 };
2841
2842 let mut used_ids = self.collect_timeline_ids();
2843 let mut video_item = video_item;
2844 Self::set_item_sync_clips(&mut video_item, sync_clips_id);
2845 Self::ensure_unique_item_id(&mut video_item, &mut used_ids);
2846
2847 self.children[video_track_index].insert_at_time(
2848 dest_time,
2849 video_item,
2850 overlap_policy,
2851 insert_policy,
2852 );
2853 true
2854 }
2855
2856 fn move_linked_items_at_time(
2857 &mut self,
2858 item_id: &str,
2859 dest_track_id: &str,
2860 dest_time: Seconds,
2861 replace_with_gap: bool,
2862 insert_policy: InsertPolicy,
2863 overlap_policy: OverlapPolicy,
2864 ) -> bool {
2865 let item_to_move = match self.get_item(item_id) {
2866 Some((_ti, _ii, item)) => item.clone(),
2867 None => return false,
2868 };
2869 let dest_track_index = match self.get_track_by_id(dest_track_id) {
2870 Some((index, _)) => index,
2871 None => return false,
2872 };
2873 let backup = self.clone();
2874 let primary_start_time = backup
2875 .stack_item_start_time(item_id)
2876 .unwrap_or(dest_time);
2877 let landing_is_free = backup.move_landing_is_free(
2878 dest_track_index,
2879 dest_time,
2880 item_to_move.duration(),
2881 &[item_id.to_string()],
2882 );
2883 let overlap_policy = if replace_with_gap {
2887 Self::effective_move_overlap_policy(
2888 overlap_policy,
2889 Some(primary_start_time),
2890 dest_time,
2891 landing_is_free,
2892 )
2893 } else {
2894 overlap_policy
2895 };
2896 let insert_policy = Self::effective_move_insert_policy(insert_policy, landing_is_free);
2897 let preferred_cluster_video_id = backup
2898 .get_track_by_id(dest_track_id)
2899 .map(|(dest_track_index, _)| dest_track_index)
2900 .and_then(|dest_track_index| {
2901 backup
2902 .boundary_group_indices(dest_track_index)
2903 .into_iter()
2904 .find_map(|track_index| {
2905 let track = backup.children.get(track_index)?;
2906 (track.kind == TrackKind::Video)
2907 .then(|| track.get_id().clone())
2908 .flatten()
2909 })
2910 });
2911 let linked_item_ids = backup.linked_item_ids_for_move(item_id, &item_to_move);
2912 let mut ids_to_remove: Vec<String> = std::iter::once(item_id.to_string())
2913 .chain(linked_item_ids.into_iter())
2914 .collect();
2915 ids_to_remove.sort_by(|left, right| {
2916 let left_pos = backup.get_item(left).map(|(ti, ii, _)| (ti, ii));
2917 let right_pos = backup.get_item(right).map(|(ti, ii, _)| (ti, ii));
2918 right_pos.cmp(&left_pos)
2919 });
2920 ids_to_remove.dedup();
2921
2922 let mut removed_items = Vec::new();
2923 for remove_id in ids_to_remove {
2924 if let Some(removed) = self.delete_one_item(&remove_id, replace_with_gap) {
2925 removed_items.push(removed);
2926 }
2927 }
2928 if removed_items.is_empty() {
2929 return false;
2930 }
2931
2932 let mut linked_audio_items = Vec::new();
2933 let mut preferred_audio_track_indices = Vec::new();
2934 let mut linked_video_item = None;
2935 let dest_is_audio = self.children.get(dest_track_index).is_some_and(|track| {
2936 track.kind == TrackKind::Audio
2937 });
2938 for (source_track_index, item) in removed_items {
2939 if item
2940 .get_id()
2941 .as_deref()
2942 .is_some_and(|id| id == item_id)
2943 {
2944 continue;
2945 }
2946 let item_destination_time = item
2947 .get_id()
2948 .as_deref()
2949 .and_then(|id| backup.stack_item_start_time(id))
2950 .map(|start_time| dest_time + start_time - primary_start_time)
2951 .unwrap_or(dest_time);
2952 let source_track_kind = backup
2953 .children
2954 .get(source_track_index)
2955 .map(|track| track.kind.clone());
2956 let is_same_time = (item_destination_time - dest_time).abs() <= EPS;
2957 let is_same_time_audio_clip = is_same_time
2958 && matches!(item, Item::Clip(_))
2959 && matches!(source_track_kind, Some(TrackKind::Audio));
2960 if is_same_time_audio_clip {
2961 if let Item::Clip(clip) = item {
2962 linked_audio_items.push(Item::Clip(clip));
2963 preferred_audio_track_indices.push(source_track_index);
2964 }
2965 } else if dest_is_audio
2966 && matches!(source_track_kind, Some(TrackKind::Video))
2967 && matches!(item, Item::Clip(_))
2968 {
2969 if linked_video_item.is_some()
2975 || backup.group_owner_of(dest_track_index) != source_track_index
2976 {
2977 *self = backup;
2978 return false;
2979 }
2980 linked_video_item = Some(item);
2981 } else if is_same_time && matches!(item, Item::Gap(_)) {
2982 } else if let Some(source_track_id) = backup
2984 .children
2985 .get(source_track_index)
2986 .and_then(|track| track.get_id())
2987 {
2988 let Some((target_track_index, _)) = self.get_track_by_id(&source_track_id) else {
2989 *self = backup;
2990 return false;
2991 };
2992 self.insert_at_time_with_sync_splits(
2993 target_track_index,
2994 item_destination_time,
2995 item,
2996 overlap_policy,
2997 insert_policy,
2998 );
2999 }
3000 }
3001
3002 let has_synced_partners =
3003 !linked_audio_items.is_empty() || linked_video_item.is_some();
3004 let insert_result = if !has_synced_partners {
3005 self.insert_synced_item_at_time(
3006 dest_track_index,
3007 dest_time,
3008 None,
3009 item_to_move,
3010 overlap_policy,
3011 insert_policy,
3012 None,
3013 None,
3014 false,
3015 None::<&str>,
3016 None::<&[usize]>,
3017 None::<&[usize]>,
3018 )
3019 } else {
3020 self.insert_synced_item_at_time(
3021 dest_track_index,
3022 dest_time,
3023 None,
3024 item_to_move,
3025 overlap_policy,
3026 insert_policy,
3027 (!linked_audio_items.is_empty()).then_some(linked_audio_items),
3028 None,
3029 linked_video_item.is_some(),
3030 preferred_cluster_video_id.as_deref(),
3031 (!preferred_audio_track_indices.is_empty())
3032 .then_some(preferred_audio_track_indices.as_slice()),
3033 None::<&[usize]>,
3034 )
3035 };
3036 if has_synced_partners {
3037 let Some(insert_result) = insert_result else {
3038 *self = backup;
3039 return false;
3040 };
3041 if let Some(video_item) = linked_video_item {
3042 let sync_clips_id = self.sync_clips_id_from_insert_result(&insert_result);
3043 let dest_audio_track_index = self
3044 .get_track_by_id(dest_track_id)
3045 .map(|(index, _)| index)
3046 .unwrap_or(dest_track_index);
3047 if !self.place_link_group_video_partner(
3048 dest_audio_track_index,
3049 dest_time,
3050 video_item,
3051 sync_clips_id,
3052 preferred_cluster_video_id.as_deref(),
3053 insert_policy,
3054 overlap_policy,
3055 ) {
3056 *self = backup;
3057 return false;
3058 }
3059 }
3060 } else if insert_result.is_none() {
3061 *self = backup;
3062 return false;
3063 }
3064
3065 self.sanitize();
3066 true
3067 }
3068
3069 fn collect_placeholder_gap_ids(&self, targets: &[(usize, usize)]) -> Vec<String> {
3070 let mut gap_ids = Vec::new();
3071 for (track_index, item_index) in targets {
3072 let Some(track) = self.children.get(*track_index) else {
3073 continue;
3074 };
3075 let Some(item) = track.items.get(*item_index) else {
3076 continue;
3077 };
3078 if matches!(item, Item::Gap(_)) {
3079 if let Some(id) = item.get_id() {
3080 gap_ids.push(id);
3081 }
3082 }
3083 }
3084 gap_ids
3085 }
3086
3087 fn remove_gaps_by_id(&mut self, gap_ids: &[String]) {
3088 let mut gap_ids: Vec<_> = gap_ids.to_vec();
3089 gap_ids.sort();
3090 gap_ids.dedup();
3091 for gap_id in gap_ids {
3092 let Some((track_index, item_index, item)) = self.get_item(&gap_id) else {
3093 continue;
3094 };
3095 if !matches!(item, Item::Gap(_)) {
3096 continue;
3097 }
3098 let Some(track) = self.children.get_mut(track_index) else {
3099 continue;
3100 };
3101 if item_index < track.items.len() {
3102 track.items.remove(item_index);
3103 track.merge_adjacent_gaps();
3104 }
3105 }
3106 }
3107
3108 fn replace_synced_item_via_insert(
3109 &mut self,
3110 items: Vec<SyncedMoveItem>,
3111 dest_track_index: usize,
3112 dest_time: Seconds,
3113 item: Item,
3114 synced_audio_clips: Option<Vec<Item>>,
3115 ) -> bool {
3116 let backup = self.clone();
3117 if dest_track_index >= self.children.len() {
3118 return false;
3119 }
3120
3121 let Some(selected) = items.iter().find(|item| item.is_selected) else {
3122 return false;
3123 };
3124 let Some(selected_id) = selected.item.get_id() else {
3125 return false;
3126 };
3127
3128 let synced_audio_input_provided = synced_audio_clips.is_some();
3129 let synced_inputs = Self::normalize_synced_inputs(synced_audio_clips, None);
3130 if !matches!(item, Item::Clip(_)) {
3131 return false;
3132 }
3133 let mut primary_item = item;
3134 primary_item.clamp_to_active_available_range();
3135 primary_item.set_id(Some(selected_id.clone()));
3136 let replacement_duration = primary_item.duration().max(0.0);
3137 if replacement_duration <= EPS {
3138 return false;
3139 }
3140 if synced_audio_input_provided
3141 && !Self::synced_inputs_match_duration(replacement_duration, &synced_inputs)
3142 {
3143 return false;
3144 }
3145
3146 let mut synced_audio = Vec::new();
3147 let mut preferred_audio_track_indices = Vec::new();
3148 let mut gap_only_audio_tracks = Vec::new();
3149 let mut synced_video = None;
3150 let mut audio_partners: Vec<_> = items
3151 .iter()
3152 .filter(|item| !item.is_selected && item.track_kind == TrackKind::Audio)
3153 .collect();
3154 let member_tracks: Vec<usize> = items.iter().map(|item| item.track_index).collect();
3155 let owner = self
3156 .column_owner_track_index(&member_tracks)
3157 .unwrap_or(selected.track_index);
3158 audio_partners.sort_by_key(|item| self.partner_slot_order(owner, item.track_index));
3159
3160 for item in &items {
3161 if item.is_selected {
3162 continue;
3163 }
3164 if item.track_kind == TrackKind::Video {
3165 let mut video_item = item.item.clone();
3166 video_item.set_duration(replacement_duration);
3167 synced_video = Some(video_item);
3168 }
3169 }
3170
3171 if synced_audio_input_provided {
3172 let mut synced_audio_inputs = synced_inputs.audio.into_iter();
3173 for partner in audio_partners {
3174 if let Some(mut next_audio) = synced_audio_inputs.next() {
3175 if let Some(audio_id) = partner.item.get_id() {
3176 next_audio.set_id(Some(audio_id));
3177 }
3178 next_audio.set_duration(replacement_duration);
3179 synced_audio.push(next_audio);
3180 preferred_audio_track_indices.push(partner.track_index);
3181 } else {
3182 gap_only_audio_tracks.push(partner.track_index);
3183 }
3184 }
3185 for audio_item in synced_audio_inputs {
3186 synced_audio.push(audio_item);
3187 }
3188 } else {
3189 for item in &items {
3190 if item.is_selected || item.track_kind != TrackKind::Audio {
3191 continue;
3192 }
3193 let mut audio_item = item.item.clone();
3194 audio_item.set_duration(replacement_duration);
3195 synced_audio.push(audio_item);
3196 preferred_audio_track_indices.push(item.track_index);
3197 }
3198 }
3199
3200 let Some(targets) = self.delete_item_targets(&selected_id) else {
3201 return false;
3202 };
3203 let removed = self.delete_clips_at_indices(targets.clone(), true);
3204 if removed.is_empty() {
3205 return false;
3206 }
3207 let placeholder_gap_ids = self.collect_placeholder_gap_ids(&targets);
3208
3209 let synced_audio_clips = (!synced_audio.is_empty()).then_some(synced_audio);
3210 let gap_only_exclude = (!gap_only_audio_tracks.is_empty())
3211 .then_some(gap_only_audio_tracks.as_slice());
3212 if self
3213 .insert_synced_item_at_time(
3214 dest_track_index,
3215 dest_time,
3216 None,
3217 primary_item,
3218 OverlapPolicy::Override,
3219 InsertPolicy::SplitAndInsert,
3220 synced_audio_clips,
3221 synced_video,
3222 false,
3223 None,
3224 (!preferred_audio_track_indices.is_empty())
3225 .then_some(preferred_audio_track_indices.as_slice()),
3226 gap_only_exclude,
3227 )
3228 .is_some()
3229 {
3230 self.remove_gaps_by_id(&placeholder_gap_ids);
3231 let mut used_ids = self.collect_timeline_ids();
3232 for &track_index in &gap_only_audio_tracks {
3233 let start = dest_time;
3234 let end = dest_time + replacement_duration;
3235 let Some(track) = self.children.get_mut(track_index) else {
3236 *self = backup;
3237 return false;
3238 };
3239 track.delete_range(start, end, false);
3240 let mut gap = Item::Gap(Gap::make_gap(replacement_duration));
3241 Self::ensure_unique_item_id(&mut gap, &mut used_ids);
3242 let result = track.insert_at_time(
3243 start,
3244 gap,
3245 OverlapPolicy::Override,
3246 InsertPolicy::InsertBefore,
3247 );
3248 if !result.success {
3249 *self = backup;
3250 return false;
3251 }
3252 }
3253 self.sanitize_preserving_all_gap_tracks();
3254 true
3255 } else {
3256 *self = backup;
3257 false
3258 }
3259 }
3260
3261 fn move_synced_items_at_time_via_insert(
3262 &mut self,
3263 items_to_move: Vec<SyncedMoveItem>,
3264 dest_track_index: usize,
3265 dest_time: Seconds,
3266 replace_with_gap: bool,
3267 insert_policy: InsertPolicy,
3268 overlap_policy: OverlapPolicy,
3269 ) -> bool {
3270 let backup = self.clone();
3271 if dest_track_index >= self.children.len() {
3272 return false;
3273 }
3274
3275 let Some(selected) = items_to_move.iter().find(|item| item.is_selected) else {
3276 return false;
3277 };
3278 let Some(item_id) = selected.item.get_id() else {
3279 return false;
3280 };
3281 let primary_item = selected.item.clone();
3282 let moved_end = dest_time + primary_item.duration().max(0.0);
3283 let column_has_video = items_to_move
3284 .iter()
3285 .any(|item| item.track_kind == TrackKind::Video);
3286 if !self.partner_move_target_allowed(
3287 selected,
3288 column_has_video,
3289 dest_track_index,
3290 dest_time,
3291 moved_end,
3292 overlap_policy,
3293 ) {
3294 return false;
3295 }
3296
3297 let preferred_video_track_id = items_to_move
3302 .iter()
3303 .find(|item| item.track_kind == TrackKind::Video)
3304 .and_then(|item| self.children[item.track_index].get_id());
3305
3306 let mut synced_audio = Vec::new();
3307 let mut preferred_audio_track_indices = Vec::new();
3308 let mut synced_video = None;
3309 let mut audio_partners: Vec<_> = items_to_move
3310 .iter()
3311 .filter(|item| !item.is_selected && item.track_kind == TrackKind::Audio)
3312 .collect();
3313 let member_tracks: Vec<usize> = items_to_move.iter().map(|item| item.track_index).collect();
3314 let owner = self
3315 .column_owner_track_index(&member_tracks)
3316 .unwrap_or(selected.track_index);
3317 audio_partners.sort_by_key(|item| self.partner_slot_order(owner, item.track_index));
3318 for item in &items_to_move {
3319 if item.is_selected {
3320 continue;
3321 }
3322 match item.track_kind {
3323 TrackKind::Audio => continue,
3324 TrackKind::Video => {
3325 if synced_video.is_some() {
3326 return false;
3327 }
3328 synced_video = Some(item.item.clone());
3329 }
3330 TrackKind::Other => return false,
3331 }
3332 }
3333 for item in audio_partners {
3334 synced_audio.push(item.item.clone());
3335 preferred_audio_track_indices.push(if item.track_index == dest_track_index {
3338 selected.track_index
3339 } else {
3340 item.track_index
3341 });
3342 }
3343 let move_previous_start = backup.children[selected.track_index]
3344 .start_time_of_item(selected.item_index);
3345 let moving_ids: Vec<String> = items_to_move
3346 .iter()
3347 .filter_map(|item| item.item.get_id())
3348 .collect();
3349 let partners_change_tracks =
3355 selected.track_kind == TrackKind::Video && dest_track_index != selected.track_index;
3356 let column_tracks: Vec<usize> = std::iter::once(dest_track_index)
3357 .chain(
3358 items_to_move
3359 .iter()
3360 .filter(|item| !item.is_selected && !partners_change_tracks)
3361 .map(|item| item.track_index),
3362 )
3363 .collect();
3364 let landing_is_free = column_tracks.iter().all(|&track_index| {
3365 backup.move_landing_is_free(
3366 track_index,
3367 dest_time,
3368 selected.item.duration(),
3369 &moving_ids,
3370 )
3371 });
3372 let requested_overlap_policy = overlap_policy;
3373 let mut overlap_policy = Self::effective_move_overlap_policy(
3374 overlap_policy,
3375 Some(move_previous_start),
3376 dest_time,
3377 landing_is_free,
3378 );
3379 let dest_time = if landing_is_free {
3383 dest_time
3384 } else {
3385 column_tracks
3386 .iter()
3387 .find_map(|&track_index| {
3388 backup.move_snap_to_clip_at(track_index, dest_time, insert_policy, &moving_ids)
3389 })
3390 .unwrap_or(dest_time)
3391 };
3392 if dest_track_index == selected.track_index && (dest_time - move_previous_start).abs() <= EPS {
3393 return true;
3395 }
3396 let insert_policy = if landing_is_free || insert_policy != InsertPolicy::SplitAndInsert {
3397 InsertPolicy::SplitAndInsert
3398 } else {
3399 insert_policy
3400 };
3401 let move_source_track_indices: Vec<usize> =
3402 items_to_move.iter().map(|item| item.track_index).collect();
3403 let partner_ids: Vec<String> = items_to_move
3404 .iter()
3405 .filter(|item| !item.is_selected)
3406 .filter_map(|item| item.item.get_id())
3407 .collect();
3408
3409 let Some(targets) = self.delete_item_targets(&item_id) else {
3410 return false;
3411 };
3412 let ripple_in_place =
3418 !replace_with_gap && move_source_track_indices.contains(&dest_track_index);
3419 let original_dest_time = dest_time;
3420 let mut dest_time = dest_time;
3421 if ripple_in_place {
3422 let moved_duration = selected.item.duration().max(0.0);
3423 if dest_time >= move_previous_start + moved_duration - EPS {
3424 dest_time -= moved_duration;
3425 } else if dest_time > move_previous_start {
3426 dest_time = move_previous_start;
3427 }
3428 }
3429 let mut rippled = false;
3432 if !replace_with_gap {
3433 let Some((_, spans)) = self.ripple_remove_items(&targets) else {
3434 *self = backup;
3435 return false;
3436 };
3437 dest_time = spans.map_time(dest_track_index, original_dest_time);
3438 rippled = true;
3439 let landing_is_free = column_tracks.iter().all(|&track_index| {
3442 self.move_landing_is_free(track_index, dest_time, selected.item.duration(), &[])
3443 });
3444 overlap_policy = Self::effective_move_overlap_policy(
3445 requested_overlap_policy,
3446 Some(move_previous_start),
3447 dest_time,
3448 landing_is_free,
3449 );
3450 }
3451 if !rippled {
3452 let removed = self.delete_clips_at_indices(targets.clone(), !ripple_in_place);
3453 if removed.is_empty() {
3454 return false;
3455 }
3456 }
3457 let placeholder_gap_ids = if ripple_in_place || rippled {
3458 Vec::new()
3459 } else {
3460 self.collect_placeholder_gap_ids(&targets)
3461 };
3462
3463 let synced_audio_clips = (!synced_audio.is_empty()).then_some(synced_audio);
3464 if self
3465 .insert_synced_item_at_time(
3466 dest_track_index,
3467 dest_time,
3468 None,
3469 primary_item,
3470 overlap_policy,
3471 insert_policy,
3472 synced_audio_clips,
3473 synced_video,
3474 false,
3475 preferred_video_track_id.as_deref(),
3476 (!preferred_audio_track_indices.is_empty())
3477 .then_some(preferred_audio_track_indices.as_slice()),
3478 Some(move_source_track_indices.as_slice()),
3479 )
3480 .is_some()
3481 {
3482 if !replace_with_gap && !ripple_in_place && !rippled {
3483 self.remove_gaps_by_id(&placeholder_gap_ids);
3484 self.realign_moved_partners(&item_id, &partner_ids);
3485 }
3486 self.sanitize();
3487 true
3488 } else {
3489 *self = backup;
3490 false
3491 }
3492 }
3493
3494 fn realign_moved_partners(&mut self, primary_id: &str, partner_ids: &[String]) {
3499 let Some(primary_start) = self.stack_item_start_time(primary_id) else {
3500 return;
3501 };
3502 let mut used_ids = self.collect_timeline_ids();
3503 for partner_id in partner_ids {
3504 let Some((track_index, item_index, _)) = self.get_item(partner_id) else {
3505 continue;
3506 };
3507 let partner_start = self.children[track_index].start_time_of_item(item_index);
3508 let delta = primary_start - partner_start;
3509 if delta > EPS {
3510 let mut gap = Item::Gap(Gap::make_gap(delta));
3511 Self::ensure_unique_item_id(&mut gap, &mut used_ids);
3512 self.children[track_index].items.insert(item_index, gap);
3513 } else if delta < -EPS {
3514 let Some(gap_index) = item_index.checked_sub(1) else {
3515 continue;
3516 };
3517 let Some(Item::Gap(gap)) = self.children[track_index].items.get_mut(gap_index)
3518 else {
3519 continue;
3520 };
3521 let available = gap.source_range.duration.to_seconds();
3522 let remaining = available - (-delta).min(available);
3523 if remaining <= EPS {
3524 self.children[track_index].items.remove(gap_index);
3525 } else {
3526 gap.source_range.duration.set_from_seconds(remaining);
3527 }
3528 }
3529 }
3530 }
3531
3532 fn move_synced_items(
3533 &mut self,
3534 items_to_move: Vec<SyncedMoveItem>,
3535 mut dest_track_index: usize,
3536 replace_with_gap: bool,
3537 overlap_policy: OverlapPolicy,
3538 dest_index: usize,
3539 ) -> bool {
3540 let backup = self.clone();
3541 if dest_track_index >= self.children.len() {
3542 return false;
3543 }
3544
3545 let Some(selected_position) = items_to_move.iter().position(|item| item.is_selected) else {
3546 return false;
3547 };
3548 let selected_item = &items_to_move[selected_position];
3549 let selected_source_track_index = selected_item.track_index;
3550 let Some(selected_id) = selected_item.item.get_id() else {
3551 return false;
3552 };
3553 let Some(sync_clips_id) = (match &selected_item.item {
3554 Item::Clip(clip) => resolve_sync_clips_id(&clip.metadata),
3555 Item::Gap(_) => None,
3556 }) else {
3557 return false;
3558 };
3559 let moved_duration = selected_item.item.duration().max(0.0);
3560 if moved_duration <= EPS
3561 || items_to_move
3562 .iter()
3563 .any(|item| (item.item.duration().max(0.0) - moved_duration).abs() > EPS)
3564 {
3565 return false;
3566 }
3567 let current_start = self.children[dest_track_index].start_time_of_item(dest_index);
3574 {
3575 let column_has_video = items_to_move
3576 .iter()
3577 .any(|item| item.track_kind == TrackKind::Video);
3578 if !self.partner_move_target_allowed(
3579 selected_item,
3580 column_has_video,
3581 dest_track_index,
3582 current_start,
3583 current_start + moved_duration,
3584 overlap_policy,
3585 ) {
3586 return false;
3587 }
3588 }
3589
3590 let mut used_ids = self.collect_timeline_ids();
3591 let mut targets: Vec<_> = items_to_move
3592 .iter()
3593 .map(|item| (item.track_index, item.item_index))
3594 .collect();
3595 let planned_start = if replace_with_gap {
3596 targets.sort_by(|a, b| b.cmp(a));
3597 for (track_index, item_index) in targets {
3598 if !self.remove_item_at_for_move(
3599 track_index,
3600 item_index,
3601 replace_with_gap,
3602 &mut used_ids,
3603 ) {
3604 *self = backup;
3605 return false;
3606 }
3607 }
3608 current_start
3609 } else {
3610 let Some((_, spans)) = self.ripple_remove_items(&targets) else {
3613 *self = backup;
3614 return false;
3615 };
3616 spans.map_time(dest_track_index, current_start)
3617 };
3618
3619 let insert_policy_for_video = InsertPolicy::SplitAndInsert;
3620 let moved_start = planned_start;
3621
3622 let mut placements = vec![(
3623 dest_track_index,
3624 items_to_move[selected_position].item.clone(),
3625 true,
3626 )];
3627 let mut created_track_indices = Vec::new();
3628 let mut used_audio_track_indices = Vec::new();
3629 let mut used_video_track_indices = Vec::new();
3630 match self.children[dest_track_index].kind {
3631 TrackKind::Audio => used_audio_track_indices.push(dest_track_index),
3632 TrackKind::Video => used_video_track_indices.push(dest_track_index),
3633 TrackKind::Other => {}
3634 }
3635
3636 let intra_cluster_move =
3637 backup.is_intra_cluster_sync_move(dest_track_index, &items_to_move);
3638 let source_member_tracks: Vec<usize> =
3639 items_to_move.iter().map(|item| item.track_index).collect();
3640
3641 let mut sync_track_items: Vec<_> = items_to_move
3642 .into_iter()
3643 .enumerate()
3644 .filter(|(position, _)| *position != selected_position)
3645 .map(|(_, move_item)| move_item)
3646 .collect();
3647 let source_owner = backup
3649 .column_owner_track_index(&source_member_tracks)
3650 .unwrap_or(selected_source_track_index);
3651 sync_track_items.sort_by_key(|move_item| {
3652 (
3653 move_item.track_kind != TrackKind::Video,
3654 backup.partner_slot_order(source_owner, move_item.track_index),
3655 )
3656 });
3657 let mut owner_track_index = dest_track_index;
3658 let moved_end = moved_start + moved_duration;
3659 let horizontal = selected_source_track_index == dest_track_index;
3660 let mut source_track_indices: Vec<usize> = std::iter::once(selected_source_track_index)
3661 .chain(
3662 sync_track_items
3663 .iter()
3664 .map(|move_item| move_item.track_index),
3665 )
3666 .collect();
3667
3668 for position in 0..sync_track_items.len() {
3669 let move_item = sync_track_items[position].clone();
3670 if intra_cluster_move {
3671 let track_index = if move_item.track_index == dest_track_index {
3675 selected_source_track_index
3676 } else {
3677 move_item.track_index
3678 };
3679 match move_item.track_kind {
3680 TrackKind::Audio => used_audio_track_indices.push(track_index),
3681 TrackKind::Video => used_video_track_indices.push(track_index),
3682 TrackKind::Other => {
3683 *self = backup;
3684 return false;
3685 }
3686 }
3687 placements.push((track_index, move_item.item, false));
3688 continue;
3689 }
3690
3691 let track_count_before = self.children.len();
3692 let track_index = match move_item.track_kind {
3693 TrackKind::Audio => {
3694 let overridden_sync_ids: HashSet<i64> =
3695 if overlap_policy == OverlapPolicy::Override {
3696 self.sync_clips_ids_in_range(dest_track_index, moved_start, moved_end)
3697 } else {
3698 HashSet::new()
3699 };
3700 let primary = dest_track_index;
3701 let owner = owner_track_index;
3702 let usable = |stack: &Stack, index: usize| {
3703 stack.partner_track_usable(
3704 primary,
3705 index,
3706 moved_start,
3707 moved_end,
3708 overlap_policy,
3709 &overridden_sync_ids,
3710 ) || (owner != primary
3711 && stack.partner_track_usable(
3712 owner,
3713 index,
3714 moved_start,
3715 moved_end,
3716 overlap_policy,
3717 &overridden_sync_ids,
3718 ))
3719 };
3720 let track_index = self.pick_partner_track(
3721 &stack_partner_tracks::PartnerSlotRequest {
3722 primary_track_index: dest_track_index,
3723 owner_track_index,
3724 kind: TrackKind::Audio,
3725 start: moved_start,
3726 end: moved_end,
3727 used: &used_audio_track_indices,
3728 preferred: Some(if move_item.track_index == dest_track_index {
3731 selected_source_track_index
3732 } else {
3733 move_item.track_index
3734 }),
3735 horizontal,
3736 exclude_fallback: &source_track_indices,
3737 },
3738 &usable,
3739 &mut created_track_indices,
3740 );
3741 track_index
3742 }
3743 TrackKind::Video => {
3744 let Some(mut track_index) = self.pick_video_partner_track(
3745 dest_track_index,
3746 moved_start,
3747 moved_end,
3748 None,
3749 &used_video_track_indices,
3750 Some(sync_clips_id),
3751 overlap_policy,
3752 insert_policy_for_video,
3753 &mut created_track_indices,
3754 ) else {
3755 *self = backup;
3756 return false;
3757 };
3758 if used_video_track_indices.contains(&track_index) {
3759 let insert_at = track_index;
3760 self.children
3761 .insert(insert_at, self.new_numbered_track(TrackKind::Video));
3762 created_track_indices.push(insert_at);
3763 track_index = insert_at;
3764 }
3765 owner_track_index = track_index;
3766 track_index
3767 }
3768 TrackKind::Other => {
3769 *self = backup;
3770 return false;
3771 }
3772 };
3773 if self.children.len() > track_count_before {
3774 shift_track_index_after_insert(&mut dest_track_index, track_index);
3777 shift_move_placements_after_insert(&mut placements, track_index);
3778 shift_track_indices_after_insert(&mut used_audio_track_indices, track_index);
3779 shift_track_indices_after_insert(&mut used_video_track_indices, track_index);
3780 shift_track_indices_after_insert(&mut source_track_indices, track_index);
3781 if owner_track_index != track_index {
3782 shift_track_index_after_insert(&mut owner_track_index, track_index);
3783 }
3784 for pending in sync_track_items[position + 1..].iter_mut() {
3785 shift_track_index_after_insert(&mut pending.track_index, track_index);
3786 }
3787 }
3788 match move_item.track_kind {
3789 TrackKind::Audio => used_audio_track_indices.push(track_index),
3790 _ => used_video_track_indices.push(track_index),
3791 }
3792 placements.push((track_index, move_item.item, false));
3793 }
3794
3795 let mut boundary_track_indices =
3796 self.boundary_track_indices_for_anchors(&[sync_clips_id], &[dest_track_index], &[]);
3797 for (track_index, _, _) in &placements {
3798 if !boundary_track_indices.contains(track_index) {
3799 boundary_track_indices.push(*track_index);
3800 }
3801 }
3802 boundary_track_indices.sort_unstable();
3803 boundary_track_indices.dedup();
3804
3805 for track_index in boundary_track_indices {
3806 if placements
3807 .iter()
3808 .any(|(placement_track_index, _, _)| *placement_track_index == track_index)
3809 {
3810 continue;
3811 }
3812 let mut gap = Item::Gap(crate::Gap::make_gap(moved_duration));
3813 Self::ensure_unique_item_id(&mut gap, &mut used_ids);
3814 placements.push((track_index, gap, false));
3815 }
3816 placements.sort_by_key(|(track_index, _, _)| *track_index);
3817
3818 let insert_policy = InsertPolicy::SplitAndInsert;
3819 let cluster = self.boundary_group_indices(dest_track_index);
3820 let placed_ids: HashSet<String> = placements
3821 .iter()
3822 .filter_map(|(_, item, _)| item.get_id())
3823 .collect();
3824 if !self.apply_sync_splits_for_column_insert(
3825 moved_start,
3826 moved_duration,
3827 insert_policy,
3828 overlap_policy,
3829 SyncSplitIdPolicy::KeepShared,
3830 Some(&cluster),
3831 ) {
3832 *self = backup;
3833 return false;
3834 }
3835
3836 let starts_before = self.clip_starts_from(moved_start);
3837 for (track_index, item, is_selected) in placements {
3838 let at_boundary = is_selected
3845 || (intra_cluster_move
3846 && overlap_policy == OverlapPolicy::Push
3847 && moved_start <= self.children[track_index].total_duration() + EPS);
3848 if at_boundary {
3849 let track = &mut self.children[track_index];
3850 track.split_at_time(moved_start);
3851 let index = (0..=track.items.len())
3852 .find(|&index| track.start_time_of_item(index) >= moved_start - EPS)
3853 .unwrap_or(track.items.len());
3854 let _ = track.insert_at_index(index, item, overlap_policy);
3855 } else {
3856 let moved_end = moved_start + item.duration().max(0.0);
3857 if range_is_gap_backed(&self.children[track_index], moved_start, moved_end) {
3858 if !self.insert_gap_only(track_index, moved_start, item) {
3859 *self = backup;
3860 return false;
3861 }
3862 } else {
3863 self.children[track_index].insert_at_time(
3864 moved_start,
3865 item,
3866 overlap_policy,
3867 InsertPolicy::SplitAndInsert,
3868 );
3869 }
3870 }
3871 }
3872 if self.get_item(&selected_id).is_none() {
3873 *self = backup;
3874 return false;
3875 }
3876 if overlap_policy == OverlapPolicy::Push
3879 && !self.push_partners_of_shifted_clips(
3880 &starts_before,
3881 moved_start,
3882 moved_duration,
3883 &placed_ids,
3884 Some(sync_clips_id),
3885 )
3886 {
3887 *self = backup;
3888 return false;
3889 }
3890
3891 self.sanitize_preserving_all_gap_tracks();
3892 true
3893 }
3894}
3895
3896pub(super) fn range_is_gap_backed(track: &Track, start: Seconds, end: Seconds) -> bool {
3897 if start < -EPS || end < start - EPS {
3898 return false;
3899 }
3900 if end <= start + EPS {
3901 return true;
3902 }
3903
3904 let total = track.total_duration();
3905 if start >= total - EPS {
3906 return true;
3907 }
3908
3909 let mut pos: Seconds = 0.0;
3910 for item in &track.items {
3911 let item_start = pos;
3912 let item_end = pos + item.duration().max(0.0);
3913 if item_end > start + EPS && item_start < end - EPS {
3914 if !matches!(item, Item::Gap(_)) {
3915 return false;
3916 }
3917 }
3918 pos = item_end;
3919 }
3920 true
3921}
3922
3923pub(super) fn track_is_empty_boundary(track: &Track) -> bool {
3924 track.items.iter().all(|item| matches!(item, Item::Gap(_)))
3925}
3926
3927fn range_has_blocking_clip(
3928 track: &Track,
3929 start: Seconds,
3930 end: Seconds,
3931 sync_clips_id: Option<i64>,
3932) -> bool {
3933 if start < -EPS || end < start - EPS {
3934 return true;
3935 }
3936 if end <= start + EPS {
3937 return false;
3938 }
3939
3940 let total = track.total_duration();
3941 if start >= total - EPS {
3942 return false;
3943 }
3944
3945 let mut pos: Seconds = 0.0;
3946 for item in &track.items {
3947 let item_start = pos;
3948 let item_end = pos + item.duration().max(0.0);
3949 if item_end > start + EPS && item_start < end - EPS {
3950 match item {
3951 Item::Gap(_) => {}
3952 Item::Clip(clip)
3953 if sync_clips_id.is_some()
3954 && resolve_sync_clips_id(&clip.metadata) == sync_clips_id => {}
3955 Item::Clip(_) => return true,
3956 }
3957 }
3958 pos = item_end;
3959 }
3960 false
3961}
3962
3963pub(super) fn split_gap_boundary(track: &mut Track, time: Seconds) {
3964 let Some(index) = track.get_item_at_time(time) else {
3965 return;
3966 };
3967 let item_start = track.start_time_of_item(index);
3968 let local = time - item_start;
3969 if local <= EPS {
3970 return;
3971 }
3972
3973 let mut gap = match track.items.remove(index) {
3974 Item::Gap(gap) => gap,
3975 other => {
3976 track.items.insert(index, other);
3977 return;
3978 }
3979 };
3980 let total = gap.source_range.duration.to_seconds().max(0.0);
3981 if local >= total - EPS {
3982 track.items.insert(index, Item::Gap(gap));
3983 return;
3984 }
3985
3986 let mut left = gap.clone();
3987 left.source_range.duration.set_from_seconds(local.max(0.0));
3988 gap.source_range
3989 .duration
3990 .set_from_seconds((total - local).max(0.0));
3991 gap.set_id(Some(crate::types::gen_hex_id_12()));
3992 track.items.insert(index, Item::Gap(left));
3993 track.items.insert(index + 1, Item::Gap(gap));
3994}
3995
3996fn clamp_clip_to_active_available_range(clip: &mut Clip) {
3997 clip.clamp_to_active_available_range();
3998}
3999
4000fn resize_effective_duration(
4001 item: &Item,
4002 requested_duration: Seconds,
4003 clamp_to_media: bool,
4004) -> Seconds {
4005 let mut item = item.clone();
4006 item.set_duration(requested_duration.max(0.0));
4007 if clamp_to_media {
4008 if let Item::Clip(clip) = &mut item {
4009 clamp_clip_to_active_available_range(clip);
4010 }
4011 }
4012 item.duration().max(0.0)
4013}
4014
4015fn item_source_start(item: &Item) -> Seconds {
4016 match item {
4017 Item::Clip(clip) => clip.source_range.start_time.to_seconds(),
4018 Item::Gap(gap) => gap.source_range.start_time.to_seconds(),
4019 }
4020}
4021
4022fn set_item_source_start(item: &mut Item, source_start_time: Seconds) {
4023 match item {
4024 Item::Clip(clip) => {
4025 clip.source_range
4026 .start_time
4027 .set_from_seconds(source_start_time);
4028 }
4029 Item::Gap(gap) => {
4030 gap.source_range
4031 .start_time
4032 .set_from_seconds(source_start_time);
4033 }
4034 }
4035}
4036
4037fn replace_track_range_with_item(
4038 track: &mut Track,
4039 range_start: Seconds,
4040 range_end: Seconds,
4041 item: Item,
4042) {
4043 let start = range_start.max(0.0);
4044 let end = range_end.max(start);
4045
4046 track.split_at_time(start);
4047 let end_split = track.split_at_time(end);
4048
4049 let start_index = track.get_item_at_time(start).unwrap_or(track.items.len());
4050 let end_index = track.get_item_at_time(end).unwrap_or(track.items.len());
4051
4052 if end_index > start_index {
4053 track.items.drain(start_index..end_index);
4054 }
4055 track.items.insert(start_index, item);
4056 if let Some(mut split) = end_split {
4058 track.restore_trimmed_head_id(&mut split);
4059 }
4060 track.sanitize_preserving_all_gap_track();
4061}
4062
4063fn insertion_start_or_end_for_policy(
4064 track: &Track,
4065 insert_time: Seconds,
4066 insert_policy: InsertPolicy,
4067) -> Option<Seconds> {
4068 let total = track.total_duration();
4069 let mut effective_time = insert_time;
4070 if effective_time < 0.0 {
4071 effective_time = total - effective_time;
4072 }
4073 if effective_time < 0.0 {
4074 return None;
4075 }
4076 if effective_time >= total - EPS {
4077 return Some(effective_time);
4078 }
4079
4080 let Some(item_index) = track.get_item_at_time(effective_time) else {
4081 return Some(effective_time);
4082 };
4083 let item_start = track.start_time_of_item(item_index);
4084 let item_end = item_start + track.items[item_index].duration().max(0.0);
4085 let start = match insert_policy {
4086 InsertPolicy::SplitAndInsert => effective_time,
4087 InsertPolicy::InsertBefore => item_start,
4088 InsertPolicy::InsertAfter => item_end,
4089 InsertPolicy::InsertBeforeOrAfter => {
4090 let d_start = (effective_time - item_start).abs();
4091 let d_end = (item_end - effective_time).abs();
4092 if d_start <= d_end {
4093 item_start
4094 } else {
4095 item_end
4096 }
4097 }
4098 };
4099 Some(start)
4100}
4101
4102fn resolve_sync_clips_id(metadata: &serde_json::Value) -> Option<i64> {
4103 Clip::resolve_otio_i64(metadata, "Link Group ID")
4104}
4105
4106pub(super) fn set_resolve_sync_clips_id(metadata: &mut serde_json::Value, sync_clips_id: i64) {
4107 if metadata.as_object().is_none() {
4108 *metadata = serde_json::Value::Object(serde_json::Map::new());
4109 }
4110 let map = metadata.as_object_mut().unwrap();
4111 let resolve = map
4112 .entry("Resolve_OTIO".to_string())
4113 .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
4114 if resolve.as_object().is_none() {
4115 *resolve = serde_json::Value::Object(serde_json::Map::new());
4116 }
4117 resolve.as_object_mut().unwrap().insert(
4118 "Link Group ID".to_string(),
4119 serde_json::Value::Number(serde_json::Number::from(sync_clips_id)),
4120 );
4121}
4122
4123fn remove_resolve_sync_clips_id(metadata: &mut serde_json::Value) -> bool {
4124 let Some(resolve) = metadata
4125 .get_mut("Resolve_OTIO")
4126 .and_then(|value| value.as_object_mut())
4127 else {
4128 return false;
4129 };
4130 resolve.remove("Link Group ID").is_some()
4131}