1use sequoia_core::{Point, Rect};
2
3use crate::Span;
4use crate::flatten::Contour;
5use crate::raster::{self, RasterOptions};
6
7#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
8#[non_exhaustive]
9pub enum LineCap {
10 #[default]
11 Butt,
12
13 Round,
14
15 Square,
16}
17
18#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19#[non_exhaustive]
20pub enum LineJoin {
21 #[default]
22 Miter,
23
24 Round,
25
26 Bevel,
27}
28
29#[derive(Clone, Debug, Default, PartialEq)]
30pub struct Dash {
31 pub array: Vec<f32>,
32
33 pub offset: f32,
34}
35
36impl Dash {
37 pub fn new(array: impl Into<Vec<f32>>) -> Self {
38 Self {
39 array: array.into(),
40 offset: 0.0,
41 }
42 }
43
44 pub fn offset(mut self, offset: f32) -> Self {
45 self.offset = offset;
46 self
47 }
48
49 fn normalized(&self) -> Option<Vec<f32>> {
50 let mut array: Vec<f32> = self
51 .array
52 .iter()
53 .copied()
54 .filter(|v| v.is_finite() && *v > 1e-4)
55 .collect();
56 if array.is_empty() {
57 return None;
58 }
59 if array.len() % 2 == 1 {
60 let copy = array.clone();
61 array.extend(copy);
62 }
63 Some(array)
64 }
65}
66
67#[derive(Clone, Debug, PartialEq)]
68pub struct StrokeStyle {
69 pub width: f32,
70
71 pub cap: LineCap,
72
73 pub join: LineJoin,
74
75 pub miter_limit: f32,
76
77 pub dash: Option<Dash>,
78}
79
80impl Default for StrokeStyle {
81 fn default() -> Self {
82 Self {
83 width: 1.0,
84 cap: LineCap::Butt,
85 join: LineJoin::Miter,
86 miter_limit: 4.0,
87 dash: None,
88 }
89 }
90}
91
92impl StrokeStyle {
93 pub fn new(width: f32) -> Self {
94 Self {
95 width,
96 ..Self::default()
97 }
98 }
99
100 pub fn width(mut self, width: f32) -> Self {
101 self.width = width;
102 self
103 }
104
105 pub fn cap(mut self, cap: LineCap) -> Self {
106 self.cap = cap;
107 self
108 }
109
110 pub fn join(mut self, join: LineJoin) -> Self {
111 self.join = join;
112 self
113 }
114
115 pub fn miter_limit(mut self, limit: f32) -> Self {
116 self.miter_limit = limit;
117 self
118 }
119
120 pub fn dash(mut self, dash: impl Into<Dash>) -> Self {
121 self.dash = Some(dash.into());
122 self
123 }
124
125 pub fn round(width: f32) -> Self {
126 Self::new(width).cap(LineCap::Round).join(LineJoin::Round)
127 }
128
129 pub(crate) fn fingerprint(&self) -> u64 {
130 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
131 let mut eat = |v: u64| {
132 hash ^= v;
133 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
134 };
135 eat(u64::from(self.width.to_bits()));
136 eat(match self.cap {
137 LineCap::Butt => 0,
138 LineCap::Round => 1,
139 LineCap::Square => 2,
140 });
141 eat(match self.join {
142 LineJoin::Miter => 0,
143 LineJoin::Round => 1,
144 LineJoin::Bevel => 2,
145 });
146 eat(u64::from(self.miter_limit.to_bits()));
147 if let Some(dash) = &self.dash {
148 eat(1);
149 eat(u64::from(dash.offset.to_bits()));
150 for v in &dash.array {
151 eat(u64::from(v.to_bits()));
152 }
153 } else {
154 eat(0);
155 }
156 hash
157 }
158}
159
160impl From<f32> for Dash {
161 fn from(array: f32) -> Self {
162 Self::new(vec![array])
163 }
164}
165
166impl From<Vec<f32>> for Dash {
167 fn from(array: Vec<f32>) -> Self {
168 Self::new(array)
169 }
170}
171
172impl From<[f32; 2]> for Dash {
173 fn from(array: [f32; 2]) -> Self {
174 Self::new(array.to_vec())
175 }
176}
177
178impl From<[f32; 4]> for Dash {
179 fn from(array: [f32; 4]) -> Self {
180 Self::new(array.to_vec())
181 }
182}
183
184pub fn stroke(
185 contours: &[Contour],
186 style: &StrokeStyle,
187 opts: RasterOptions,
188 out: &mut Vec<Span>,
189) -> Rect {
190 let width = style.width;
191 if !width.is_finite() || width <= 0.0 {
192 return Rect::ZERO;
193 }
194 let outline = outline_contours(contours, style, opts.tolerance);
195 if outline.is_empty() {
196 return Rect::ZERO;
197 }
198
199 raster::fill_contours(&outline, crate::FillRule::NonZero, opts, out)
200}
201
202pub fn stroke_path(
203 path: &crate::Path,
204 style: &StrokeStyle,
205 opts: RasterOptions,
206 out: &mut Vec<Span>,
207) -> Rect {
208 let contours = path.contours(opts.tolerance);
209 stroke(&contours, style, opts, out)
210}
211
212fn outline_contours(contours: &[Contour], style: &StrokeStyle, tolerance: f32) -> Vec<Contour> {
213 let half = style.width * 0.5;
214 let dash = style
215 .dash
216 .as_ref()
217 .and_then(|d| d.normalized().map(|pattern| (pattern, d)));
218 let mut out: Vec<Contour> = Vec::new();
219 for contour in contours {
220 let mut points: Vec<Point> = contour.points.clone();
221
222 if points.len() >= 2 && points[0] == points[points.len() - 1] {
223 points.pop();
224 }
225 if points.len() < 2 {
226 continue;
227 }
228 let closed = contour.closed;
229 let pieces: Vec<Contour> = match &dash {
230 Some((pattern, d)) => dash_contour(&points, closed, pattern, d),
231 None => vec![Contour { points, closed }],
232 };
233 for piece in &pieces {
234 stroke_polyline(piece, half, style, tolerance, &mut out);
235 }
236 }
237 out
238}
239
240fn stroke_polyline(
241 piece: &Contour,
242 half: f32,
243 style: &StrokeStyle,
244 tolerance: f32,
245 out: &mut Vec<Contour>,
246) {
247 let pts = &piece.points;
248 if pts.len() < 2 {
249 return;
250 }
251 let mut segs: Vec<(Point, Point)> = Vec::with_capacity(pts.len());
252 for w in pts.windows(2) {
253 segs.push((w[0], w[1]));
254 }
255 if piece.closed {
256 segs.push((pts[pts.len() - 1], pts[0]));
257 }
258
259 for &(a, b) in &segs {
260 push_segment_quad(a, b, half, out);
261 }
262
263 let joint_count = if piece.closed {
264 segs.len()
265 } else {
266 segs.len().saturating_sub(1)
267 };
268 for i in 0..joint_count {
269 let (_, v) = segs[i];
270 let d0 = direction(segs[i].0, v);
271 let d1 = direction(v, segs[(i + 1) % segs.len()].1);
272 if let (Some(d0), Some(d1)) = (d0, d1) {
273 push_join(v, d0, d1, half, style, tolerance, out);
274 }
275 }
276
277 if !piece.closed
278 && style.cap != LineCap::Butt
279 && let (Some(first), Some(last)) = (segs.first(), segs.last())
280 {
281 if let Some(d) = direction(first.0, first.1) {
282 push_cap(
283 first.0,
284 Point::new(-d.x, -d.y),
285 half,
286 style.cap,
287 tolerance,
288 out,
289 );
290 }
291 if let Some(d) = direction(last.0, last.1) {
292 push_cap(last.1, d, half, style.cap, tolerance, out);
293 }
294 }
295}
296
297fn push_segment_quad(a: Point, b: Point, half: f32, out: &mut Vec<Contour>) {
298 let Some(d) = direction(a, b) else {
299 return;
300 };
301 let n = normal(d);
302 let quad = vec![
303 Point::new(a.x + n.x * half, a.y + n.y * half),
304 Point::new(b.x + n.x * half, b.y + n.y * half),
305 Point::new(b.x - n.x * half, b.y - n.y * half),
306 Point::new(a.x - n.x * half, a.y - n.y * half),
307 ];
308 push_contour(quad, out);
309}
310
311fn push_join(
312 v: Point,
313 d0: Point,
314 d1: Point,
315 half: f32,
316 style: &StrokeStyle,
317 tolerance: f32,
318 out: &mut Vec<Contour>,
319) {
320 let cross = d0.x * d1.y - d0.y * d1.x;
321 if cross.abs() < 1e-6 && d0.x * d1.x + d0.y * d1.y > 0.0 {
322 return;
323 }
324
325 let side = if cross >= 0.0 { 1.0 } else { -1.0 };
326 let n0 = scale(normal(d0), side);
327 let n1 = scale(normal(d1), side);
328 let m0 = Point::new(v.x + n0.x * half, v.y + n0.y * half);
329 let m1 = Point::new(v.x + n1.x * half, v.y + n1.y * half);
330
331 match style.join {
332 LineJoin::Round => {
333 let a0 = n0.y.atan2(n0.x);
334 let a1 = n1.y.atan2(n1.x);
335 let delta = wrap_pi(a1 - a0);
336 let steps = arc_steps_for(half, delta.abs(), tolerance);
337 let mut poly = Vec::with_capacity(steps + 2);
338 poly.push(v);
339 for i in 0..=steps {
340 let a = a0 + delta * (i as f32 / steps as f32);
341 poly.push(Point::new(v.x + a.cos() * half, v.y + a.sin() * half));
342 }
343 push_contour(poly, out);
344 }
345 LineJoin::Bevel => {
346 push_contour(vec![v, m0, m1], out);
347 }
348 LineJoin::Miter => match line_intersection(m0, d0, m1, d1) {
349 Some(tip) => {
350 let limit = style.miter_limit.max(1.0) * half;
351 let dist = ((tip.x - v.x).powi(2) + (tip.y - v.y).powi(2)).sqrt();
352 if dist <= limit {
353 push_contour(vec![v, m0, tip, m1], out);
354 } else {
355 push_contour(vec![v, m0, m1], out);
356 }
357 }
358 None => push_contour(vec![v, m0, m1], out),
359 },
360 }
361}
362
363fn push_cap(
364 end: Point,
365 dir: Point,
366 half: f32,
367 cap: LineCap,
368 tolerance: f32,
369 out: &mut Vec<Contour>,
370) {
371 let len = (dir.x * dir.x + dir.y * dir.y).sqrt();
372 if len <= f32::MIN_POSITIVE {
373 return;
374 }
375 let d = Point::new(dir.x / len, dir.y / len);
376 let n = normal(d);
377 match cap {
378 LineCap::Butt => {}
379 LineCap::Square => {
380 let e = Point::new(end.x + d.x * half, end.y + d.y * half);
381 push_contour(
382 vec![
383 Point::new(end.x + n.x * half, end.y + n.y * half),
384 Point::new(e.x + n.x * half, e.y + n.y * half),
385 Point::new(e.x - n.x * half, e.y - n.y * half),
386 Point::new(end.x - n.x * half, end.y - n.y * half),
387 ],
388 out,
389 );
390 }
391 LineCap::Round => {
392 let a0 = n.y.atan2(n.x);
393 let steps = arc_steps_for(half, std::f32::consts::PI, tolerance).max(2);
394 let mut poly = Vec::with_capacity(steps + 1);
395 for i in 0..=steps {
396 let a = a0 + std::f32::consts::PI * (i as f32 / steps as f32);
397 poly.push(Point::new(end.x + a.cos() * half, end.y + a.sin() * half));
398 }
399 push_contour(poly, out);
400 }
401 }
402}
403
404fn dash_contour(points: &[Point], closed: bool, pattern: &[f32], dash: &Dash) -> Vec<Contour> {
405 let total: f32 = pattern.iter().sum();
406 if total <= 1e-4 {
407 return vec![Contour {
408 points: points.to_vec(),
409 closed,
410 }];
411 }
412
413 let mut idx = 0usize;
414 let mut remaining;
415 let mut on;
416 let mut pos = dash.offset.rem_euclid(total);
417 loop {
418 let len = pattern[idx];
419 if pos < len {
420 remaining = len - pos;
421 on = idx.is_multiple_of(2);
422 break;
423 }
424 pos -= len;
425 idx = (idx + 1) % pattern.len();
426 }
427
428 let mut segs: Vec<(Point, Point)> = points.windows(2).map(|w| (w[0], w[1])).collect();
429 if closed {
430 segs.push((points[points.len() - 1], points[0]));
431 }
432
433 let mut out = Vec::new();
434 let mut current: Vec<Point> = Vec::new();
435 for (a, b) in segs {
436 if direction(a, b).is_none() {
437 continue;
438 };
439 let seg_len = ((b.x - a.x).powi(2) + (b.y - a.y).powi(2)).sqrt();
440 let mut travelled = 0.0;
441 while travelled < seg_len - 1e-4 {
442 let step = remaining.min(seg_len - travelled);
443 travelled += step;
444 remaining -= step;
445 let t = travelled / seg_len;
446 let p = Point::new(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t);
447 if on {
448 if current.is_empty() {
449 current.push(a);
450 }
451 current.push(p);
452 }
453 if remaining <= 1e-4 {
454 if on {
455 if current.len() >= 2 {
456 out.push(Contour {
457 points: std::mem::take(&mut current),
458 closed: false,
459 });
460 }
461 current.clear();
462 } else {
463 current = vec![p];
464 }
465 on = !on;
466 idx = (idx + 1) % pattern.len();
467 remaining = pattern[idx];
468 }
469 }
470 }
471 if on && current.len() >= 2 {
472 out.push(Contour {
473 points: current,
474 closed: false,
475 });
476 }
477 out
478}
479
480fn push_contour(points: Vec<Point>, out: &mut Vec<Contour>) {
481 if points.len() < 3 {
482 return;
483 }
484 let area = signed_area(&points);
485 if area.abs() <= 1e-9 {
486 return;
487 }
488 let points = if area < 0.0 {
489 let mut rev = points;
490 rev.reverse();
491 rev
492 } else {
493 points
494 };
495 out.push(Contour {
496 points,
497 closed: false,
498 });
499}
500
501fn signed_area(points: &[Point]) -> f32 {
502 let mut sum = 0.0;
503 for i in 0..points.len() {
504 let a = points[i];
505 let b = points[(i + 1) % points.len()];
506 sum += a.x * b.y - b.x * a.y;
507 }
508 sum * 0.5
509}
510
511fn direction(a: Point, b: Point) -> Option<Point> {
512 let dx = b.x - a.x;
513 let dy = b.y - a.y;
514 let len = (dx * dx + dy * dy).sqrt();
515 if !len.is_finite() || len <= 1e-6 {
516 return None;
517 }
518 Some(Point::new(dx / len, dy / len))
519}
520
521fn normal(d: Point) -> Point {
522 Point::new(d.y, -d.x)
523}
524
525fn scale(p: Point, s: f32) -> Point {
526 Point::new(p.x * s, p.y * s)
527}
528
529fn wrap_pi(angle: f32) -> f32 {
530 let two_pi = std::f32::consts::TAU;
531 let mut a = angle % two_pi;
532 if a > std::f32::consts::PI {
533 a -= two_pi;
534 } else if a <= -std::f32::consts::PI {
535 a += two_pi;
536 }
537 a
538}
539
540fn arc_steps_for(radius: f32, span: f32, tolerance: f32) -> usize {
541 crate::flatten::arc_steps(radius, span, tolerance).max(1)
542}
543
544fn line_intersection(p: Point, d: Point, q: Point, e: Point) -> Option<Point> {
545 let denom = d.x * e.y - d.y * e.x;
546 if denom.abs() < 1e-6 {
547 return None;
548 }
549 let t = ((q.x - p.x) * e.y - (q.y - p.y) * e.x) / denom;
550 Some(Point::new(p.x + d.x * t, p.y + d.y * t))
551}