/rust/registry/src/index.crates.io-1949cf8c6b5b557f/kurbo-0.12.0/src/stroke.rs
Line | Count | Source |
1 | | // Copyright 2023 the Kurbo Authors |
2 | | // SPDX-License-Identifier: Apache-2.0 OR MIT |
3 | | |
4 | | use core::{borrow::Borrow, f64::consts::PI}; |
5 | | |
6 | | use alloc::vec::Vec; |
7 | | |
8 | | use smallvec::SmallVec; |
9 | | |
10 | | #[cfg(not(feature = "std"))] |
11 | | use crate::common::FloatFuncs; |
12 | | |
13 | | use crate::{ |
14 | | common::solve_quadratic, Affine, Arc, BezPath, CubicBez, Line, ParamCurve, ParamCurveArclen, |
15 | | PathEl, PathSeg, Point, QuadBez, Vec2, |
16 | | }; |
17 | | |
18 | | /// Defines the connection between two segments of a stroke. |
19 | | #[derive(Copy, Clone, PartialEq, Eq, Debug)] |
20 | | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
21 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
22 | | pub enum Join { |
23 | | /// A straight line connecting the segments. |
24 | | Bevel, |
25 | | /// The segments are extended to their natural intersection point. |
26 | | Miter, |
27 | | /// An arc between the segments. |
28 | | Round, |
29 | | } |
30 | | |
31 | | /// Defines the shape to be drawn at the ends of a stroke. |
32 | | #[derive(Copy, Clone, PartialEq, Eq, Debug)] |
33 | | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
34 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
35 | | pub enum Cap { |
36 | | /// Flat cap. |
37 | | Butt, |
38 | | /// Square cap with dimensions equal to half the stroke width. |
39 | | Square, |
40 | | /// Rounded cap with radius equal to half the stroke width. |
41 | | Round, |
42 | | } |
43 | | |
44 | | /// The visual style of a stroke. |
45 | | #[derive(Clone, Debug, PartialEq)] |
46 | | #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] |
47 | | #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] |
48 | | pub struct Stroke { |
49 | | /// Width of the stroke. |
50 | | pub width: f64, |
51 | | /// Style for connecting segments of the stroke. |
52 | | pub join: Join, |
53 | | /// Limit for miter joins. |
54 | | pub miter_limit: f64, |
55 | | /// Style for capping the beginning of an open subpath. |
56 | | pub start_cap: Cap, |
57 | | /// Style for capping the end of an open subpath. |
58 | | pub end_cap: Cap, |
59 | | /// Lengths of dashes in alternating on/off order. |
60 | | pub dash_pattern: Dashes, |
61 | | /// Offset of the first dash. |
62 | | pub dash_offset: f64, |
63 | | } |
64 | | |
65 | | /// Options for path stroking. |
66 | | #[derive(Clone, Copy, Debug, PartialEq)] |
67 | | pub struct StrokeOpts { |
68 | | opt_level: StrokeOptLevel, |
69 | | } |
70 | | |
71 | | /// Optimization level for computing stroke outlines. |
72 | | /// |
73 | | /// Note that in the current implementation, this setting has no effect. |
74 | | /// However, having a tradeoff between optimization of number of segments |
75 | | /// and speed makes sense and may be added in the future, so applications |
76 | | /// should set it appropriately. For real time rendering, the appropriate |
77 | | /// value is `Subdivide`. |
78 | | #[derive(Clone, Copy, Debug, Eq, PartialEq)] |
79 | | pub enum StrokeOptLevel { |
80 | | /// Adaptively subdivide segments in half. |
81 | | Subdivide, |
82 | | /// Compute optimized subdivision points to minimize error. |
83 | | Optimized, |
84 | | } |
85 | | |
86 | | impl Default for StrokeOpts { |
87 | 0 | fn default() -> Self { |
88 | 0 | let opt_level = StrokeOptLevel::Subdivide; |
89 | 0 | StrokeOpts { opt_level } |
90 | 0 | } |
91 | | } |
92 | | |
93 | | impl Default for Stroke { |
94 | 0 | fn default() -> Self { |
95 | 0 | Self { |
96 | 0 | width: 1.0, |
97 | 0 | join: Join::Round, |
98 | 0 | miter_limit: 4.0, |
99 | 0 | start_cap: Cap::Round, |
100 | 0 | end_cap: Cap::Round, |
101 | 0 | dash_pattern: SmallVec::default(), |
102 | 0 | dash_offset: 0.0, |
103 | 0 | } |
104 | 0 | } |
105 | | } |
106 | | |
107 | | impl Stroke { |
108 | | /// Creates a new stroke with the specified width. |
109 | 0 | pub fn new(width: f64) -> Self { |
110 | 0 | Self { |
111 | 0 | width, |
112 | 0 | ..Stroke::default() |
113 | 0 | } |
114 | 0 | } |
115 | | |
116 | | /// Builder method for setting the join style. |
117 | 0 | pub fn with_join(mut self, join: Join) -> Self { |
118 | 0 | self.join = join; |
119 | 0 | self |
120 | 0 | } |
121 | | |
122 | | /// Builder method for setting the limit for miter joins. |
123 | 0 | pub fn with_miter_limit(mut self, limit: f64) -> Self { |
124 | 0 | self.miter_limit = limit; |
125 | 0 | self |
126 | 0 | } |
127 | | |
128 | | /// Builder method for setting the cap style for the start of the stroke. |
129 | 0 | pub fn with_start_cap(mut self, cap: Cap) -> Self { |
130 | 0 | self.start_cap = cap; |
131 | 0 | self |
132 | 0 | } |
133 | | |
134 | | /// Builder method for setting the cap style for the end of the stroke. |
135 | 0 | pub fn with_end_cap(mut self, cap: Cap) -> Self { |
136 | 0 | self.end_cap = cap; |
137 | 0 | self |
138 | 0 | } |
139 | | |
140 | | /// Builder method for setting the cap style. |
141 | 0 | pub fn with_caps(mut self, cap: Cap) -> Self { |
142 | 0 | self.start_cap = cap; |
143 | 0 | self.end_cap = cap; |
144 | 0 | self |
145 | 0 | } |
146 | | |
147 | | /// Builder method for setting the dashing parameters. |
148 | 0 | pub fn with_dashes<P>(mut self, offset: f64, pattern: P) -> Self |
149 | 0 | where |
150 | 0 | P: IntoIterator, |
151 | 0 | P::Item: Borrow<f64>, |
152 | | { |
153 | 0 | self.dash_offset = offset; |
154 | 0 | self.dash_pattern.clear(); |
155 | 0 | self.dash_pattern |
156 | 0 | .extend(pattern.into_iter().map(|dash| *dash.borrow())); |
157 | 0 | self |
158 | 0 | } |
159 | | } |
160 | | |
161 | | impl StrokeOpts { |
162 | | /// Set optimization level for computing stroke outlines. |
163 | 0 | pub fn opt_level(mut self, opt_level: StrokeOptLevel) -> Self { |
164 | 0 | self.opt_level = opt_level; |
165 | 0 | self |
166 | 0 | } |
167 | | } |
168 | | |
169 | | /// Collection of values representing lengths in a dash pattern. |
170 | | pub type Dashes = SmallVec<[f64; 4]>; |
171 | | |
172 | | /// A structure that is used for creating strokes. |
173 | | /// |
174 | | /// See also [`stroke_with`]. |
175 | | #[derive(Default, Debug)] |
176 | | pub struct StrokeCtx { |
177 | | // As a possible future optimization, we might not need separate storage |
178 | | // for forward and backward paths, we can add forward to the output in-place. |
179 | | // However, this structure is clearer and the cost fairly modest. |
180 | | output: BezPath, |
181 | | forward_path: BezPath, |
182 | | backward_path: BezPath, |
183 | | result_path: BezPath, |
184 | | start_pt: Point, |
185 | | start_norm: Vec2, |
186 | | start_tan: Vec2, |
187 | | last_pt: Point, |
188 | | last_tan: Vec2, |
189 | | // Precomputation of the join threshold, to optimize per-join logic. |
190 | | // If hypot < (hypot + dot) * join_thresh, omit join altogether. |
191 | | join_thresh: f64, |
192 | | } |
193 | | |
194 | | impl StrokeCtx { |
195 | | /// Return the path that defines the expanded stroke. |
196 | 0 | pub fn output(&self) -> &BezPath { |
197 | 0 | &self.output |
198 | 0 | } |
199 | | } |
200 | | |
201 | | impl StrokeCtx { |
202 | 0 | fn reset(&mut self) { |
203 | 0 | self.output.truncate(0); |
204 | 0 | self.forward_path.truncate(0); |
205 | 0 | self.backward_path.truncate(0); |
206 | 0 | self.start_pt = Point::default(); |
207 | 0 | self.start_norm = Vec2::default(); |
208 | 0 | self.start_tan = Vec2::default(); |
209 | 0 | self.last_pt = Point::default(); |
210 | 0 | self.last_tan = Vec2::default(); |
211 | 0 | self.join_thresh = 0.0; |
212 | 0 | } |
213 | | } |
214 | | |
215 | | /// Expand a stroke into a fill. |
216 | | /// |
217 | | /// The `tolerance` parameter controls the accuracy of the result. In general, |
218 | | /// the number of subdivisions in the output scales at least to the -1/4 power |
219 | | /// of the parameter, for example making it 1/16 as big generates twice as many |
220 | | /// segments. Currently the algorithm is not tuned for extremely fine tolerances. |
221 | | /// The theoretically optimum scaling exponent is -1/6, but achieving this may |
222 | | /// require slow numerical techniques (currently a subject of research). The |
223 | | /// appropriate value depends on the application; if the result of the stroke |
224 | | /// will be scaled up, a smaller value is needed. |
225 | | /// |
226 | | /// This method attempts a fairly high degree of correctness, but ultimately |
227 | | /// is based on computing parallel curves and adding joins and caps, rather than |
228 | | /// computing the rigorously correct parallel sweep (which requires evolutes in |
229 | | /// the general case). See [Nehab 2020] for more discussion. |
230 | | /// |
231 | | /// [Nehab 2020]: https://dl.acm.org/doi/10.1145/3386569.3392392 |
232 | 0 | pub fn stroke( |
233 | 0 | path: impl IntoIterator<Item = PathEl>, |
234 | 0 | style: &Stroke, |
235 | 0 | _opts: &StrokeOpts, |
236 | 0 | tolerance: f64, |
237 | 0 | ) -> BezPath { |
238 | 0 | let mut ctx = StrokeCtx::default(); |
239 | 0 | stroke_with(path, style, _opts, tolerance, &mut ctx); |
240 | | |
241 | 0 | ctx.output |
242 | 0 | } |
243 | | |
244 | | /// Expand a stroke into a fill. |
245 | | /// |
246 | | /// This is the same as [`stroke`], except for the fact that you can explicitly pass a |
247 | | /// `StrokeCtx`. By doing so, you can reuse the same context over multiple calls and ensure |
248 | | /// that the number of reallocations is minimized. |
249 | | /// |
250 | | /// Unlike [`stroke`], this method doesn't return an owned version of the expanded stroke as a |
251 | | /// [`BezPath`]. Instead, you can get a reference to the resulting path by calling |
252 | | /// [`StrokeCtx::output`]. |
253 | 0 | pub fn stroke_with( |
254 | 0 | path: impl IntoIterator<Item = PathEl>, |
255 | 0 | style: &Stroke, |
256 | 0 | _opts: &StrokeOpts, |
257 | 0 | tolerance: f64, |
258 | 0 | ctx: &mut StrokeCtx, |
259 | 0 | ) { |
260 | 0 | if style.dash_pattern.is_empty() { |
261 | 0 | stroke_undashed(path, style, tolerance, ctx); |
262 | 0 | } else { |
263 | 0 | let dashed = dash(path.into_iter(), style.dash_offset, &style.dash_pattern); |
264 | 0 | stroke_undashed(dashed, style, tolerance, ctx); |
265 | 0 | } |
266 | 0 | } |
267 | | |
268 | | /// Version of stroke expansion for styles with no dashes. |
269 | 0 | fn stroke_undashed( |
270 | 0 | path: impl IntoIterator<Item = PathEl>, |
271 | 0 | style: &Stroke, |
272 | 0 | tolerance: f64, |
273 | 0 | ctx: &mut StrokeCtx, |
274 | 0 | ) { |
275 | 0 | ctx.reset(); |
276 | 0 | ctx.join_thresh = 2.0 * tolerance / style.width; |
277 | | |
278 | 0 | for el in path { |
279 | 0 | let p0 = ctx.last_pt; |
280 | 0 | match el { |
281 | 0 | PathEl::MoveTo(p) => { |
282 | 0 | ctx.finish(style); |
283 | 0 | ctx.start_pt = p; |
284 | 0 | ctx.last_pt = p; |
285 | 0 | } |
286 | 0 | PathEl::LineTo(p1) => { |
287 | 0 | if p1 != p0 { |
288 | 0 | let tangent = p1 - p0; |
289 | 0 | ctx.do_join(style, tangent); |
290 | 0 | ctx.last_tan = tangent; |
291 | 0 | ctx.do_line(style, tangent, p1); |
292 | 0 | } |
293 | | } |
294 | 0 | PathEl::QuadTo(p1, p2) => { |
295 | 0 | if p1 != p0 || p2 != p0 { |
296 | 0 | let q = QuadBez::new(p0, p1, p2); |
297 | 0 | let (tan0, tan1) = PathSeg::Quad(q).tangents(); |
298 | 0 | ctx.do_join(style, tan0); |
299 | 0 | ctx.do_cubic(style, q.raise(), tolerance); |
300 | 0 | ctx.last_tan = tan1; |
301 | 0 | } |
302 | | } |
303 | 0 | PathEl::CurveTo(p1, p2, p3) => { |
304 | 0 | if p1 != p0 || p2 != p0 || p3 != p0 { |
305 | 0 | let c = CubicBez::new(p0, p1, p2, p3); |
306 | 0 | let (tan0, tan1) = PathSeg::Cubic(c).tangents(); |
307 | 0 | ctx.do_join(style, tan0); |
308 | 0 | ctx.do_cubic(style, c, tolerance); |
309 | 0 | ctx.last_tan = tan1; |
310 | 0 | } |
311 | | } |
312 | | PathEl::ClosePath => { |
313 | 0 | if p0 != ctx.start_pt { |
314 | 0 | let tangent = ctx.start_pt - p0; |
315 | 0 | ctx.do_join(style, tangent); |
316 | 0 | ctx.last_tan = tangent; |
317 | 0 | ctx.do_line(style, tangent, ctx.start_pt); |
318 | 0 | } |
319 | 0 | ctx.finish_closed(style); |
320 | | } |
321 | | } |
322 | | } |
323 | 0 | ctx.finish(style); |
324 | 0 | } |
325 | | |
326 | 0 | fn round_cap(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2) { |
327 | 0 | round_join(out, tolerance, center, norm, PI); |
328 | 0 | } |
329 | | |
330 | 0 | fn round_join(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) { |
331 | 0 | let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]); |
332 | 0 | let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0); |
333 | 0 | arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3)); |
334 | 0 | } |
335 | | |
336 | 0 | fn round_join_rev(out: &mut BezPath, tolerance: f64, center: Point, norm: Vec2, angle: f64) { |
337 | 0 | let a = Affine::new([norm.x, norm.y, norm.y, -norm.x, center.x, center.y]); |
338 | 0 | let arc = Arc::new(Point::ORIGIN, (1.0, 1.0), PI - angle, angle, 0.0); |
339 | 0 | arc.to_cubic_beziers(tolerance, |p1, p2, p3| out.curve_to(a * p1, a * p2, a * p3)); |
340 | 0 | } |
341 | | |
342 | 0 | fn square_cap(out: &mut BezPath, close: bool, center: Point, norm: Vec2) { |
343 | 0 | let a = Affine::new([norm.x, norm.y, -norm.y, norm.x, center.x, center.y]); |
344 | 0 | out.line_to(a * Point::new(1.0, 1.0)); |
345 | 0 | out.line_to(a * Point::new(-1.0, 1.0)); |
346 | 0 | if close { |
347 | 0 | out.close_path(); |
348 | 0 | } else { |
349 | 0 | out.line_to(a * Point::new(-1.0, 0.0)); |
350 | 0 | } |
351 | 0 | } |
352 | | |
353 | 0 | fn extend_reversed(out: &mut BezPath, elements: &[PathEl]) { |
354 | 0 | for i in (1..elements.len()).rev() { |
355 | 0 | let end = elements[i - 1].end_point().unwrap(); |
356 | 0 | match elements[i] { |
357 | 0 | PathEl::LineTo(_) => out.line_to(end), |
358 | 0 | PathEl::QuadTo(p1, _) => out.quad_to(p1, end), |
359 | 0 | PathEl::CurveTo(p1, p2, _) => out.curve_to(p2, p1, end), |
360 | 0 | _ => unreachable!(), |
361 | | } |
362 | | } |
363 | 0 | } |
364 | | |
365 | | impl StrokeCtx { |
366 | | /// Append forward and backward paths to output. |
367 | 0 | fn finish(&mut self, style: &Stroke) { |
368 | | // TODO: scale |
369 | 0 | let tolerance = 1e-3; |
370 | 0 | if self.forward_path.is_empty() { |
371 | 0 | return; |
372 | 0 | } |
373 | 0 | self.output.extend(&self.forward_path); |
374 | 0 | let back_els = self.backward_path.elements(); |
375 | 0 | let return_p = back_els[back_els.len() - 1].end_point().unwrap(); |
376 | 0 | let d = self.last_pt - return_p; |
377 | 0 | match style.end_cap { |
378 | 0 | Cap::Butt => self.output.line_to(return_p), |
379 | 0 | Cap::Round => round_cap(&mut self.output, tolerance, self.last_pt, d), |
380 | 0 | Cap::Square => square_cap(&mut self.output, false, self.last_pt, d), |
381 | | } |
382 | 0 | extend_reversed(&mut self.output, back_els); |
383 | 0 | match style.start_cap { |
384 | 0 | Cap::Butt => self.output.close_path(), |
385 | 0 | Cap::Round => round_cap(&mut self.output, tolerance, self.start_pt, self.start_norm), |
386 | 0 | Cap::Square => square_cap(&mut self.output, true, self.start_pt, self.start_norm), |
387 | | } |
388 | | |
389 | 0 | self.forward_path.truncate(0); |
390 | 0 | self.backward_path.truncate(0); |
391 | 0 | } |
392 | | |
393 | | /// Finish a closed path |
394 | 0 | fn finish_closed(&mut self, style: &Stroke) { |
395 | 0 | if self.forward_path.is_empty() { |
396 | 0 | return; |
397 | 0 | } |
398 | 0 | self.do_join(style, self.start_tan); |
399 | 0 | self.output.extend(&self.forward_path); |
400 | 0 | self.output.close_path(); |
401 | 0 | let back_els = self.backward_path.elements(); |
402 | 0 | let last_pt = back_els[back_els.len() - 1].end_point().unwrap(); |
403 | 0 | self.output.move_to(last_pt); |
404 | 0 | extend_reversed(&mut self.output, back_els); |
405 | 0 | self.output.close_path(); |
406 | 0 | self.forward_path.truncate(0); |
407 | 0 | self.backward_path.truncate(0); |
408 | 0 | } |
409 | | |
410 | 0 | fn do_join(&mut self, style: &Stroke, tan0: Vec2) { |
411 | | // TODO: scale |
412 | 0 | let tolerance = 1e-3; |
413 | 0 | let scale = 0.5 * style.width / tan0.hypot(); |
414 | 0 | let norm = scale * Vec2::new(-tan0.y, tan0.x); |
415 | 0 | let p0 = self.last_pt; |
416 | 0 | if self.forward_path.elements().is_empty() { |
417 | 0 | self.forward_path.move_to(p0 - norm); |
418 | 0 | self.backward_path.move_to(p0 + norm); |
419 | 0 | self.start_tan = tan0; |
420 | 0 | self.start_norm = norm; |
421 | 0 | } else { |
422 | 0 | let ab = self.last_tan; |
423 | 0 | let cd = tan0; |
424 | 0 | let cross = ab.cross(cd); |
425 | 0 | let dot = ab.dot(cd); |
426 | 0 | let hypot = cross.hypot(dot); |
427 | | // possible TODO: a minor speedup could be squaring both sides |
428 | 0 | if dot <= 0.0 || cross.abs() >= hypot * self.join_thresh { |
429 | 0 | match style.join { |
430 | 0 | Join::Bevel => { |
431 | 0 | self.forward_path.line_to(p0 - norm); |
432 | 0 | self.backward_path.line_to(p0 + norm); |
433 | 0 | } |
434 | | Join::Miter => { |
435 | 0 | if 2.0 * hypot < (hypot + dot) * style.miter_limit.powi(2) { |
436 | | // TODO: maybe better to store last_norm or derive from path? |
437 | 0 | let last_scale = 0.5 * style.width / ab.hypot(); |
438 | 0 | let last_norm = last_scale * Vec2::new(-ab.y, ab.x); |
439 | 0 | if cross > 0.0 { |
440 | 0 | let fp_last = p0 - last_norm; |
441 | 0 | let fp_this = p0 - norm; |
442 | 0 | let h = ab.cross(fp_this - fp_last) / cross; |
443 | 0 | let miter_pt = fp_this - cd * h; |
444 | 0 | self.forward_path.line_to(miter_pt); |
445 | 0 | self.backward_path.line_to(p0); |
446 | 0 | } else if cross < 0.0 { |
447 | 0 | let fp_last = p0 + last_norm; |
448 | 0 | let fp_this = p0 + norm; |
449 | 0 | let h = ab.cross(fp_this - fp_last) / cross; |
450 | 0 | let miter_pt = fp_this - cd * h; |
451 | 0 | self.backward_path.line_to(miter_pt); |
452 | 0 | self.forward_path.line_to(p0); |
453 | 0 | } |
454 | 0 | } |
455 | 0 | self.forward_path.line_to(p0 - norm); |
456 | 0 | self.backward_path.line_to(p0 + norm); |
457 | | } |
458 | | Join::Round => { |
459 | 0 | let angle = cross.atan2(dot); |
460 | 0 | if angle > 0.0 { |
461 | 0 | self.backward_path.line_to(p0 + norm); |
462 | 0 | round_join(&mut self.forward_path, tolerance, p0, norm, angle); |
463 | 0 | } else { |
464 | 0 | self.forward_path.line_to(p0 - norm); |
465 | 0 | round_join_rev(&mut self.backward_path, tolerance, p0, -norm, -angle); |
466 | 0 | } |
467 | | } |
468 | | } |
469 | 0 | } |
470 | | } |
471 | 0 | } |
472 | | |
473 | 0 | fn do_line(&mut self, style: &Stroke, tangent: Vec2, p1: Point) { |
474 | 0 | let scale = 0.5 * style.width / tangent.hypot(); |
475 | 0 | let norm = scale * Vec2::new(-tangent.y, tangent.x); |
476 | 0 | self.forward_path.line_to(p1 - norm); |
477 | 0 | self.backward_path.line_to(p1 + norm); |
478 | 0 | self.last_pt = p1; |
479 | 0 | } |
480 | | |
481 | 0 | fn do_cubic(&mut self, style: &Stroke, c: CubicBez, tolerance: f64) { |
482 | | // First, detect degenerate linear case |
483 | | |
484 | | // Ordinarily, this is the direction of the chord, but if the chord is very |
485 | | // short, we take the longer control arm. |
486 | 0 | let chord = c.p3 - c.p0; |
487 | 0 | let mut chord_ref = chord; |
488 | 0 | let mut chord_ref_hypot2 = chord_ref.hypot2(); |
489 | 0 | let d01 = c.p1 - c.p0; |
490 | 0 | if d01.hypot2() > chord_ref_hypot2 { |
491 | 0 | chord_ref = d01; |
492 | 0 | chord_ref_hypot2 = chord_ref.hypot2(); |
493 | 0 | } |
494 | 0 | let d23 = c.p3 - c.p2; |
495 | 0 | if d23.hypot2() > chord_ref_hypot2 { |
496 | 0 | chord_ref = d23; |
497 | 0 | chord_ref_hypot2 = chord_ref.hypot2(); |
498 | 0 | } |
499 | | // Project Bézier onto chord |
500 | 0 | let p0 = c.p0.to_vec2().dot(chord_ref); |
501 | 0 | let p1 = c.p1.to_vec2().dot(chord_ref); |
502 | 0 | let p2 = c.p2.to_vec2().dot(chord_ref); |
503 | 0 | let p3 = c.p3.to_vec2().dot(chord_ref); |
504 | | const ENDPOINT_D: f64 = 0.01; |
505 | 0 | if p3 <= p0 |
506 | 0 | || p1 > p2 |
507 | 0 | || p1 < p0 + ENDPOINT_D * (p3 - p0) |
508 | 0 | || p2 > p3 - ENDPOINT_D * (p3 - p0) |
509 | | { |
510 | | // potentially a cusp inside |
511 | 0 | let x01 = d01.cross(chord_ref); |
512 | 0 | let x23 = d23.cross(chord_ref); |
513 | 0 | let x03 = chord.cross(chord_ref); |
514 | 0 | let thresh = tolerance.powi(2) * chord_ref_hypot2; |
515 | 0 | if x01 * x01 < thresh && x23 * x23 < thresh && x03 * x03 < thresh { |
516 | | // control points are nearly co-linear |
517 | 0 | let midpoint = c.p0.midpoint(c.p3); |
518 | | // Mapping back from projection of reference chord |
519 | 0 | let ref_vec = chord_ref / chord_ref_hypot2; |
520 | 0 | let ref_pt = midpoint - 0.5 * (p0 + p3) * ref_vec; |
521 | 0 | self.do_linear(style, c, [p0, p1, p2, p3], ref_pt, ref_vec); |
522 | 0 | return; |
523 | 0 | } |
524 | 0 | } |
525 | | |
526 | 0 | crate::offset::offset_cubic(c, -0.5 * style.width, tolerance, &mut self.result_path); |
527 | 0 | self.forward_path.extend(self.result_path.iter().skip(1)); |
528 | 0 | crate::offset::offset_cubic(c, 0.5 * style.width, tolerance, &mut self.result_path); |
529 | 0 | self.backward_path.extend(self.result_path.iter().skip(1)); |
530 | 0 | self.last_pt = c.p3; |
531 | 0 | } |
532 | | |
533 | | /// Do a cubic which is actually linear. |
534 | | /// |
535 | | /// The `p` argument is the control points projected to the reference chord. |
536 | | /// The ref arguments are the inverse map of a projection back to the client |
537 | | /// coordinate space. |
538 | 0 | fn do_linear( |
539 | 0 | &mut self, |
540 | 0 | style: &Stroke, |
541 | 0 | c: CubicBez, |
542 | 0 | p: [f64; 4], |
543 | 0 | ref_pt: Point, |
544 | 0 | ref_vec: Vec2, |
545 | 0 | ) { |
546 | | // Always do round join, to model cusp as limit of finite curvature (see Nehab). |
547 | 0 | let style = Stroke::new(style.width).with_join(Join::Round); |
548 | | // Tangents of endpoints (for connecting to joins) |
549 | 0 | let (tan0, tan1) = PathSeg::Cubic(c).tangents(); |
550 | 0 | self.last_tan = tan0; |
551 | | // find cusps |
552 | 0 | let c0 = p[1] - p[0]; |
553 | 0 | let c1 = 2.0 * p[2] - 4.0 * p[1] + 2.0 * p[0]; |
554 | 0 | let c2 = p[3] - 3.0 * p[2] + 3.0 * p[1] - p[0]; |
555 | 0 | let roots = solve_quadratic(c0, c1, c2); |
556 | | // discard cusps right at endpoints |
557 | | const EPSILON: f64 = 1e-6; |
558 | 0 | for t in roots { |
559 | 0 | if t > EPSILON && t < 1.0 - EPSILON { |
560 | 0 | let mt = 1.0 - t; |
561 | 0 | let z = mt * (mt * mt * p[0] + 3.0 * t * (mt * p[1] + t * p[2])) + t * t * t * p[3]; |
562 | 0 | let p = ref_pt + z * ref_vec; |
563 | 0 | let tan = p - self.last_pt; |
564 | 0 | self.do_join(&style, tan); |
565 | 0 | self.do_line(&style, tan, p); |
566 | 0 | self.last_tan = tan; |
567 | 0 | } |
568 | | } |
569 | 0 | let tan = c.p3 - self.last_pt; |
570 | 0 | self.do_join(&style, tan); |
571 | 0 | self.do_line(&style, tan, c.p3); |
572 | 0 | self.last_tan = tan; |
573 | 0 | self.do_join(&style, tan1); |
574 | 0 | } |
575 | | } |
576 | | |
577 | | /// An implementation of dashing as an iterator-to-iterator transformation. |
578 | | struct DashIterator<'a, T> { |
579 | | inner: T, |
580 | | input_done: bool, |
581 | | closepath_pending: bool, |
582 | | dashes: &'a [f64], |
583 | | dash_ix: usize, |
584 | | init_dash_ix: usize, |
585 | | init_dash_remaining: f64, |
586 | | init_is_active: bool, |
587 | | is_active: bool, |
588 | | state: DashState, |
589 | | current_seg: PathSeg, |
590 | | t: f64, |
591 | | dash_remaining: f64, |
592 | | seg_remaining: f64, |
593 | | start_pt: Point, |
594 | | last_pt: Point, |
595 | | stash: Vec<PathEl>, |
596 | | stash_ix: usize, |
597 | | } |
598 | | |
599 | | #[derive(PartialEq, Eq)] |
600 | | enum DashState { |
601 | | NeedInput, |
602 | | ToStash, |
603 | | Working, |
604 | | FromStash, |
605 | | } |
606 | | |
607 | | impl<T: Iterator<Item = PathEl>> Iterator for DashIterator<'_, T> { |
608 | | type Item = PathEl; |
609 | | |
610 | 0 | fn next(&mut self) -> Option<PathEl> { |
611 | | loop { |
612 | 0 | match self.state { |
613 | | DashState::NeedInput => { |
614 | 0 | if self.input_done { |
615 | 0 | return None; |
616 | 0 | } |
617 | 0 | self.get_input(); |
618 | 0 | if self.input_done { |
619 | 0 | return None; |
620 | 0 | } |
621 | 0 | self.state = DashState::ToStash; |
622 | | } |
623 | | DashState::ToStash => { |
624 | 0 | if let Some(el) = self.step() { |
625 | 0 | self.stash.push(el); |
626 | 0 | } |
627 | | } |
628 | | DashState::Working => { |
629 | 0 | if let Some(el) = self.step() { |
630 | 0 | return Some(el); |
631 | 0 | } |
632 | | } |
633 | | DashState::FromStash => { |
634 | 0 | if let Some(el) = self.stash.get(self.stash_ix) { |
635 | 0 | self.stash_ix += 1; |
636 | 0 | return Some(*el); |
637 | | } else { |
638 | 0 | self.stash.clear(); |
639 | 0 | self.stash_ix = 0; |
640 | 0 | if self.input_done { |
641 | 0 | return None; |
642 | 0 | } |
643 | 0 | if self.closepath_pending { |
644 | 0 | self.closepath_pending = false; |
645 | 0 | self.state = DashState::NeedInput; |
646 | 0 | } else { |
647 | 0 | self.state = DashState::ToStash; |
648 | 0 | } |
649 | | } |
650 | | } |
651 | | } |
652 | | } |
653 | 0 | } |
654 | | } |
655 | | |
656 | 0 | fn seg_to_el(el: &PathSeg) -> PathEl { |
657 | 0 | match el { |
658 | 0 | PathSeg::Line(l) => PathEl::LineTo(l.p1), |
659 | 0 | PathSeg::Quad(q) => PathEl::QuadTo(q.p1, q.p2), |
660 | 0 | PathSeg::Cubic(c) => PathEl::CurveTo(c.p1, c.p2, c.p3), |
661 | | } |
662 | 0 | } |
663 | | |
664 | | const DASH_ACCURACY: f64 = 1e-6; |
665 | | |
666 | | /// Create a new dashing iterator. |
667 | | /// |
668 | | /// Handling of dashes is fairly orthogonal to stroke expansion. This iterator |
669 | | /// is an internal detail of the stroke expansion logic, but is also available |
670 | | /// separately, and is expected to be useful when doing stroke expansion on |
671 | | /// GPU. |
672 | | /// |
673 | | /// It is implemented as an iterator-to-iterator transform. Because it consumes |
674 | | /// the input sequentially and produces consistent output with correct joins, |
675 | | /// it requires internal state and may allocate. |
676 | | /// |
677 | | /// Accuracy is currently hard-coded to 1e-6. This is better than generally |
678 | | /// expected, and care is taken to get cusps correct, among other things. |
679 | 0 | pub fn dash<'a>( |
680 | 0 | inner: impl Iterator<Item = PathEl> + 'a, |
681 | 0 | dash_offset: f64, |
682 | 0 | dashes: &'a [f64], |
683 | 0 | ) -> impl Iterator<Item = PathEl> + 'a { |
684 | | // ensure that offset is positive and minimal by normalization using period |
685 | 0 | let period = dashes.iter().sum(); |
686 | 0 | let dash_offset = dash_offset.rem_euclid(period); |
687 | | |
688 | 0 | let mut dash_ix = 0; |
689 | 0 | let mut dash_remaining = dashes[dash_ix] - dash_offset; |
690 | 0 | let mut is_active = true; |
691 | | // Find place in dashes array for initial offset. |
692 | 0 | while dash_remaining < 0.0 { |
693 | 0 | dash_ix = (dash_ix + 1) % dashes.len(); |
694 | 0 | dash_remaining += dashes[dash_ix]; |
695 | 0 | is_active = !is_active; |
696 | 0 | } |
697 | 0 | DashIterator { |
698 | 0 | inner, |
699 | 0 | input_done: false, |
700 | 0 | closepath_pending: false, |
701 | 0 | dashes, |
702 | 0 | dash_ix, |
703 | 0 | init_dash_ix: dash_ix, |
704 | 0 | init_dash_remaining: dash_remaining, |
705 | 0 | init_is_active: is_active, |
706 | 0 | is_active, |
707 | 0 | state: DashState::NeedInput, |
708 | 0 | current_seg: PathSeg::Line(Line::new(Point::ORIGIN, Point::ORIGIN)), |
709 | 0 | t: 0.0, |
710 | 0 | dash_remaining, |
711 | 0 | seg_remaining: 0.0, |
712 | 0 | start_pt: Point::ORIGIN, |
713 | 0 | last_pt: Point::ORIGIN, |
714 | 0 | stash: Vec::new(), |
715 | 0 | stash_ix: 0, |
716 | 0 | } |
717 | 0 | } |
718 | | |
719 | | impl<'a, T: Iterator<Item = PathEl>> DashIterator<'a, T> { |
720 | 0 | fn get_input(&mut self) { |
721 | | loop { |
722 | 0 | if self.closepath_pending { |
723 | 0 | self.handle_closepath(); |
724 | 0 | break; |
725 | 0 | } |
726 | 0 | let Some(next_el) = self.inner.next() else { |
727 | 0 | self.input_done = true; |
728 | 0 | self.state = DashState::FromStash; |
729 | 0 | return; |
730 | | }; |
731 | 0 | let p0 = self.last_pt; |
732 | 0 | match next_el { |
733 | 0 | PathEl::MoveTo(p) => { |
734 | 0 | if !self.stash.is_empty() { |
735 | 0 | self.state = DashState::FromStash; |
736 | 0 | } |
737 | 0 | self.start_pt = p; |
738 | 0 | self.last_pt = p; |
739 | 0 | self.reset_phase(); |
740 | 0 | continue; |
741 | | } |
742 | 0 | PathEl::LineTo(p1) => { |
743 | 0 | let l = Line::new(p0, p1); |
744 | 0 | self.seg_remaining = l.arclen(DASH_ACCURACY); |
745 | 0 | self.current_seg = PathSeg::Line(l); |
746 | 0 | self.last_pt = p1; |
747 | 0 | } |
748 | 0 | PathEl::QuadTo(p1, p2) => { |
749 | 0 | let q = QuadBez::new(p0, p1, p2); |
750 | 0 | self.seg_remaining = q.arclen(DASH_ACCURACY); |
751 | 0 | self.current_seg = PathSeg::Quad(q); |
752 | 0 | self.last_pt = p2; |
753 | 0 | } |
754 | 0 | PathEl::CurveTo(p1, p2, p3) => { |
755 | 0 | let c = CubicBez::new(p0, p1, p2, p3); |
756 | 0 | self.seg_remaining = c.arclen(DASH_ACCURACY); |
757 | 0 | self.current_seg = PathSeg::Cubic(c); |
758 | 0 | self.last_pt = p3; |
759 | 0 | } |
760 | | PathEl::ClosePath => { |
761 | 0 | self.closepath_pending = true; |
762 | 0 | if p0 != self.start_pt { |
763 | 0 | let l = Line::new(p0, self.start_pt); |
764 | 0 | self.seg_remaining = l.arclen(DASH_ACCURACY); |
765 | 0 | self.current_seg = PathSeg::Line(l); |
766 | 0 | self.last_pt = self.start_pt; |
767 | 0 | } else { |
768 | 0 | self.handle_closepath(); |
769 | 0 | } |
770 | | } |
771 | | } |
772 | 0 | break; |
773 | | } |
774 | 0 | self.t = 0.0; |
775 | 0 | } |
776 | | |
777 | | /// Move arc length forward to next event. |
778 | 0 | fn step(&mut self) -> Option<PathEl> { |
779 | 0 | let mut result = None; |
780 | 0 | if self.state == DashState::ToStash && self.stash.is_empty() { |
781 | 0 | if self.is_active { |
782 | 0 | result = Some(PathEl::MoveTo(self.current_seg.start())); |
783 | 0 | } else { |
784 | 0 | self.state = DashState::Working; |
785 | 0 | } |
786 | 0 | } else if self.dash_remaining < self.seg_remaining { |
787 | | // next transition is a dash transition |
788 | 0 | let seg = self.current_seg.subsegment(self.t..1.0); |
789 | 0 | let t1 = seg.inv_arclen(self.dash_remaining, DASH_ACCURACY); |
790 | 0 | if self.is_active { |
791 | 0 | let subseg = seg.subsegment(0.0..t1); |
792 | 0 | result = Some(seg_to_el(&subseg)); |
793 | 0 | self.state = DashState::Working; |
794 | 0 | } else { |
795 | 0 | let p = seg.eval(t1); |
796 | 0 | result = Some(PathEl::MoveTo(p)); |
797 | 0 | } |
798 | 0 | self.is_active = !self.is_active; |
799 | 0 | self.t += t1 * (1.0 - self.t); |
800 | 0 | self.seg_remaining -= self.dash_remaining; |
801 | 0 | self.dash_ix += 1; |
802 | 0 | if self.dash_ix == self.dashes.len() { |
803 | 0 | self.dash_ix = 0; |
804 | 0 | } |
805 | 0 | self.dash_remaining = self.dashes[self.dash_ix]; |
806 | | } else { |
807 | 0 | if self.is_active { |
808 | 0 | let seg = self.current_seg.subsegment(self.t..1.0); |
809 | 0 | result = Some(seg_to_el(&seg)); |
810 | 0 | } |
811 | 0 | self.dash_remaining -= self.seg_remaining; |
812 | 0 | self.get_input(); |
813 | | } |
814 | 0 | result |
815 | 0 | } |
816 | | |
817 | 0 | fn handle_closepath(&mut self) { |
818 | 0 | if self.state == DashState::ToStash { |
819 | 0 | // Have looped back without breaking a dash, just play it back |
820 | 0 | self.stash.push(PathEl::ClosePath); |
821 | 0 | } else if self.is_active { |
822 | 0 | // connect with path in stash, skip MoveTo. |
823 | 0 | self.stash_ix = 1; |
824 | 0 | } |
825 | 0 | self.state = DashState::FromStash; |
826 | 0 | self.reset_phase(); |
827 | 0 | } |
828 | | |
829 | 0 | fn reset_phase(&mut self) { |
830 | 0 | self.dash_ix = self.init_dash_ix; |
831 | 0 | self.dash_remaining = self.init_dash_remaining; |
832 | 0 | self.is_active = self.init_is_active; |
833 | 0 | } |
834 | | } |
835 | | |
836 | | #[cfg(test)] |
837 | | mod tests { |
838 | | use crate::{ |
839 | | dash, segments, stroke, BezPath, Cap::Butt, CubicBez, Join::Miter, Line, PathEl, PathSeg, |
840 | | Shape, Stroke, StrokeOpts, |
841 | | }; |
842 | | |
843 | | // A degenerate stroke with a cusp at the endpoint. |
844 | | #[test] |
845 | | fn pathological_stroke() { |
846 | | let curve = CubicBez::new( |
847 | | (602.469, 286.585), |
848 | | (641.975, 286.585), |
849 | | (562.963, 286.585), |
850 | | (562.963, 286.585), |
851 | | ); |
852 | | let path = curve.into_path(0.1); |
853 | | let stroke_style = Stroke::new(1.); |
854 | | let stroked = stroke(path, &stroke_style, &StrokeOpts::default(), 0.001); |
855 | | assert!(stroked.is_finite()); |
856 | | } |
857 | | |
858 | | #[test] |
859 | | /// <https://github.com/linebender/kurbo/issues/482> |
860 | | fn dash_miter_join() { |
861 | | let path = BezPath::from_vec(vec![ |
862 | | PathEl::MoveTo((70.0, 80.0).into()), |
863 | | PathEl::LineTo((0.0, 80.0).into()), |
864 | | PathEl::LineTo((0.0, 77.0).into()), |
865 | | ]); |
866 | | let expected_stroke = BezPath::from_vec(vec![ |
867 | | PathEl::MoveTo((70.0, 90.0).into()), |
868 | | PathEl::LineTo((0.0, 90.0).into()), |
869 | | // Miter join point on forward path |
870 | | PathEl::LineTo((-10.0, 90.0).into()), |
871 | | PathEl::LineTo((-10.0, 80.0).into()), |
872 | | PathEl::LineTo((-10.0, 77.0).into()), |
873 | | PathEl::LineTo((10.0, 77.0).into()), |
874 | | PathEl::LineTo((10.0, 80.0).into()), |
875 | | // Miter join point on backward path |
876 | | PathEl::LineTo((0.0, 80.0).into()), |
877 | | PathEl::LineTo((0.0, 70.0).into()), |
878 | | PathEl::LineTo((70.0, 70.0).into()), |
879 | | PathEl::ClosePath, |
880 | | ]); |
881 | | let stroke_style = Stroke::new(20.0) |
882 | | .with_join(Miter) |
883 | | .with_caps(Butt) |
884 | | .with_dashes(0.0, [73.0, 12.0]); |
885 | | let stroke = stroke(path, &stroke_style, &StrokeOpts::default(), 0.25); |
886 | | assert_eq!(stroke, expected_stroke); |
887 | | } |
888 | | |
889 | | // Test cases adapted from https://github.com/linebender/vello/pull/388 |
890 | | #[test] |
891 | | fn broken_strokes() { |
892 | | let broken_cubics = [ |
893 | | [ |
894 | | (465.24423, 107.11105), |
895 | | (475.50754, 107.11105), |
896 | | (475.50754, 107.11105), |
897 | | (475.50754, 107.11105), |
898 | | ], |
899 | | [(0., -0.01), (128., 128.001), (128., -0.01), (0., 128.001)], // Near-cusp |
900 | | [(0., 0.), (0., -10.), (0., -10.), (0., 10.)], // Flat line with 180 |
901 | | [(10., 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s |
902 | | [(39., -39.), (40., -40.), (40., -40.), (0., 0.)], // Flat diagonal with 180 |
903 | | [(40., 40.), (0., 0.), (200., 200.), (0., 0.)], // Diag w/ an internal 180 |
904 | | [(0., 0.), (1e-2, 0.), (-1e-2, 0.), (0., 0.)], // Circle |
905 | | // Flat line with no turns: |
906 | | [ |
907 | | (400.75, 100.05), |
908 | | (400.75, 100.05), |
909 | | (100.05, 300.95), |
910 | | (100.05, 300.95), |
911 | | ], |
912 | | [(0.5, 0.), (0., 0.), (20., 0.), (10., 0.)], // Flat line with 2 180s |
913 | | [(10., 0.), (0., 0.), (10., 0.), (10., 0.)], // Flat line with a 180 |
914 | | ]; |
915 | | let stroke_style = Stroke::new(30.).with_caps(Butt).with_join(Miter); |
916 | | for cubic in &broken_cubics { |
917 | | let path = CubicBez::new(cubic[0], cubic[1], cubic[2], cubic[3]).into_path(0.1); |
918 | | let stroked = stroke(path, &stroke_style, &StrokeOpts::default(), 0.001); |
919 | | assert!(stroked.is_finite()); |
920 | | } |
921 | | } |
922 | | |
923 | | #[test] |
924 | | fn dash_sequence() { |
925 | | let shape = Line::new((0.0, 0.0), (21.0, 0.0)); |
926 | | let dashes = [1., 5., 2., 5.]; |
927 | | let expansion = [ |
928 | | PathSeg::Line(Line::new((6., 0.), (8., 0.))), |
929 | | PathSeg::Line(Line::new((13., 0.), (14., 0.))), |
930 | | PathSeg::Line(Line::new((19., 0.), (21., 0.))), |
931 | | PathSeg::Line(Line::new((0., 0.), (1., 0.))), |
932 | | ]; |
933 | | let iter = segments(dash(shape.path_elements(0.), 0., &dashes)); |
934 | | assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion); |
935 | | } |
936 | | |
937 | | #[test] |
938 | | fn dash_sequence_offset() { |
939 | | // Same as dash_sequence, but with a dash offset |
940 | | // of 3, which skips the first dash and cuts into |
941 | | // the first gap. |
942 | | let shape = Line::new((0.0, 0.0), (21.0, 0.0)); |
943 | | let dashes = [1., 5., 2., 5.]; |
944 | | let expansion = [ |
945 | | PathSeg::Line(Line::new((3., 0.), (5., 0.))), |
946 | | PathSeg::Line(Line::new((10., 0.), (11., 0.))), |
947 | | PathSeg::Line(Line::new((16., 0.), (18., 0.))), |
948 | | ]; |
949 | | let iter = segments(dash(shape.path_elements(0.), 3., &dashes)); |
950 | | assert_eq!(iter.collect::<Vec<PathSeg>>(), expansion); |
951 | | } |
952 | | |
953 | | #[test] |
954 | | fn dash_negative_offset() { |
955 | | let shape = Line::new((0.0, 0.0), (28.0, 0.0)); |
956 | | let dashes = [4., 2.]; |
957 | | let pos = segments(dash(shape.path_elements(0.), 60., &dashes)).collect::<Vec<PathSeg>>(); |
958 | | let neg = segments(dash(shape.path_elements(0.), -60., &dashes)).collect::<Vec<PathSeg>>(); |
959 | | assert_eq!(neg, pos); |
960 | | } |
961 | | } |