1use crate::geometry::Point;
19use crate::path::Contour;
20use std::f32::consts::PI;
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
24pub enum LineCap {
25 #[default]
27 Butt,
28 Round,
30 Square,
32}
33
34#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
36pub enum LineJoin {
37 #[default]
39 Miter,
40 Round,
42 Bevel,
44}
45
46#[derive(Clone, Debug)]
57pub struct Stroke {
58 pub width: f32,
59 pub line_cap: LineCap,
60 pub line_join: LineJoin,
61 pub miter_limit: f32,
62 pub dash: Vec<f32>,
64 pub dash_offset: f32,
66}
67
68impl Default for Stroke {
69 fn default() -> Self {
70 Stroke {
71 width: 1.0,
72 line_cap: LineCap::Butt,
73 line_join: LineJoin::Miter,
74 miter_limit: 4.0,
75 dash: Vec::new(),
76 dash_offset: 0.0,
77 }
78 }
79}
80
81impl Stroke {
82 pub fn new(width: f32) -> Self {
84 Stroke { width, ..Stroke::default() }
85 }
86
87 #[inline]
89 pub fn is_hairline(&self) -> bool {
90 self.width <= 0.0
91 }
92}
93
94pub(crate) fn build_stroke(contours: &[Contour], stroke: &Stroke, tolerance: f32) -> Vec<Vec<Point>> {
101 let r = (stroke.width * 0.5).max(0.0);
102 let mut polys: Vec<Vec<Point>> = Vec::new();
103 if r <= 0.0 {
104 return polys;
105 }
106
107 if !stroke.dash.is_empty() {
109 let dashed = apply_dash(contours, &stroke.dash, stroke.dash_offset);
110 for c in &dashed {
111 stroke_contour(c, r, stroke, tolerance, &mut polys);
112 }
113 return polys;
114 }
115
116 for c in contours {
117 stroke_contour(c, r, stroke, tolerance, &mut polys);
118 }
119 polys
120}
121
122fn apply_dash(contours: &[Contour], intervals: &[f32], offset: f32) -> Vec<Contour> {
129 let mut pattern: Vec<f32> = intervals.iter().map(|&v| v.max(0.0)).collect();
131 if pattern.len() % 2 == 1 {
132 let dup = pattern.clone();
133 pattern.extend(dup);
134 }
135 let total: f32 = pattern.iter().sum();
136 if total <= 0.0 {
137 return contours.to_vec();
138 }
139
140 let mut out: Vec<Contour> = Vec::new();
141 for contour in contours {
142 let mut pts: Vec<Point> = contour.points.clone();
144 if contour.closed && pts.len() >= 2 {
145 pts.push(pts[0]);
146 }
147 if pts.len() < 2 {
148 continue;
149 }
150
151 let mut phase = offset.rem_euclid(total);
153 let mut idx = 0usize;
154 while phase >= pattern[idx] {
155 phase -= pattern[idx];
156 idx = (idx + 1) % pattern.len();
157 }
158 let mut remaining = pattern[idx] - phase;
159 let mut on = idx.is_multiple_of(2);
160
161 let mut current: Vec<Point> = Vec::new();
162 if on {
163 current.push(pts[0]);
164 }
165
166 for w in pts.windows(2) {
167 let (mut a, b) = (w[0], w[1]);
168 let mut seg_len = a.distance(b);
169 if seg_len <= 1e-9 {
170 continue;
171 }
172 let dir = (b - a).normalize();
173 while seg_len > remaining {
175 let cut = a + dir * remaining;
176 if on {
177 current.push(cut);
178 flush_dash(&mut current, &mut out);
179 } else {
180 current.clear();
181 current.push(cut);
182 }
183 a = cut;
184 seg_len -= remaining;
185 idx = (idx + 1) % pattern.len();
186 remaining = pattern[idx];
187 on = !on;
188 }
189 remaining -= seg_len;
190 if on {
191 current.push(b);
192 }
193 }
194 flush_dash(&mut current, &mut out);
195 }
196 out
197}
198
199fn flush_dash(current: &mut Vec<Point>, out: &mut Vec<Contour>) {
201 if current.len() >= 2 {
202 out.push(Contour { points: std::mem::take(current), closed: false });
203 } else {
204 current.clear();
205 }
206}
207
208fn stroke_contour(
209 contour: &Contour,
210 r: f32,
211 stroke: &Stroke,
212 tolerance: f32,
213 polys: &mut Vec<Vec<Point>>,
214) {
215 let pts = dedupe(&contour.points, contour.closed);
216
217 if pts.len() < 2 {
218 if pts.len() == 1 && stroke.line_cap == LineCap::Round {
220 push_disc(pts[0], r, tolerance, polys);
221 }
222 return;
223 }
224
225 let n = pts.len();
226 let closed = contour.closed;
227 let seg_count = if closed { n } else { n - 1 };
228
229 for i in 0..seg_count {
231 let a = pts[i];
232 let b = pts[(i + 1) % n];
233 let dir = (b - a).normalize();
234 if dir == Point::ZERO {
235 continue;
236 }
237 let normal = dir.left_normal() * r;
238 push_ccw(vec![a + normal, b + normal, b - normal, a - normal], polys);
239 }
240
241 if closed {
243 for i in 0..n {
244 let prev = pts[(i + n - 1) % n];
245 let v = pts[i];
246 let next = pts[(i + 1) % n];
247 add_join(prev, v, next, r, stroke, tolerance, polys);
248 }
249 } else {
250 for i in 1..n - 1 {
251 add_join(pts[i - 1], pts[i], pts[i + 1], r, stroke, tolerance, polys);
252 }
253 let start_dir = (pts[0] - pts[1]).normalize();
255 let end_dir = (pts[n - 1] - pts[n - 2]).normalize();
256 add_cap(pts[0], start_dir, r, stroke.line_cap, tolerance, polys);
257 add_cap(pts[n - 1], end_dir, r, stroke.line_cap, tolerance, polys);
258 }
259}
260
261fn add_join(
262 prev: Point,
263 v: Point,
264 next: Point,
265 r: f32,
266 stroke: &Stroke,
267 tolerance: f32,
268 polys: &mut Vec<Vec<Point>>,
269) {
270 let din = (v - prev).normalize();
271 let dout = (next - v).normalize();
272 if din == Point::ZERO || dout == Point::ZERO {
273 return;
274 }
275
276 match stroke.line_join {
277 LineJoin::Round => {
278 push_disc(v, r, tolerance, polys);
279 }
280 LineJoin::Bevel => {
281 add_bevel(v, din, dout, r, polys);
282 }
283 LineJoin::Miter => {
284 add_bevel(v, din, dout, r, polys);
285 add_miter(v, din, dout, r, stroke.miter_limit, polys);
286 }
287 }
288}
289
290fn add_bevel(v: Point, din: Point, dout: Point, r: f32, polys: &mut Vec<Vec<Point>>) {
292 let nin = din.left_normal() * r;
293 let nout = dout.left_normal() * r;
294 push_ccw(vec![v, v + nin, v + nout], polys);
295 push_ccw(vec![v, v - nin, v - nout], polys);
296}
297
298fn add_miter(
300 v: Point,
301 din: Point,
302 dout: Point,
303 r: f32,
304 miter_limit: f32,
305 polys: &mut Vec<Vec<Point>>,
306) {
307 let nin = din.left_normal() * r;
308 let nout = dout.left_normal() * r;
309 let turn = din.cross(dout);
311 let (a, da, b, db, base_a, base_b) = if turn < 0.0 {
312 (v + nin, din, v + nout, dout, v + nin, v + nout)
314 } else {
315 (v - nin, din, v - nout, dout, v - nin, v - nout)
316 };
317 if let Some(m) = line_intersection(a, da, b, db) {
318 if m.distance(v) <= miter_limit * r {
319 push_ccw(vec![base_a, m, base_b], polys);
320 }
321 }
322}
323
324fn add_cap(
325 p: Point,
326 out_dir: Point,
327 r: f32,
328 cap: LineCap,
329 tolerance: f32,
330 polys: &mut Vec<Vec<Point>>,
331) {
332 if out_dir == Point::ZERO {
333 return;
334 }
335 match cap {
336 LineCap::Butt => {}
337 LineCap::Round => push_disc(p, r, tolerance, polys),
338 LineCap::Square => {
339 let n = out_dir.left_normal() * r;
340 let e = out_dir * r;
341 push_ccw(vec![p + n, p - n, p - n + e, p + n + e], polys);
342 }
343 }
344}
345
346fn line_intersection(p: Point, d: Point, q: Point, e: Point) -> Option<Point> {
348 let denom = d.cross(e);
349 if denom.abs() < 1e-6 {
350 return None;
351 }
352 let t = (q - p).cross(e) / denom;
353 Some(p + d * t)
354}
355
356fn push_disc(center: Point, r: f32, tolerance: f32, polys: &mut Vec<Vec<Point>>) {
358 let segs = arc_segments(r, tolerance);
359 let mut pts = Vec::with_capacity(segs);
360 for i in 0..segs {
361 let a = (i as f32 / segs as f32) * 2.0 * PI;
362 pts.push(Point::new(center.x + r * a.cos(), center.y + r * a.sin()));
363 }
364 push_ccw(pts, polys);
365}
366
367fn arc_segments(r: f32, tol: f32) -> usize {
369 if r <= tol {
370 return 6;
371 }
372 let theta = 2.0 * (1.0 - tol / r).clamp(-1.0, 1.0).acos();
373 if theta <= 1e-3 {
374 return 64;
375 }
376 ((2.0 * PI / theta).ceil() as usize).clamp(8, 512)
377}
378
379fn push_ccw(mut pts: Vec<Point>, polys: &mut Vec<Vec<Point>>) {
381 if pts.len() < 3 {
382 return;
383 }
384 if signed_area(&pts) < 0.0 {
385 pts.reverse();
386 }
387 polys.push(pts);
388}
389
390fn signed_area(pts: &[Point]) -> f32 {
391 let mut area = 0.0;
392 for i in 0..pts.len() {
393 let a = pts[i];
394 let b = pts[(i + 1) % pts.len()];
395 area += a.cross(b);
396 }
397 area * 0.5
398}
399
400fn dedupe(points: &[Point], closed: bool) -> Vec<Point> {
402 let mut out: Vec<Point> = Vec::with_capacity(points.len());
403 for &p in points {
404 if out.last().is_none_or(|&l: &Point| l.distance(p) > 1e-4) {
405 out.push(p);
406 }
407 }
408 if closed && out.len() >= 2 && out[0].distance(out[out.len() - 1]) <= 1e-4 {
409 out.pop();
410 }
411 out
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::path::Contour;
418
419 #[test]
420 fn stroke_segment_makes_quad() {
421 let contour = Contour { points: vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0)], closed: false };
422 let polys = build_stroke(&[contour], &Stroke { width: 2.0, ..Stroke::default() }, 0.1);
423 assert!(!polys.is_empty());
424 assert_eq!(polys[0].len(), 4);
426 }
427
428 #[test]
429 fn dash_splits_segment_into_runs() {
430 let contours = vec![Contour {
432 points: vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0)],
433 closed: false,
434 }];
435 let dashed = apply_dash(&contours, &[2.0, 2.0], 0.0);
436 assert_eq!(dashed.len(), 3, "expected three runs");
437 assert!((dashed[0].points[0].x - 0.0).abs() < 1e-4);
438 assert!((dashed[0].points[1].x - 2.0).abs() < 1e-4);
439 assert!((dashed[2].points[0].x - 8.0).abs() < 1e-4);
440 assert!((dashed[2].points[1].x - 10.0).abs() < 1e-4);
441 }
442
443 #[test]
444 fn dash_offset_shifts_phase() {
445 let contours = vec![Contour {
448 points: vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0)],
449 closed: false,
450 }];
451 let dashed = apply_dash(&contours, &[2.0, 2.0], 1.0);
452 assert!((dashed[0].points[0].x - 0.0).abs() < 1e-4, "first run from zero");
453 assert!((dashed[0].points[1].x - 1.0).abs() < 1e-4, "shortened to 1");
454 assert!((dashed[1].points[0].x - 3.0).abs() < 1e-4, "second run from 3");
455 }
456
457 #[test]
458 fn build_stroke_dashed_produces_multiple_runs() {
459 let contour = Contour {
460 points: vec![Point::new(0.0, 0.0), Point::new(20.0, 0.0)],
461 closed: false,
462 };
463 let s = Stroke { width: 2.0, dash: vec![4.0, 4.0], ..Stroke::default() };
464 let polys = build_stroke(&[contour], &s, 0.1);
465 assert!(polys.len() >= 3, "polys={}", polys.len());
467 }
468
469 #[test]
470 fn round_cap_adds_discs() {
471 let contour = Contour { points: vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0)], closed: false };
472 let s = Stroke { width: 4.0, line_cap: LineCap::Round, ..Stroke::default() };
473 let polys = build_stroke(&[contour], &s, 0.1);
474 assert!(polys.len() >= 3);
476 }
477}