tellers_timeline_core/stack_methods/
stack_snap.rs1use std::collections::HashSet;
2
3use crate::metadata::{item_link_group_id, item_tellers_group_id};
4use crate::{IdMetadataExt, Item, Seconds, Stack};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum SnapEdge {
9 Start,
10 End,
11}
12
13#[derive(Debug, Clone, PartialEq)]
15pub enum SnapTarget {
16 ItemEdge {
18 item_id: Option<String>,
19 track_id: Option<String>,
20 track_index: usize,
21 edge: SnapEdge,
22 },
23 Point { index: usize },
26}
27
28#[derive(Debug, Clone, PartialEq)]
30pub struct SnapMatch {
31 pub target: SnapTarget,
32 pub target_time: Seconds,
34 pub moving_edge: SnapEdge,
36 pub delta: Seconds,
38}
39
40const SNAP_TIME_EPS: Seconds = 1e-6;
42
43impl Stack {
44 pub fn find_snap(
60 &self,
61 moving_start: Seconds,
62 moving_duration: Seconds,
63 tolerance: Seconds,
64 exclude_item_ids: &[String],
65 extra_points: &[Seconds],
66 ignored_times: &[Seconds],
67 ) -> Option<SnapMatch> {
68 if tolerance.is_nan()
69 || tolerance <= 0.0
70 || !moving_start.is_finite()
71 || !moving_duration.is_finite()
72 {
73 return None;
74 }
75 let moving_duration = moving_duration.max(0.0);
76 let moving_end = moving_start + moving_duration;
77 let excluded = self.snap_excluded_items(exclude_item_ids);
78
79 let mut best: Option<SnapMatch> = None;
80 let mut consider = |target: SnapTarget, target_time: Seconds| {
81 if ignored_times
82 .iter()
83 .any(|ignored| (ignored - target_time).abs() <= SNAP_TIME_EPS)
84 {
85 return;
86 }
87 let mut edges = vec![(SnapEdge::Start, moving_start)];
88 if moving_duration > 0.0 {
89 edges.push((SnapEdge::End, moving_end));
90 }
91 for (moving_edge, moving_time) in edges {
92 let delta = target_time - moving_time;
93 if delta.abs() > tolerance || moving_start + delta < -super::EPS {
94 continue;
95 }
96 if best.as_ref().is_some_and(|b| b.delta.abs() <= delta.abs()) {
97 continue;
98 }
99 best = Some(SnapMatch {
100 target: target.clone(),
101 target_time,
102 moving_edge,
103 delta,
104 });
105 }
106 };
107
108 for (track_index, track) in self.children.iter().enumerate() {
109 let track_id = track.get_id();
110 let mut cursor = 0.0;
111 for (item_index, item) in track.items.iter().enumerate() {
112 let start = cursor;
113 cursor += item.duration();
114 if matches!(item, Item::Gap(_)) || excluded.contains(&(track_index, item_index)) {
115 continue;
116 }
117 for (edge, time) in [(SnapEdge::Start, start), (SnapEdge::End, cursor)] {
118 consider(
119 SnapTarget::ItemEdge {
120 item_id: item.get_id(),
121 track_id: track_id.clone(),
122 track_index,
123 edge,
124 },
125 time,
126 );
127 }
128 }
129 }
130 for (index, &time) in extra_points.iter().enumerate() {
131 if time.is_finite() {
132 consider(SnapTarget::Point { index }, time);
133 }
134 }
135 best
136 }
137
138 fn snap_excluded_items(&self, exclude_item_ids: &[String]) -> HashSet<(usize, usize)> {
142 let ids: HashSet<&str> = exclude_item_ids.iter().map(String::as_str).collect();
143 let mut link_groups = HashSet::new();
144 let mut tellers_groups = HashSet::new();
145 for id in &ids {
146 if let Some((_, _, item)) = self.get_item(id) {
147 link_groups.extend(item_link_group_id(item));
148 tellers_groups.extend(item_tellers_group_id(item));
149 }
150 }
151 let mut excluded = HashSet::new();
152 for (track_index, track) in self.children.iter().enumerate() {
153 for (item_index, item) in track.items.iter().enumerate() {
154 if item.get_id().is_some_and(|id| ids.contains(id.as_str()))
155 || item_link_group_id(item).is_some_and(|g| link_groups.contains(&g))
156 || item_tellers_group_id(item).is_some_and(|g| tellers_groups.contains(&g))
157 {
158 excluded.insert((track_index, item_index));
159 }
160 }
161 }
162 excluded
163 }
164}