Skip to main content

sequoia_draw/
stroke.rs

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}