Skip to main content

renamite_behavior_common/
path.rs

1//! Helpers for editing animated paths at the correct frame.
2
3use renamite_animation::Frame;
4use renamite_history::EditorCommand;
5use renamite_model::{Document, NodeId, PropPath};
6
7/// Determine whether path edits should target the static base path or a
8/// keyframe at `playhead`, and whether a seed keyframe must be created first.
9///
10/// Returns `(edit_frame, seed_command)`:
11/// - `edit_frame = None`  -> edit the static base path
12/// - `edit_frame = Some(f)` -> edit the keyframe at `f`
13/// - `seed_command` is an `AddKeyframe` that must be applied before edits
14pub fn path_edit_target(
15    doc: &Document,
16    id: NodeId,
17    playhead: Frame,
18    record: bool,
19) -> Option<(Option<Frame>, Option<EditorCommand>)> {
20    let prop = PropPath::new("shape.path");
21    let animated = doc.property_is_animated(id, &prop);
22
23    if !record && !animated {
24        return Some((None, None));
25    }
26
27    if doc.keyframe_data(id, &prop, playhead).is_some() {
28        return Some((Some(playhead), None));
29    }
30
31    let value = doc.value_at(id, &prop, playhead.0 as f64).ok()?;
32    Some((
33        Some(playhead),
34        Some(EditorCommand::AddKeyframe {
35            id,
36            prop,
37            frame: playhead,
38            value,
39        }),
40    ))
41}
42
43#[cfg(test)]
44mod tests {
45    use super::*;
46    use renamite_animation::Animated;
47    use renamite_geometry::VectorPath;
48    use renamite_model::{Node, NodeKind, Parent, ShapeKind, Value};
49
50    fn doc_with_path() -> (Document, NodeId) {
51        let mut doc = Document::empty();
52        let id = doc.create_node(Node::new(
53            "path",
54            NodeKind::Shape(ShapeKind::Path(Animated::new(VectorPath::default()))),
55        ));
56        doc.attach(id, Parent::Comp(doc.main), 0).unwrap();
57        (doc, id)
58    }
59
60    #[test]
61    fn static_no_record_targets_base() {
62        let (doc, id) = doc_with_path();
63        let (frame, seed) = path_edit_target(&doc, id, Frame(10), false).unwrap();
64        assert_eq!(frame, None);
65        assert!(seed.is_none());
66    }
67
68    #[test]
69    fn record_seeds_playhead_key() {
70        let (doc, id) = doc_with_path();
71        let (frame, seed) = path_edit_target(&doc, id, Frame(10), true).unwrap();
72        assert_eq!(frame, Some(Frame(10)));
73        assert!(matches!(
74            seed,
75            Some(EditorCommand::AddKeyframe {
76                frame: Frame(10),
77                ..
78            })
79        ));
80    }
81
82    #[test]
83    fn animated_without_key_at_playhead_seeds() {
84        let (mut doc, id) = doc_with_path();
85        let prop = PropPath::new("shape.path");
86        doc.add_keyframe(id, &prop, Frame(0), &Value::Path(VectorPath::default()))
87            .unwrap();
88
89        let (frame, seed) = path_edit_target(&doc, id, Frame(5), false).unwrap();
90        assert_eq!(frame, Some(Frame(5)));
91        assert!(seed.is_some());
92    }
93
94    #[test]
95    fn animated_with_key_at_playhead_needs_no_seed() {
96        let (mut doc, id) = doc_with_path();
97        let prop = PropPath::new("shape.path");
98        doc.add_keyframe(id, &prop, Frame(10), &Value::Path(VectorPath::default()))
99            .unwrap();
100
101        let (frame, seed) = path_edit_target(&doc, id, Frame(10), false).unwrap();
102        assert_eq!(frame, Some(Frame(10)));
103        assert!(seed.is_none());
104    }
105}