1//! Thick line join.
2
3use 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))]
15pub 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))]
48pub 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))]
59pub 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
70impl 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
297fn 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