Skip to main content

aviutl2_alias/
track.rs

1/// トラックバーのフラグ。
2#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
3pub struct TrackFlags {
4    /// 「加速」相当。
5    ease_in: bool,
6    /// 「減速」相当。
7    ease_out: bool,
8    /// 「中間点無視」相当。
9    twopoints: bool,
10}
11
12/// トラックバーの移動単位。
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
14pub struct TrackStep(pub u64);
15
16/// トラックバーの移動単位のパースエラー。
17#[derive(Debug, Clone, thiserror::Error)]
18#[non_exhaustive]
19pub enum TrackStepParseError {
20    #[error("invalid track step precision")]
21    InvalidPrecision,
22    #[error("failed to parse track step value")]
23    ParseError(#[from] std::num::ParseFloatError),
24}
25
26impl TrackStep {
27    /// パースして精度と値を取得します。
28    pub fn parse_and_get(value: &str) -> Result<(TrackStep, f64), TrackStepParseError> {
29        let maybe_negative = value.starts_with('-');
30        let abs_value_str = if maybe_negative { &value[1..] } else { value };
31        let v: f64 = abs_value_str.parse()?;
32        let dot_index = abs_value_str.find('.');
33        let step = TrackStep(match dot_index {
34            None => 0,
35            Some(idx) => (abs_value_str.len() - idx - 1)
36                .try_into()
37                .map_err(|_| TrackStepParseError::InvalidPrecision)?,
38        });
39        let final_value = if maybe_negative { -v } else { v };
40        Ok((step, final_value))
41    }
42
43    /// 値を特定の精度に丸めて文字列化します。
44    pub fn round_to_string(&self, value: f64) -> String {
45        match self.0 {
46            0 => format!("{}", value.round() as i64),
47            precision => format!("{:.*}", precision as usize, value),
48        }
49    }
50}
51
52impl TryFrom<f64> for TrackStep {
53    type Error = ();
54
55    fn try_from(value: f64) -> Result<Self, Self::Error> {
56        if value == 1.0 {
57            return Ok(TrackStep(0));
58        }
59        if !(value > 0.0 && value < 1.0) {
60            return Err(());
61        }
62
63        let mut current = value;
64        let mut precision = 0u64;
65        loop {
66            if (current - 1.0).abs() < f64::EPSILON {
67                return Ok(TrackStep(precision));
68            }
69            if current > 1.0 || precision >= 15 {
70                return Err(());
71            }
72            current *= 10.0;
73            precision += 1;
74        }
75    }
76}
77impl From<TrackStep> for f64 {
78    fn from(step: TrackStep) -> Self {
79        10f64.powi(-(step.0 as i32))
80    }
81}
82impl TrackStep {
83    /// トラックバーのステップ値を取得します。
84    pub fn value(&self) -> f64 {
85        (*self).into()
86    }
87}
88
89impl std::fmt::Display for TrackStep {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self.0 {
92            0 => write!(f, "1"),
93            precision => write!(f, "0.{}1", "0".repeat(precision as usize - 1)),
94        }
95    }
96}
97
98/// 時間制御のカーブ。
99#[derive(Debug, Clone, PartialEq)]
100pub struct TimeCurve {
101    /// カーブの制御点。
102    pub control_points: Vec<TimeCurvePoint>,
103}
104
105impl std::fmt::Display for TimeCurve {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        if self.control_points.is_empty() {
108            return write!(f, "");
109        }
110        if self.control_points.len() == 2
111            && self.control_points[0].position == 0.0
112            && self.control_points[0].value == 0.0
113            && self.control_points[1].position == 1.0
114            && self.control_points[1].value == 1.0
115        {
116            return write!(
117                f,
118                "{},{},{},{}",
119                self.control_points[0].right_handle.0,
120                self.control_points[0].right_handle.1,
121                self.control_points[1].right_handle.0,
122                self.control_points[1].right_handle.1
123            );
124        }
125        let parts: Vec<String> = self
126            .control_points
127            .iter()
128            .map(|pt| {
129                format!(
130                    "{},{},{},{}",
131                    pt.position, pt.value, pt.right_handle.0, pt.right_handle.1
132                )
133            })
134            .collect();
135        write!(f, "{}", parts.join(","))
136    }
137}
138
139/// 時間制御カーブの制御点。
140#[derive(Debug, Clone, PartialEq)]
141pub struct TimeCurvePoint {
142    /// 時間軸の位置(0.0〜1.0)。
143    pub position: f64,
144    /// 値(0.0〜1.0)。
145    pub value: f64,
146    /// 右ハンドルの相対位置。
147    ///
148    /// # Note
149    ///
150    /// 左ハンドルはこの点の数値を反転した位置にあります。
151    pub right_handle: (f64, f64),
152}
153
154impl Default for TimeCurve {
155    fn default() -> Self {
156        TimeCurve {
157            control_points: vec![
158                TimeCurvePoint {
159                    position: 0.0,
160                    value: 0.0,
161                    right_handle: (0.25, 0.25),
162                },
163                TimeCurvePoint {
164                    position: 1.0,
165                    value: 1.0,
166                    right_handle: (0.25, 0.25),
167                },
168            ],
169        }
170    }
171}
172
173/// 時間制御カーブのパースエラー。
174#[derive(Debug, Clone, thiserror::Error)]
175#[non_exhaustive]
176pub enum TimeCurveParseError {
177    #[error("invalid format")]
178    InvalidFormat,
179
180    #[error("invalid number of components ({0})")]
181    InvalidNumComponents(usize),
182
183    #[error("value out of range")]
184    ValueOutOfRange,
185
186    #[error("control points are not in increasing order")]
187    BadPositionOrder,
188}
189
190impl std::str::FromStr for TimeCurve {
191    type Err = TimeCurveParseError;
192
193    fn from_str(s: &str) -> Result<Self, Self::Err> {
194        let parts: Vec<f64> = s
195            .split(',')
196            .map(|part| {
197                part.parse::<f64>()
198                    .map_err(|_| TimeCurveParseError::InvalidFormat)
199            })
200            .collect::<Result<_, _>>()?;
201        match parts.len() {
202            n if n % 4 != 0 => Err(TimeCurveParseError::InvalidNumComponents(n)),
203            0 => Ok(TimeCurve::default()),
204            4 => Ok(TimeCurve {
205                control_points: vec![
206                    TimeCurvePoint {
207                        position: 0.0,
208                        value: 0.0,
209                        right_handle: (parts[0], parts[1]),
210                    },
211                    TimeCurvePoint {
212                        position: 1.0,
213                        value: 1.0,
214                        right_handle: (parts[2], parts[3]),
215                    },
216                ],
217            }),
218            _ => {
219                let control_points = parts
220                    .chunks(4)
221                    .map(|chunk| {
222                        if chunk[0] < 0.0
223                            || chunk[0] > 1.0
224                            || chunk[1] < 0.0
225                            || chunk[1] > 1.0
226                            || chunk[2] < 0.0
227                        {
228                            return Err(TimeCurveParseError::ValueOutOfRange);
229                        }
230                        Ok(TimeCurvePoint {
231                            position: chunk[0],
232                            value: chunk[1],
233                            right_handle: (chunk[2], chunk[3]),
234                        })
235                    })
236                    .collect::<Result<Vec<_>, _>>()?;
237                if !control_points
238                    .windows(2)
239                    .all(|w| w[0].position < w[1].position)
240                {
241                    return Err(TimeCurveParseError::BadPositionOrder);
242                }
243                Ok(TimeCurve { control_points })
244            }
245        }
246    }
247}
248
249/// `.aup2`に保存されるトラックバー項目を表します。
250#[derive(Debug, Clone, PartialEq)]
251pub enum TrackItem {
252    /// スクリプトを伴わない単一値トラック。
253    Static(StaticTrackItem),
254    /// スクリプトや時間制御を含んだトラックバー項目。
255    Animated(AnimatedTrackItem),
256}
257
258impl std::fmt::Display for TrackItem {
259    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
260        match self {
261            TrackItem::Static(item) => write!(f, "{}", item),
262            TrackItem::Animated(item) => write!(f, "{}", item),
263        }
264    }
265}
266
267impl std::str::FromStr for TrackItem {
268    type Err = TrackItemParseError;
269
270    fn from_str(s: &str) -> Result<Self, Self::Err> {
271        if s.contains(',') {
272            let animated: AnimatedTrackItem = s.parse()?;
273            Ok(TrackItem::Animated(animated))
274        } else {
275            let static_item: StaticTrackItem = s.parse()?;
276            Ok(TrackItem::Static(static_item))
277        }
278    }
279}
280
281/// スクリプトを伴わない単一値トラックを表します。
282#[derive(Debug, Clone, PartialEq)]
283pub struct StaticTrackItem {
284    /// 移動単位。
285    pub step: TrackStep,
286
287    /// 値。
288    pub value: f64,
289}
290
291impl std::fmt::Display for StaticTrackItem {
292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293        let value_str = self.step.round_to_string(self.value);
294        write!(f, "{}", value_str)
295    }
296}
297
298impl std::str::FromStr for StaticTrackItem {
299    type Err = TrackStepParseError;
300
301    fn from_str(s: &str) -> Result<Self, Self::Err> {
302        let (step, value) = TrackStep::parse_and_get(s)?;
303        Ok(StaticTrackItem { step, value })
304    }
305}
306
307/// スクリプトや時間制御を含んだトラックバー項目です。
308#[derive(Debug, Clone, PartialEq)]
309pub struct AnimatedTrackItem {
310    /// 移動単位。
311    pub step: TrackStep,
312    /// 中間点ごとの値。
313    pub values: Vec<f64>,
314
315    /// フラグ。
316    pub flags: TrackFlags,
317    /// トラックバースクリプトの名前。
318    pub script_name: String,
319    /// 設定値。
320    pub parameter: Option<f64>,
321    /// 時間制御のカーブ。
322    pub time_curve: Option<TimeCurve>,
323}
324
325/// トラックバー項目のパースエラー。
326#[derive(Debug, Clone, thiserror::Error)]
327#[non_exhaustive]
328pub enum TrackItemParseError {
329    #[error("invalid segments count")]
330    InvalidNumSegments(usize),
331
332    #[error("invalid elements count")]
333    InvalidNumElements(usize),
334
335    #[error("failed to parse element")]
336    ElementParseError(#[from] std::num::ParseFloatError),
337
338    #[error("failed to parse curve")]
339    TimeCurveParseError(#[from] TimeCurveParseError),
340
341    #[error("invalid flag value")]
342    InvalidFlagValue,
343
344    #[error("inconsistent step")]
345    InconsistentStep,
346
347    #[error("invalid step value")]
348    InvalidStepValue(#[from] TrackStepParseError),
349}
350
351impl std::fmt::Display for AnimatedTrackItem {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        let mut elements = Vec::new();
354        for value in &self.values {
355            elements.push(self.step.round_to_string(*value));
356        }
357        elements.push(self.script_name.clone());
358        let flags_value = (if self.flags.ease_in { 0b0001 } else { 0 })
359            | (if self.flags.ease_out { 0b0010 } else { 0 })
360            | (if self.flags.twopoints { 0b0100 } else { 0 });
361        elements.push(flags_value.to_string());
362        let mut result = elements.join(",");
363        if let Some(param) = self.parameter {
364            result.push('|');
365            result.push_str(&param.to_string());
366        }
367        if let Some(curve) = &self.time_curve {
368            result.push('|');
369            result.push_str(&curve.to_string());
370        }
371        write!(f, "{}", result)
372    }
373}
374
375impl std::str::FromStr for AnimatedTrackItem {
376    type Err = TrackItemParseError;
377
378    fn from_str(s: &str) -> Result<Self, Self::Err> {
379        let segments = s.split('|').collect::<Vec<&str>>();
380        let items = segments[0].split(",").collect::<Vec<&str>>();
381        if items.len() < 4 {
382            return Err(TrackItemParseError::InvalidNumElements(items.len()));
383        }
384        let flags_value: u8 = items[items.len() - 1]
385            .parse()
386            .map_err(|_| TrackItemParseError::InvalidFlagValue)?;
387        let flags = TrackFlags {
388            ease_in: (flags_value & 0b0001) != 0,
389            ease_out: (flags_value & 0b0010) != 0,
390            twopoints: (flags_value & 0b0100) != 0,
391        };
392        let (parameter, time_curve) = match segments.len() {
393            1 => (None, None),
394            2 if segments[1].contains(',') => {
395                let time_curve: TimeCurve = segments[1]
396                    .parse()
397                    .map_err(TrackItemParseError::TimeCurveParseError)?;
398                (None, Some(time_curve))
399            }
400            2 => {
401                let parameter: f64 = segments[1]
402                    .parse()
403                    .map_err(TrackItemParseError::ElementParseError)?;
404                (Some(parameter), None)
405            }
406            3 => {
407                let parameter: f64 = segments[1]
408                    .parse()
409                    .map_err(TrackItemParseError::ElementParseError)?;
410                let time_curve: TimeCurve = segments[2]
411                    .parse()
412                    .map_err(TrackItemParseError::TimeCurveParseError)?;
413                (Some(parameter), Some(time_curve))
414            }
415            n => {
416                return Err(TrackItemParseError::InvalidNumSegments(n));
417            }
418        };
419
420        let script_name = items[items.len() - 2].to_string();
421        let (step, _) = TrackStep::parse_and_get(items[0])?;
422        let mut values = Vec::new();
423        for item in &items[0..items.len() - 2] {
424            let (item_step, value) = TrackStep::parse_and_get(item)?;
425            if item_step != step {
426                return Err(TrackItemParseError::InconsistentStep);
427            }
428            values.push(value);
429        }
430        Ok(AnimatedTrackItem {
431            step,
432            values,
433            flags,
434            script_name,
435            parameter,
436            time_curve,
437        })
438    }
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444    use rstest::rstest;
445
446    #[test]
447    fn test_time_curve_from_str() {
448        let tc: TimeCurve = "0.25,0.25,0.25,0.25".parse().unwrap();
449        assert_eq!(
450            tc.control_points,
451            vec![
452                TimeCurvePoint {
453                    position: 0.0,
454                    value: 0.0,
455                    right_handle: (0.25, 0.25),
456                },
457                TimeCurvePoint {
458                    position: 1.0,
459                    value: 1.0,
460                    right_handle: (0.25, 0.25),
461                },
462            ]
463        );
464        assert_eq!(tc.to_string(), "0.25,0.25,0.25,0.25");
465    }
466
467    #[test]
468    fn test_time_curve_from_str_multiple_points() {
469        let tc: TimeCurve = "0,0,0.25,0,0.5,0.5,0.25,0,1,1,0.25,0".parse().unwrap();
470
471        assert_eq!(
472            tc.control_points,
473            vec![
474                TimeCurvePoint {
475                    position: 0.0,
476                    value: 0.0,
477                    right_handle: (0.25, 0.0),
478                },
479                TimeCurvePoint {
480                    position: 0.5,
481                    value: 0.5,
482                    right_handle: (0.25, 0.0),
483                },
484                TimeCurvePoint {
485                    position: 1.0,
486                    value: 1.0,
487                    right_handle: (0.25, 0.0),
488                },
489            ]
490        );
491
492        assert_eq!(tc.to_string(), "0,0,0.25,0,0.5,0.5,0.25,0,1,1,0.25,0");
493    }
494
495    #[test]
496    fn test_track_item_parse() {
497        let item_str = "0.1,0.2,MyScript,3|1.5|0.25,0.25,0.25,0.25";
498        let animated_item: AnimatedTrackItem = item_str.parse().unwrap();
499        assert_eq!(
500            animated_item,
501            AnimatedTrackItem {
502                step: TrackStep(1),
503                values: vec![0.1, 0.2],
504                flags: TrackFlags {
505                    ease_in: true,
506                    ease_out: true,
507                    twopoints: false,
508                },
509                script_name: "MyScript".to_string(),
510                parameter: Some(1.5),
511                time_curve: Some(TimeCurve {
512                    control_points: vec![
513                        TimeCurvePoint {
514                            position: 0.0,
515                            value: 0.0,
516                            right_handle: (0.25, 0.25),
517                        },
518                        TimeCurvePoint {
519                            position: 1.0,
520                            value: 1.0,
521                            right_handle: (0.25, 0.25),
522                        },
523                    ],
524                }),
525            }
526        );
527        assert_eq!(animated_item.to_string(), item_str);
528    }
529    #[test]
530    fn test_track_item_parse_segments() {
531        let item_str = "0.1,0.2,MyScript,3|1.5|0.25,0.25,0.25,0.25";
532        let animated_item: AnimatedTrackItem = item_str.parse().unwrap();
533        assert_eq!(animated_item.parameter, Some(1.5));
534        assert!(animated_item.time_curve.is_some());
535
536        let item_str_no_curve = "0.1,0.2,MyScript,3|1.5";
537        let animated_item_no_curve: AnimatedTrackItem = item_str_no_curve.parse().unwrap();
538        assert_eq!(animated_item_no_curve.parameter, Some(1.5));
539        assert!(animated_item_no_curve.time_curve.is_none());
540
541        let item_str_no_param = "0.1,0.2,MyScript,3|0.25,0.25,0.25,0.25";
542        let animated_item_no_param: AnimatedTrackItem = item_str_no_param.parse().unwrap();
543        assert!(animated_item_no_param.parameter.is_none());
544        assert!(animated_item_no_param.time_curve.is_some());
545
546        let item_str_only_values = "0.1,0.2,MyScript,3";
547        let animated_item_only_values: AnimatedTrackItem = item_str_only_values.parse().unwrap();
548        assert!(animated_item_only_values.parameter.is_none());
549        assert!(animated_item_only_values.time_curve.is_none());
550    }
551
552    #[rstest]
553    #[case("1", TrackStep(0), 1.0)]
554    #[case("0.1", TrackStep(1), 0.1)]
555    #[case("0.01", TrackStep(2), 0.01)]
556    #[case("0.001", TrackStep(3), 0.001)]
557    #[case("-2.34", TrackStep(2), -2.34)]
558    fn test_track_step_parse_and_get(
559        #[case] input: &str,
560        #[case] expected_step: TrackStep,
561        #[case] expected_value: f64,
562    ) {
563        let (step, value) = TrackStep::parse_and_get(input).unwrap();
564        assert_eq!(step, expected_step);
565        assert_eq!(value, expected_value);
566    }
567
568    #[rstest]
569    #[case(TrackStep(0), 2.34, "2")]
570    #[case(TrackStep(1), 2.34, "2.3")]
571    #[case(TrackStep(2), 2.345, "2.35")]
572    #[case(TrackStep(3), 2.3456, "2.346")]
573    #[case(TrackStep(2), -2.345, "-2.35")]
574    #[case(TrackStep(0), -2.34, "-2")]
575    fn test_track_step_round_to_string(
576        #[case] step: TrackStep,
577        #[case] value: f64,
578        #[case] expected_str: &str,
579    ) {
580        let result_str = step.round_to_string(value);
581        assert_eq!(result_str, expected_str);
582    }
583
584    #[rstest]
585    #[case(1.0, TrackStep(0))]
586    #[case(0.1, TrackStep(1))]
587    #[case(0.01, TrackStep(2))]
588    #[case(0.001, TrackStep(3))]
589    #[case(0.0001, TrackStep(4))]
590    fn test_track_step_try_from(#[case] input: f64, #[case] expected_step: TrackStep) {
591        assert_eq!(TrackStep::try_from(input).unwrap(), expected_step);
592    }
593}