1 | //! Thick line join. |
2 | |
3 | use crate::{ |
4 | geometry::{Point, PointExt}, |
5 | primitives::{ |
6 | common::{LineSide, LinearEquation, StrokeOffset}, |
7 | line::intersection_params::{Intersection, IntersectionParams}, |
8 | Line, |
9 | }, |
10 | }; |
11 | |
12 | /// Join kind |
13 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
14 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
15 | pub enum JoinKind { |
16 | /// Mitered (sharp point) |
17 | Miter, |
18 | |
19 | /// Bevelled (flattened point) |
20 | Bevel { |
21 | /// Left side or right side? |
22 | outer_side: LineSide, |
23 | }, |
24 | |
25 | /// Degenerate (angle between lines is too small to properly render stroke). |
26 | /// |
27 | /// Degenerate corners are rendered with a bevel. |
28 | Degenerate { |
29 | /// Left side or right side? |
30 | outer_side: LineSide, |
31 | }, |
32 | |
33 | /// Lines are colinear. |
34 | /// |
35 | /// Start and end points for this join will be equal. |
36 | Colinear, |
37 | |
38 | /// The starting cap of a line. |
39 | Start, |
40 | |
41 | /// The ending cap of a line. |
42 | End, |
43 | } |
44 | |
45 | /// The left/right corners that make up the start or end edge of a thick line. |
46 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
47 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
48 | pub struct EdgeCorners { |
49 | /// Left side point. |
50 | pub left: Point, |
51 | |
52 | /// Right side point. |
53 | pub right: Point, |
54 | } |
55 | |
56 | /// A join between two lines. |
57 | #[derive (Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)] |
58 | #[cfg_attr (feature = "defmt" , derive(::defmt::Format))] |
59 | pub struct LineJoin { |
60 | /// Join kind. |
61 | pub kind: JoinKind, |
62 | |
63 | /// Corners comprising the ending edge of the line that ends at this join. |
64 | pub first_edge_end: EdgeCorners, |
65 | |
66 | /// Corners comprising the start edge of the line that begins at this join. |
67 | pub second_edge_start: EdgeCorners, |
68 | } |
69 | |
70 | impl LineJoin { |
71 | /// Create a starting join. |
72 | /// |
73 | /// `first_edge_end` and `second_edge_start` are set to the same points. |
74 | pub fn start(start: Point, mid: Point, width: u32, stroke_offset: StrokeOffset) -> Self { |
75 | let line = Line::new(start, mid); |
76 | |
77 | let (l, r) = line.extents(width, stroke_offset); |
78 | |
79 | let points = EdgeCorners { |
80 | left: l.start, |
81 | right: r.start, |
82 | }; |
83 | |
84 | Self { |
85 | kind: JoinKind::Start, |
86 | first_edge_end: points, |
87 | second_edge_start: points, |
88 | } |
89 | } |
90 | |
91 | /// Create an ending join. |
92 | /// |
93 | /// `first_edge_end` and `second_edge_start` are set to the same points. |
94 | pub fn end(mid: Point, end: Point, width: u32, stroke_offset: StrokeOffset) -> Self { |
95 | let line = Line::new(mid, end); |
96 | |
97 | let (l, r) = line.extents(width, stroke_offset); |
98 | |
99 | let points = EdgeCorners { |
100 | left: l.end, |
101 | right: r.end, |
102 | }; |
103 | |
104 | Self { |
105 | kind: JoinKind::End, |
106 | first_edge_end: points, |
107 | second_edge_start: points, |
108 | } |
109 | } |
110 | |
111 | /// Empty join |
112 | pub const fn empty() -> Self { |
113 | Self { |
114 | kind: JoinKind::End, |
115 | first_edge_end: EdgeCorners { |
116 | left: Point::zero(), |
117 | right: Point::zero(), |
118 | }, |
119 | second_edge_start: EdgeCorners { |
120 | left: Point::zero(), |
121 | right: Point::zero(), |
122 | }, |
123 | } |
124 | } |
125 | |
126 | /// Compute a join. |
127 | pub fn from_points( |
128 | start: Point, |
129 | mid: Point, |
130 | end: Point, |
131 | width: u32, |
132 | stroke_offset: StrokeOffset, |
133 | ) -> Self { |
134 | let first_line = Line::new(start, mid); |
135 | let second_line = Line::new(mid, end); |
136 | |
137 | // Left and right edges of thick first segment |
138 | let (first_edge_left, first_edge_right) = first_line.extents(width, stroke_offset); |
139 | // Left and right edges of thick second segment |
140 | let (second_edge_left, second_edge_right) = second_line.extents(width, stroke_offset); |
141 | |
142 | if let Some((l_intersection, outer_side, r_intersection)) = intersections( |
143 | &first_edge_left, |
144 | &first_edge_right, |
145 | &second_edge_left, |
146 | &second_edge_right, |
147 | ) { |
148 | // Check if the inside end point of the second line lies inside the first segment. |
149 | let self_intersection = match outer_side { |
150 | LineSide::Right => LinearEquation::from_line(&first_edge_left) |
151 | .check_side(second_edge_left.end, LineSide::Right), |
152 | LineSide::Left => LinearEquation::from_line(&first_edge_right) |
153 | .check_side(second_edge_right.end, LineSide::Left), |
154 | }; |
155 | |
156 | // Normal line: non-overlapping line end caps |
157 | if !self_intersection { |
158 | // Distance from midpoint to miter outside end point. |
159 | let miter_length_squared = Line::new( |
160 | mid, |
161 | match outer_side { |
162 | LineSide::Left => l_intersection, |
163 | LineSide::Right => r_intersection, |
164 | }, |
165 | ) |
166 | .delta() |
167 | .length_squared() as u32; |
168 | |
169 | // Miter length limit is double the line width (but squared to avoid sqrt() costs) |
170 | let miter_limit = (width * 2).pow(2); |
171 | |
172 | // Intersection is within limit at which it will be chopped off into a bevel, so |
173 | // return a miter. |
174 | if miter_length_squared <= miter_limit { |
175 | let corners = EdgeCorners { |
176 | left: l_intersection, |
177 | right: r_intersection, |
178 | }; |
179 | |
180 | Self { |
181 | kind: JoinKind::Miter, |
182 | first_edge_end: corners, |
183 | second_edge_start: corners, |
184 | } |
185 | } |
186 | // Miter is too long, chop it into bevel-style corner |
187 | else { |
188 | match outer_side { |
189 | LineSide::Right => Self { |
190 | kind: JoinKind::Bevel { outer_side }, |
191 | first_edge_end: EdgeCorners { |
192 | left: l_intersection, |
193 | right: first_edge_right.end, |
194 | }, |
195 | second_edge_start: EdgeCorners { |
196 | left: l_intersection, |
197 | right: second_edge_right.start, |
198 | }, |
199 | }, |
200 | LineSide::Left => Self { |
201 | kind: JoinKind::Bevel { outer_side }, |
202 | first_edge_end: EdgeCorners { |
203 | left: first_edge_left.end, |
204 | right: r_intersection, |
205 | }, |
206 | second_edge_start: EdgeCorners { |
207 | left: second_edge_left.start, |
208 | right: r_intersection, |
209 | }, |
210 | }, |
211 | } |
212 | } |
213 | } |
214 | // Line segments overlap (degenerate) |
215 | else { |
216 | Self { |
217 | kind: JoinKind::Degenerate { outer_side }, |
218 | first_edge_end: EdgeCorners { |
219 | left: first_edge_left.end, |
220 | right: first_edge_right.end, |
221 | }, |
222 | second_edge_start: EdgeCorners { |
223 | left: second_edge_left.start, |
224 | right: second_edge_right.start, |
225 | }, |
226 | } |
227 | } |
228 | } |
229 | // Lines are colinear |
230 | else { |
231 | Self { |
232 | kind: JoinKind::Colinear, |
233 | first_edge_end: EdgeCorners { |
234 | left: first_edge_left.end, |
235 | right: first_edge_right.end, |
236 | }, |
237 | second_edge_start: EdgeCorners { |
238 | left: second_edge_left.start, |
239 | right: second_edge_right.start, |
240 | }, |
241 | } |
242 | } |
243 | } |
244 | |
245 | /// The filler line (if any) for bevel and degenerate joints. |
246 | const fn filler_line(&self) -> Option<Line> { |
247 | match self.kind { |
248 | JoinKind::Bevel { outer_side, .. } | JoinKind::Degenerate { outer_side, .. } => { |
249 | let line = match outer_side { |
250 | LineSide::Left => { |
251 | Line::new(self.first_edge_end.left, self.second_edge_start.left) |
252 | } |
253 | LineSide::Right => { |
254 | Line::new(self.first_edge_end.right, self.second_edge_start.right) |
255 | } |
256 | }; |
257 | |
258 | Some(line) |
259 | } |
260 | _ => None, |
261 | } |
262 | } |
263 | |
264 | fn cap(&self, cap: &EdgeCorners) -> (Line, Option<Line>) { |
265 | if let Some(filler) = self.filler_line() { |
266 | let midpoint = filler.midpoint(); |
267 | |
268 | let l1 = Line::new(cap.left, midpoint); |
269 | let l2 = Line::new(midpoint, cap.right); |
270 | |
271 | (l1, l2.into()) |
272 | } else { |
273 | (Line::new(cap.left, cap.right), None) |
274 | } |
275 | } |
276 | |
277 | /// Start node bevel line(s). |
278 | /// |
279 | /// If the join is a bevel join, this will return two lines, otherwise one. |
280 | pub fn start_cap_lines(&self) -> (Line, Option<Line>) { |
281 | self.cap(&self.second_edge_start) |
282 | } |
283 | |
284 | /// End node bevel line(s). |
285 | /// |
286 | /// If the join is a bevel join, this will return two lines, otherwise one. |
287 | pub fn end_cap_lines(&self) -> (Line, Option<Line>) { |
288 | self.cap(&self.first_edge_end) |
289 | } |
290 | |
291 | /// Whether the join is degenerate (segments self-intersect) or not. |
292 | pub const fn is_degenerate(&self) -> bool { |
293 | matches!(self.kind, JoinKind::Degenerate { .. }) |
294 | } |
295 | } |
296 | |
297 | fn intersections( |
298 | first_edge_left: &Line, |
299 | first_edge_right: &Line, |
300 | second_edge_left: &Line, |
301 | second_edge_right: &Line, |
302 | ) -> Option<(Point, LineSide, Point)> { |
303 | let params = IntersectionParams::from_lines(second_edge_left, first_edge_left); |
304 | |
305 | let (l_intersection, outer_side) = if let Intersection::Point { |
306 | point, outer_side, .. |
307 | } = params.intersection() |
308 | { |
309 | if !params.nearly_colinear_has_error() { |
310 | (point, outer_side) |
311 | } else { |
312 | (first_edge_left.end, outer_side) |
313 | } |
314 | } else { |
315 | return None; |
316 | }; |
317 | |
318 | let params = IntersectionParams::from_lines(second_edge_right, first_edge_right); |
319 | |
320 | let r_intersection = if let Intersection::Point { point, .. } = params.intersection() { |
321 | if !params.nearly_colinear_has_error() { |
322 | point |
323 | } else { |
324 | first_edge_right.end |
325 | } |
326 | } else { |
327 | return None; |
328 | }; |
329 | |
330 | Some((l_intersection, outer_side, r_intersection)) |
331 | } |
332 | |