Skip to main content

renamite_behavior_common/
stroke.rs

1//! Pure command builders for stroke dash structure.
2
3use renamite_animation::Animated;
4use renamite_history::{EditorCommand, NodeTree};
5use renamite_model::{
6    AnimatedDash, Document, Node, NodeId, NodeKind, Parent, StrokeCap, StrokeJoin, StyleKind,
7    StylePaint,
8};
9
10/// Prefer a stroke *after* the shape in sibling order, until the next
11/// geometry-bearing sibling (same boundary as fill).
12pub fn stroke_style_for_shape(doc: &Document, shape_id: NodeId) -> Option<NodeId> {
13    let (parent, shape_index) = doc.locate(shape_id)?;
14    let siblings: Vec<NodeId> = match parent {
15        Parent::Comp(c) => doc.compositions.get(c)?.children.clone(),
16        Parent::Node(n) => doc.nodes.get(n)?.children.clone(),
17    };
18    for &id in siblings.iter().skip(shape_index + 1) {
19        if is_stroke(doc, id) {
20            return Some(id);
21        }
22        if is_style_stack_boundary(doc, id) {
23            break;
24        }
25    }
26    None
27}
28
29fn is_style_stack_boundary(doc: &Document, id: NodeId) -> bool {
30    matches!(
31        doc.nodes.get(id).map(|n| &n.kind),
32        Some(
33            NodeKind::Shape(_)
34                | NodeKind::Text(_)
35                | NodeKind::Image(_)
36                | NodeKind::Group
37                | NodeKind::Layer(_)
38                | NodeKind::Precomp { .. }
39                | NodeKind::Mask(_)
40        )
41    )
42}
43
44fn is_stroke(doc: &Document, id: NodeId) -> bool {
45    matches!(
46        doc.nodes.get(id).map(|n| &n.kind),
47        Some(NodeKind::Style(StyleKind::Stroke { .. }))
48    )
49}
50
51pub fn stroke_dash(doc: &Document, id: NodeId) -> Option<&AnimatedDash> {
52    match &doc.nodes.get(id)?.kind {
53        NodeKind::Style(StyleKind::Stroke {
54            dash: Some(dash), ..
55        }) => Some(dash),
56
57        _ => None,
58    }
59}
60
61pub fn cmd_enable_stroke_dash(doc: &Document, id: NodeId) -> Option<EditorCommand> {
62    let NodeKind::Style(StyleKind::Stroke { dash, .. }) = &doc.nodes.get(id)?.kind else {
63        return None;
64    };
65
66    if dash.is_some() {
67        return None;
68    }
69
70    Some(EditorCommand::SetStrokeDash {
71        id,
72        dash: Some(AnimatedDash {
73            dashes: vec![Animated::new(12.0), Animated::new(8.0)],
74            offset: Animated::new(0.0),
75        }),
76    })
77}
78
79pub fn cmd_disable_stroke_dash(doc: &Document, id: NodeId) -> Option<EditorCommand> {
80    stroke_dash(doc, id)?;
81
82    Some(EditorCommand::SetStrokeDash { id, dash: None })
83}
84
85pub fn cmd_add_stroke_dash_pair(doc: &Document, id: NodeId) -> Option<EditorCommand> {
86    let mut dash = stroke_dash(doc, id)?.clone();
87
88    dash.dashes.push(Animated::new(8.0));
89    dash.dashes.push(Animated::new(4.0));
90
91    Some(EditorCommand::SetStrokeDash {
92        id,
93        dash: Some(dash),
94    })
95}
96
97pub fn cmd_remove_stroke_dash_pair(doc: &Document, id: NodeId) -> Option<EditorCommand> {
98    let mut dash = stroke_dash(doc, id)?.clone();
99
100    if dash.dashes.len() <= 2 {
101        return None;
102    }
103
104    let new_len = dash.dashes.len().saturating_sub(2).max(2);
105    dash.dashes.truncate(new_len);
106
107    Some(EditorCommand::SetStrokeDash {
108        id,
109        dash: Some(dash),
110    })
111}
112
113pub fn cmd_add_stroke_after(
114    doc: &Document,
115    shape_id: NodeId,
116    paint: StylePaint,
117    width: f64,
118) -> Option<EditorCommand> {
119    if stroke_style_for_shape(doc, shape_id).is_some() {
120        return None;
121    }
122    let (parent, shape_index) = doc.locate(shape_id)?;
123    let mut index = shape_index + 1;
124    if let Some(fill) = super::fill::fill_style_for_shape(doc, shape_id)
125        && let Some((f_parent, f_idx)) = doc.locate(fill)
126        && f_parent == parent
127        && f_idx >= shape_index
128        && f_idx + 1 > index
129    {
130        index = f_idx + 1;
131    }
132    Some(EditorCommand::InsertNode {
133        parent,
134        index,
135        tree: NodeTree::leaf(Node::new(
136            "Stroke",
137            NodeKind::Style(StyleKind::Stroke {
138                paint,
139                width: Animated::new(width.max(0.0)),
140                cap: StrokeCap::Round,
141                join: StrokeJoin::Round,
142                miter_limit: Animated::new(4.0),
143                dash: None,
144            }),
145        )),
146    })
147}
148
149pub fn cmd_remove_stroke_for_shape(doc: &Document, shape_id: NodeId) -> Option<EditorCommand> {
150    let stroke = stroke_style_for_shape(doc, shape_id)?;
151    let (parent, shape_index) = doc.locate(shape_id)?;
152    let (s_parent, s_idx) = doc.locate(stroke)?;
153    if s_parent != parent || s_idx <= shape_index {
154        return None;
155    }
156    Some(EditorCommand::RemoveNode { id: stroke })
157}