renamite_behavior_common/
stroke.rs1use 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
10pub 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}