Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/src/stroke.rs

37.8 KiB, 84 runs

created by r1870400018:13952, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Stroking: the ink a pen leaves as it travels a path.
2//!
3//! # A stroke is a fill
4//!
5//! Stroking is not a second kind of painting that needs a second rasteriser. The ink a pen leaves
6//! is a region of the plane like any other, so the whole job here is to build that region as a
7//! [`Path`], hand it to the filler, and add no code at all to the rasteriser. Everything the
8//! rasteriser already knows -- the analytic anti-aliasing, the clipping, the compositing -- comes
9//! for nothing.
10//!
11//! # How the region is built
12//!
13//! The tempting way is to offset the path to one side, offset it to the other, and sew the two
14//! offsets into a single outline. That way lies grief. On the inside of a turn tighter than the pen
15//! is wide the two offsets cross, and the outline ties itself into knots that only a
16//! boolean-geometry engine can untie.
17//!
18//! So the region is built instead as a heap of convex pieces: a quadrilateral for each straight run
19//! of the pen, a wedge or a triangle at each corner it turns, a cap at each loose end. Every piece
20//! is wound the same way, by [`piece`], and that is the whole trick. Wound alike they add and never
21//! cancel, so under the non-zero rule their union is exactly the ink -- knots, overlaps, hairpins
22//! and all. It is also why the path [`Path::stroke`] returns must be filled with
23//! [`crate::raster::FillRule::NonZero`]: fill it even-odd and every place two pieces overlap would
24//! come out as a hole.
25//!
26//! # Why the pieces meet rather than overlap
27//!
28//! Where two pieces can be cut to share an edge, they are. A bevel or a miter join meets the two
29//! runs it joins along the pen's end edge; a round join is a wedge of a disc rather than the whole
30//! disc, and a round cap a half disc rather than a whole one.
31//!
32//! This is not tidiness. Because the rasteriser accumulates area rather than compositing coverage,
33//! two pieces that meet edge to edge sum to exactly one across the seam, and the seam cannot be
34//! seen. Two pieces that lie over each other along the union's own boundary would sum to two there,
35//! and a pixel half covered would come out fully inked: a bright bead at every join. Cutting the
36//! pieces to meet is what buys clean edges, and it costs nothing.
37//!
38//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
39//! Anthropic Claude
40
41use crate::path::{
42 Path,
43 PathBuilder,
44 Polyline,
45 Pt,
46 TOLERANCE,
47};
48use crate::transform::Transform;
49
50use oxedyne_fe2o3_core::prelude::*;
51
52const MAX_ARC_STEPS: usize = 256; // however large the pen
53
54// The most dashes one contour may be cut into: a ceiling against a pattern so fine, on a path so
55// long, that the outline would swallow the memory of the machine.
56pub const MAX_DASHES: usize = 1 << 16;
57
58// Below this a cross product counts as zero and two directions as parallel. Both are unit vectors,
59// so this is the sine of the angle between them, and an angle this small bends nothing a pixel can
60// show.
61const EPS_TURN: f32 = 1e-5;
62
63// Below this two points count as one, and a run of pen between them as having no length and so no
64// direction to be offset along.
65const EPS_LEN: f32 = 1e-6;
66
67// The default miter limit, as SVG and PostScript both have it: a corner whose miter would reach
68// more than four line widths past it is bevelled instead.
69pub const MITER_LIMIT: f32 = 4.0;
70
71/// How a stroke finishes at the loose end of an open contour.
72#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
73pub enum Cap {
74 #[default]
75 Butt, // stops dead on the end point, so a contour with no length is left undrawn
76 Round, // a half disc past the end point, so a contour with no length comes out as a dot
77 Square, // a half square past the end point, reaching out by half the line width
78}
79
80/// How a stroke turns a corner.
81#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
82pub enum Join {
83 #[default]
84 Miter, // the outer edges carry on until they meet, or bevel where the limit is passed
85 Round, // a wedge of a disc, rounding the corner off
86 Bevel, // a straight cut across the corner, from one outer edge to the other
87}
88
89/// A dash pattern: alternating lengths of ink and gap, walked round and round along the contour.
90#[derive(Clone, Debug, Default, PartialEq)]
91pub struct Dash {
92 // A pattern of odd length is walked twice over, so that ink and gap trade places on the second
93 // pass and the pattern only truly repeats after both, which is the rule SVG and PostScript
94 // share.
95 pub pattern: Vec<f32>, // lengths of ink and gap in turn, beginning with ink
96 pub offset: f32, // how far into the pattern the contour's first point stands
97}
98
99impl Dash {
100
101 /// Creates a pattern that begins at the start of its first length of ink.
102 pub fn new(pattern: Vec<f32>) -> Self {
103 Self { pattern, offset: 0.0 }
104 }
105
106 pub fn with_offset(mut self, offset: f32) -> Self {
107 self.offset = offset;
108 self
109 }
110}
111
112/// The pen: everything that decides what ink a path leaves.
113#[derive(Clone, Debug, PartialEq)]
114pub struct Stroke {
115 pub width: f32, // in the coordinates the path is expressed in; must be positive
116 pub cap: Cap, // only an open contour has loose ends to finish
117 pub join: Join,
118 // The furthest a miter may reach past a corner, as a multiple of the line width. A corner
119 // sharper than this is bevelled instead, which is what stops a path that nearly doubles back
120 // from throwing a spike clear across the page. Must be at least one, since even a right angle
121 // mitres to more than one line width.
122 pub miter_limit: f32,
123 pub dash: Option<Dash>, // set where the line is to be broken
124 // The flattening tolerance, in the coordinates the path is expressed in: the furthest a
125 // straight segment may stray from the curve or the arc it stands in for. A caller who will
126 // then scale the stroked path up tenfold should divide this by ten, as Path::flatten does with
127 // its own tolerance, and as Pixmap::stroke_path does on the caller's behalf.
128 pub tol: f32,
129}
130
131impl Default for Stroke {
132 fn default() -> Self {
133 Self {
134 width: 1.0,
135 cap: Cap::default(),
136 join: Join::default(),
137 miter_limit: MITER_LIMIT,
138 dash: None,
139 tol: TOLERANCE,
140 }
141 }
142}
143
144impl Stroke {
145
146 /// Creates a pen, refusing a width that cannot draw.
147 pub fn new(width: f32) -> Outcome<Self> {
148 let s = Self { width, ..Self::default() };
149 res!(s.check());
150 Ok(s)
151 }
152
153 pub fn with_cap(mut self, cap: Cap) -> Self {
154 self.cap = cap;
155 self
156 }
157
158 pub fn with_join(mut self, join: Join) -> Self {
159 self.join = join;
160 self
161 }
162
163 /// The limit is a multiple of the line width.
164 pub fn with_miter_limit(mut self, limit: f32) -> Self {
165 self.miter_limit = limit;
166 self
167 }
168
169 pub fn with_dash(mut self, dash: Dash) -> Self {
170 self.dash = Some(dash);
171 self
172 }
173
174 pub fn with_tolerance(mut self, tol: f32) -> Self {
175 self.tol = tol;
176 self
177 }
178
179 /// Refuses a pen that cannot draw.
180 ///
181 /// The fields are public, so a pen can be assembled without passing through [`Stroke::new`].
182 /// This is where every pen is checked all the same, once, at the moment it is asked to draw.
183 pub fn check(&self) -> Outcome<()> {
184 if !self.width.is_finite() || self.width <= 0.0 {
185 return Err(err!(
186 "A stroke width must be positive and finite, but {} was given.", self.width;
187 Invalid, Input));
188 }
189 if !self.miter_limit.is_finite() || self.miter_limit < 1.0 {
190 return Err(err!(
191 "A miter limit must be at least one, since a miter reaches at least one line width \
192 past even a right angle, but {} was given.", self.miter_limit;
193 Invalid, Input, Range));
194 }
195 if !self.tol.is_finite() || self.tol <= 0.0 {
196 return Err(err!(
197 "A flattening tolerance must be positive and finite, but {} was given.", self.tol;
198 Invalid, Input));
199 }
200 if let Some(d) = &self.dash {
201 if d.pattern.is_empty() {
202 return Err(err!(
203 "A dash pattern must name at least one length of ink.";
204 Invalid, Input, Missing));
205 }
206 if !d.offset.is_finite() {
207 return Err(err!(
208 "A dash offset must be finite, but {} was given.", d.offset;
209 Invalid, Input));
210 }
211 let mut total = 0.0f32;
212 for (i, len) in d.pattern.iter().enumerate() {
213 if !len.is_finite() || *len < 0.0 {
214 return Err(err!(
215 "A dash length must be finite and no less than zero, but the one at {} is \
216 {}.", i, len;
217 Invalid, Input));
218 }
219 total += *len;
220 }
221 if total <= 0.0 {
222 return Err(err!(
223 "A dash pattern of {} lengths that are all zero never turns the ink on.",
224 d.pattern.len();
225 Invalid, Input));
226 }
227 }
228 Ok(())
229 }
230}
231
232impl Path {
233
234 /// The ink the pen leaves, as a new path.
235 ///
236 /// The result is a union of convex pieces all wound the same way, so it must be filled under
237 /// [`crate::raster::FillRule::NonZero`] -- which is the default, and what
238 /// [`crate::pixmap::Pixmap::fill_path`] uses. Filling it even-odd would open a hole wherever two
239 /// pieces overlap.
240 pub fn stroke(&self, pen: &Stroke) -> Outcome<Self> {
241 res!(pen.check());
242 let r = 0.5 * pen.width; // Half the width: how far the pen reaches to either side.
243 let mut pb = PathBuilder::new();
244 for pl in self.flatten_contours(&Transform::IDENTITY, pen.tol) {
245 match &pen.dash {
246 None => stroke_contour(&mut pb, &pl, pen, r),
247 Some(d) => {
248 for run in res!(dash(&pl, d)) {
249 stroke_contour(&mut pb, &run, pen, r);
250 }
251 },
252 }
253 }
254 pb.finish()
255 }
256}
257
258fn stroke_contour(pb: &mut PathBuilder, pl: &Polyline, pen: &Stroke, r: f32) {
259 let pts = dedup(&pl.pts, pl.closed);
260 if pts.is_empty() {
261 return;
262 }
263 if pts.len() == 1 {
264 // A contour with no length. It still leaves a mark, if the cap reaches anywhere.
265 point_cap(pb, pts[0], pen.cap, r, pen.tol);
266 return;
267 }
268 let n = pts.len();
269 // The runs of pen: one for each edge, and for a closed contour the edge back to the start too.
270 let edges = if pl.closed { n } else { n - 1 };
271 let mut dirs: Vec<Pt> = Vec::with_capacity(edges);
272 for i in 0..edges {
273 match dir(pts[i], pts[(i + 1) % n]) {
274 Some(d) => dirs.push(d),
275 // Unreachable after `dedup`, but no offset here may ever divide by a zero length.
276 None => return,
277 }
278 }
279
280 // Each straight run of the pen, as a quadrilateral.
281 for i in 0..edges {
282 let (a, b) = (pts[i], pts[(i + 1) % n]);
283 let nv = mul(left(dirs[i]), r);
284 piece(pb, &[add(a, nv), add(b, nv), sub(b, nv), sub(a, nv)]);
285 }
286
287 if pl.closed {
288 // A closed contour turns a corner at every point, its first included, and has no ends.
289 for i in 0..edges {
290 let prev = (i + edges - 1) % edges;
291 join(pb, pts[i], dirs[prev], dirs[i], pen, r);
292 }
293 } else {
294 for i in 1..(n - 1) {
295 join(pb, pts[i], dirs[i - 1], dirs[i], pen, r);
296 }
297 // The two loose ends. The pen leaves the first one travelling backwards.
298 end_cap(pb, pts[0], mul(dirs[0], -1.0), pen.cap, r, pen.tol);
299 end_cap(pb, pts[n - 1], dirs[edges - 1], pen.cap, r, pen.tol);
300 }
301}
302
303/// Adds the piece that fills the corner at `v`, where the pen turns from direction `d0` to `d1`.
304fn join(pb: &mut PathBuilder, v: Pt, d0: Pt, d1: Pt, pen: &Stroke, r: f32) {
305 let cross = d0.x * d1.y - d0.y * d1.x;
306 let dot = d0.x * d1.x + d0.y * d1.y;
307
308 if cross.abs() < EPS_TURN {
309 if dot > 0.0 {
310 return; // Straight on. There is no corner here to fill.
311 }
312 // A hairpin: the pen doubles back along itself, and the corner is the whole half disc past
313 // `v`. A miter here would reach to infinity and so is always over its limit, and the bevel
314 // it falls back to is a triangle with no area, so only a round join leaves anything at all.
315 if let Join::Round = pen.join {
316 piece(pb, &round_cap(v, d0, r, pen.tol));
317 }
318 return;
319 }
320
321 // The outside of the turn is the side the pen sweeps the long way round: the right hand turning
322 // one way, the left hand turning the other.
323 let outer = if cross > 0.0 { right } else { left };
324 let n0 = mul(outer(d0), r);
325 let n1 = mul(outer(d1), r);
326 // The signed angle the pen turns through, which is also the angle from `n0` to `n1`, a normal
327 // being nothing but its direction under a quarter turn.
328 //
329 // Taken at double width and brought back. See [`arc_steps`] for why: the single-width form of this
330 // function is what ties a binary to the C library it was built against.
331 let phi = (cross as f64).atan2(dot as f64) as f32;
332
333 match pen.join {
334 Join::Bevel => piece(pb, &[v, add(v, n0), add(v, n1)]),
335 Join::Round => {
336 // A wedge of the disc, not the disc: its two straight edges are the end edges of the
337 // two runs of pen it sits between, so it meets them instead of lying over them.
338 let steps = arc_steps(r, phi, pen.tol);
339 let step = phi / (steps as f32);
340 let mut pts = Vec::with_capacity(steps + 2);
341 pts.push(v);
342 for k in 0..=steps {
343 pts.push(add(v, rot(n0, (k as f32) * step)));
344 }
345 piece(pb, &pts);
346 },
347 Join::Miter => {
348 // The miter reaches 1 / cos(phi / 2) line widths past the corner, which runs away to
349 // nothing as the corner sharpens towards a hairpin. The limit is what keeps a needle
350 // from becoming a spear.
351 let c = (0.5 * phi).cos();
352 let reach = if c > f32::EPSILON { 1.0 / c } else { f32::INFINITY };
353 let bisect = add(n0, n1); // Two normals of a length bisect the angle between them.
354 let len = (bisect.x * bisect.x + bisect.y * bisect.y).sqrt();
355 if !reach.is_finite() || reach > pen.miter_limit || len <= EPS_LEN {
356 piece(pb, &[v, add(v, n0), add(v, n1)]); // Over the limit: bevel it instead.
357 } else {
358 let m = add(v, mul(bisect, r * reach / len));
359 piece(pb, &[v, add(v, n0), m, add(v, n1)]);
360 }
361 },
362 }
363}
364
365/// Adds the piece that finishes a loose end at `e`, which the pen reached travelling in `d`.
366fn end_cap(pb: &mut PathBuilder, e: Pt, d: Pt, cap: Cap, r: f32, tol: f32) {
367 match cap {
368 Cap::Butt => (),
369 Cap::Round => piece(pb, &round_cap(e, d, r, tol)),
370 Cap::Square => piece(pb, &square_cap(e, d, r)),
371 }
372}
373
374/// Adds the mark a contour with no length leaves: a dot under a round cap, a square under a square
375/// cap, and nothing at all under a butt cap, which reaches nowhere.
376fn point_cap(pb: &mut PathBuilder, e: Pt, cap: Cap, r: f32, tol: f32) {
377 match cap {
378 Cap::Butt => (),
379 Cap::Round => {
380 // A whole disc, since there is no direction here to take half of.
381 let steps = arc_steps(r, std::f32::consts::TAU, tol).max(3);
382 let step = std::f32::consts::TAU / (steps as f32);
383 let pts: Vec<Pt> = (0..steps)
384 .map(|k| add(e, rot(Pt::new(r, 0.0), (k as f32) * step)))
385 .collect();
386 piece(pb, &pts);
387 },
388 Cap::Square => piece(pb, &[
389 Pt::new(e.x - r, e.y - r),
390 Pt::new(e.x + r, e.y - r),
391 Pt::new(e.x + r, e.y + r),
392 Pt::new(e.x - r, e.y + r),
393 ]),
394 }
395}
396
397/// The points of a round cap: a half disc past `e`, bulging the way `d` points.
398///
399/// The straight edge of the half disc runs from one side of the line to the other, which is exactly
400/// the end edge of the run of pen reaching `e`, so cap and run meet rather than overlap.
401fn round_cap(e: Pt, d: Pt, r: f32, tol: f32) -> Vec<Pt> {
402 let n = mul(left(d), r);
403 // The left normal leads the direction by a quarter turn, so sweeping back by half a turn from
404 // it passes through the direction itself, which is the way the cap must bulge. Sweeping forward
405 // would put the cap behind the pen, inside the ink, where it would do nothing.
406 let steps = arc_steps(r, std::f32::consts::PI, tol);
407 let step = -std::f32::consts::PI / (steps as f32);
408 (0..=steps).map(|k| add(e, rot(n, (k as f32) * step))).collect()
409}
410
411/// The points of a square cap: a half square past `e`, reaching out by `r` the way `d` points.
412fn square_cap(e: Pt, d: Pt, r: f32) -> Vec<Pt> {
413 let n = mul(left(d), r);
414 let out = mul(d, r);
415 let (a, b) = (add(e, n), sub(e, n));
416 vec![a, add(a, out), add(b, out), b]
417}
418
419/// Adds one convex piece of the outline, wound the same way as every other piece.
420///
421/// The winding is settled here, by the sign of the shoelace area, and nowhere else. The union only
422/// holds if nothing cancels: two pieces wound against each other would subtract where they overlap
423/// and eat a hole out of the middle of a perfectly good stroke.
424fn piece(pb: &mut PathBuilder, pts: &[Pt]) {
425 if pts.len() < 3 {
426 return; // Nothing with an interior.
427 }
428 let mut area = 0.0f32;
429 for i in 0..pts.len() {
430 let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
431 area += a.x * b.y - b.x * a.y;
432 }
433 if area >= 0.0 {
434 pb.move_to(pts[0]);
435 for p in &pts[1..] {
436 pb.line_to(*p);
437 }
438 } else {
439 pb.move_to(pts[pts.len() - 1]);
440 for p in pts[..pts.len() - 1].iter().rev() {
441 pb.line_to(*p);
442 }
443 }
444 pb.close();
445}
446
447/// Cuts a contour into the runs of ink a dash pattern leaves along it.
448fn dash(pl: &Polyline, d: &Dash) -> Outcome<Vec<Polyline>> {
449 // An odd pattern is walked twice, so that ink and gap trade places on the second pass.
450 let mut pat = d.pattern.clone();
451 if pat.len() % 2 == 1 {
452 pat.extend_from_within(..);
453 }
454 let total: f32 = pat.iter().sum();
455 let pts = &pl.pts;
456 if pts.len() < 2 || total <= 0.0 {
457 return Ok(vec![pl.clone()]);
458 }
459
460 // Where in the pattern the contour's first point already stands.
461 let mut phase = d.offset % total;
462 if phase < 0.0 {
463 phase += total;
464 }
465 let mut i = 0usize;
466 // The phase is less than the total, so it is spent before the pattern runs out.
467 for _ in 0..pat.len() {
468 if phase < pat[i] {
469 break;
470 }
471 phase -= pat[i];
472 i = (i + 1) % pat.len();
473 }
474 let mut on = i % 2 == 0; // Even lengths are ink, odd ones gap.
475 let mut rest = pat[i] - phase; // How much of the current length has yet to run.
476
477 let began_on = on;
478 let n = pts.len();
479 let edges = if pl.closed { n } else { n - 1 };
480 let mut out: Vec<Polyline> = Vec::new();
481 let mut cur: Vec<Pt> = if on { vec![pts[0]] } else { Vec::new() };
482
483 for e in 0..edges {
484 let (a, b) = (pts[e], pts[(e + 1) % n]);
485 let len = a.distance(b);
486 if !len.is_finite() || len <= EPS_LEN {
487 continue; // Nothing to walk along, and nothing to divide by.
488 }
489 let mut t = 0.0f32; // How far along this edge the walk has come.
490 while rest < len - t {
491 t += rest;
492 let p = lerp(a, b, t / len);
493 if on {
494 cur.push(p);
495 out.push(Polyline { pts: std::mem::take(&mut cur), closed: false });
496 if out.len() > MAX_DASHES {
497 return Err(err!(
498 "A dash pattern of total length {} cuts this contour into more than {} \
499 runs of ink.", total, MAX_DASHES;
500 Invalid, Input, Excessive));
501 }
502 } else {
503 cur.clear();
504 cur.push(p);
505 }
506 on = !on;
507 i = (i + 1) % pat.len();
508 rest = pat[i];
509 }
510 rest -= len - t;
511 if on {
512 cur.push(b);
513 }
514 }
515
516 // Whatever the walk was still laying down when it ran out of contour.
517 if on && !cur.is_empty() {
518 if out.is_empty() {
519 // The pattern never turned off, so the contour survives whole, and closed if it began
520 // so: a dash long enough to swallow a ring leaves a ring, not a ring cut open.
521 out.push(Polyline { pts: cur, closed: pl.closed });
522 } else if pl.closed && began_on {
523 // The walk began and ended inside the same length of ink, on either side of the
524 // contour's first point. They are one run, and must be sewn back into one, or the
525 // corner there would come out capped twice over instead of joined.
526 let head = std::mem::take(&mut out[0].pts);
527 let mut sewn = cur;
528 sewn.extend_from_slice(&head[1..]);
529 out[0].pts = sewn;
530 } else {
531 out.push(Polyline { pts: cur, closed: false });
532 }
533 }
534 Ok(out)
535}
536
537/// Drops each point that repeats the one before it, and the closing point of a closed contour that
538/// names its first point twice.
539///
540/// A run of pen with no length has no direction, and a direction is what every offset, every join
541/// and every cap here is built from.
542fn dedup(pts: &[Pt], closed: bool) -> Vec<Pt> {
543 let mut out: Vec<Pt> = Vec::with_capacity(pts.len());
544 for p in pts {
545 match out.last() {
546 Some(q) if q.distance(*p) < EPS_LEN => (),
547 _ => out.push(*p),
548 }
549 }
550 if closed && out.len() > 1 {
551 let (first, last) = (out[0], out[out.len() - 1]);
552 if first.distance(last) < EPS_LEN {
553 out.pop();
554 }
555 }
556 out
557}
558
559/// How many straight segments an arc needs to stay within the tolerance.
560///
561/// A chord subtending an angle `a` on a circle of radius `r` bulges away from it by `r(1 -
562/// cos(a/2))`, so holding that below the tolerance fixes the angle, and the angle fixes the count.
563fn arc_steps(r: f32, sweep: f32, tol: f32) -> usize {
564 let sweep = sweep.abs();
565 if !sweep.is_finite() || sweep <= 0.0 {
566 return 1;
567 }
568 let cos = (1.0 - tol / r).clamp(-1.0, 1.0);
569 // The widest angle one chord may span. Taken at DOUBLE width and brought back, rather than in single
570 // width directly.
571 //
572 // This is a portability matter, not a numerical one. The single-width transcendentals -- `acosf`,
573 // `atan2f` and their kin -- gained fresh symbol versions in glibc 2.43, so a binary built against it
574 // asks for `acosf@GLIBC_2.43` and will not load on a machine whose C library is older, however little
575 // of it the program actually uses. The double-width forms have carried the same version since
576 // glibc 2.2.5 and are on every machine that will ever run this. A binary built on the newest
577 // distribution therefore still runs on the ones beside it -- which for a library whose whole point is
578 // to be self-contained is the behaviour to want. The double-width answer is also the more accurate of
579 // the two; nothing is given up.
580 let a = 2.0 * (cos as f64).acos() as f32;
581 if !a.is_finite() || a <= 0.0 {
582 return MAX_ARC_STEPS;
583 }
584 ((sweep / a).ceil().max(1.0) as usize).min(MAX_ARC_STEPS)
585}
586
587/// The unit direction from `a` to `b`, or `None` where there is none because they are one point.
588fn dir(a: Pt, b: Pt) -> Option<Pt> {
589 let d = sub(b, a);
590 let len = (d.x * d.x + d.y * d.y).sqrt();
591 if !len.is_finite() || len < EPS_LEN {
592 return None;
593 }
594 Some(mul(d, 1.0 / len))
595}
596
597fn add(a: Pt, b: Pt) -> Pt {
598 Pt::new(a.x + b.x, a.y + b.y)
599}
600
601fn sub(a: Pt, b: Pt) -> Pt {
602 Pt::new(a.x - b.x, a.y - b.y)
603}
604
605fn mul(a: Pt, s: f32) -> Pt {
606 Pt::new(a.x * s, a.y * s)
607}
608
609fn lerp(a: Pt, b: Pt, s: f32) -> Pt {
610 Pt::new(a.x + (b.x - a.x) * s, a.y + (b.y - a.y) * s)
611}
612
613/// The left normal of a direction: the direction under a quarter turn.
614fn left(d: Pt) -> Pt {
615 Pt::new(-d.y, d.x)
616}
617
618/// The right normal of a direction: the direction under a quarter turn the other way.
619fn right(d: Pt) -> Pt {
620 Pt::new(d.y, -d.x)
621}
622
623fn rot(v: Pt, a: f32) -> Pt {
624 let (s, c) = a.sin_cos();
625 Pt::new(v.x * c - v.y * s, v.x * s + v.y * c)
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 use crate::{
633 colour::Rgba,
634 pixmap::Pixmap,
635 };
636
637 /// Renders a stroke, black on white, so that a test can read the ink back pixel by pixel.
638 fn ink(path: &Path, pen: &Stroke, w: usize, h: usize) -> Outcome<Pixmap> {
639 let mut pm = res!(Pixmap::filled(w, h, Rgba::WHITE));
640 res!(pm.stroke_path(path, &Transform::IDENTITY, Rgba::BLACK, None, pen));
641 Ok(pm)
642 }
643
644 /// How dark a pixel came out, from 0 for untouched to 1 for solid.
645 fn dark(pm: &Pixmap, x: usize, y: usize) -> f32 {
646 match pm.pixel(x, y) {
647 Some(c) => 1.0 - (c.r as f32) / 255.0,
648 None => 0.0,
649 }
650 }
651
652 /// The topmost row holding any real ink, which is how far a corner reaches.
653 fn top_row(pm: &Pixmap) -> Option<usize> {
654 for y in 0..pm.height() {
655 for x in 0..pm.width() {
656 if dark(pm, x, y) > 0.5 {
657 return Some(y);
658 }
659 }
660 }
661 None
662 }
663
664 fn line(a: Pt, b: Pt) -> Outcome<Path> {
665 let mut pb = PathBuilder::new();
666 pb.move_to(a);
667 pb.line_to(b);
668 pb.finish()
669 }
670
671 fn square(x0: f32, y0: f32, x1: f32, y1: f32, closed: bool) -> Outcome<Path> {
672 let mut pb = PathBuilder::new();
673 pb.move_to(Pt::new(x0, y0));
674 pb.line_to(Pt::new(x1, y0));
675 pb.line_to(Pt::new(x1, y1));
676 pb.line_to(Pt::new(x0, y1));
677 if closed {
678 pb.close();
679 }
680 pb.finish()
681 }
682
683 /// A narrow V, whose apex is sharp enough to mitre past the default limit.
684 ///
685 /// The arms meet at about 28 degrees, so the miter reaches about 4.1 line widths past the apex:
686 /// over the default limit of 4, and under a limit of 6.
687 fn vee() -> Outcome<Path> {
688 let mut pb = PathBuilder::new();
689 pb.move_to(Pt::new(14.0, 38.0));
690 pb.line_to(Pt::new(20.0, 14.0));
691 pb.line_to(Pt::new(26.0, 38.0));
692 pb.finish()
693 }
694
695 #[test]
696 fn test_a_width_that_cannot_draw_is_refused_00() -> Outcome<()> {
697 let path = res!(line(Pt::new(2.0, 8.0), Pt::new(14.0, 8.0)));
698 assert!(Stroke::new(0.0).is_err(), "a zero width");
699 assert!(Stroke::new(-3.0).is_err(), "a negative width");
700 assert!(Stroke::new(f32::NAN).is_err(), "a width that is not a number");
701 assert!(Stroke::new(f32::INFINITY).is_err(), "an infinite width");
702 // The fields are public, so the check must also bite at the moment of drawing.
703 let bad = Stroke { width: -1.0, ..Stroke::default() };
704 assert!(path.stroke(&bad).is_err(), "a negative width set after construction");
705 Ok(())
706 }
707
708 #[test]
709 fn test_a_miter_limit_below_one_is_refused_01() -> Outcome<()> {
710 let path = res!(vee());
711 let pen = res!(Stroke::new(4.0)).with_miter_limit(0.5);
712 assert!(pen.check().is_err(), "a limit no miter could ever meet");
713 assert!(path.stroke(&pen).is_err());
714 Ok(())
715 }
716
717 #[test]
718 fn test_a_line_strokes_to_a_band_02() -> Outcome<()> {
719 // A pen four wide, run from (2, 8) to (14, 8), inks the band x in [2, 14], y in [6, 10].
720 let path = res!(line(Pt::new(2.0, 8.0), Pt::new(14.0, 8.0)));
721 let pen = res!(Stroke::new(4.0));
722 let pm = res!(ink(&path, &pen, 16, 16));
723 assert!(dark(&pm, 8, 6) > 0.99, "the top row of the band, found {}", dark(&pm, 8, 6));
724 assert!(dark(&pm, 8, 9) > 0.99, "the bottom row of the band");
725 assert!(dark(&pm, 8, 5) < 0.01, "above the band, found {}", dark(&pm, 8, 5));
726 assert!(dark(&pm, 8, 10) < 0.01, "below the band, found {}", dark(&pm, 8, 10));
727 assert!(dark(&pm, 2, 8) > 0.99, "the first column of the band");
728 assert!(dark(&pm, 13, 8) > 0.99, "the last column of the band");
729 Ok(())
730 }
731
732 #[test]
733 fn test_a_butt_cap_reaches_nowhere_but_the_others_reach_out_03() -> Outcome<()> {
734 let path = res!(line(Pt::new(2.0, 8.0), Pt::new(14.0, 8.0)));
735 let base = res!(Stroke::new(4.0));
736
737 let butt = res!(ink(&path, &base.clone().with_cap(Cap::Butt), 16, 16));
738 assert!(dark(&butt, 1, 8) < 0.01, "a butt cap stops dead, found {}", dark(&butt, 1, 8));
739
740 let square = res!(ink(&path, &base.clone().with_cap(Cap::Square), 16, 16));
741 assert!(dark(&square, 1, 8) > 0.99, "a square cap reaches out by half the width");
742 assert!(dark(&square, 0, 6) > 0.99, "and squarely, right into its corner");
743
744 let round = res!(ink(&path, &base.with_cap(Cap::Round), 16, 16));
745 assert!(dark(&round, 1, 8) > 0.99, "a round cap reaches out too");
746 assert!(
747 dark(&round, 0, 6) < 0.5,
748 "but roundly, so it does not fill the corner, found {}", dark(&round, 0, 6),
749 );
750 Ok(())
751 }
752
753 #[test]
754 fn test_a_point_is_a_dot_under_a_round_cap_and_nothing_under_a_butt_04() -> Outcome<()> {
755 // A contour with no length: the pen is set down and lifted in the same place.
756 let mut pb = PathBuilder::new();
757 pb.move_to(Pt::new(8.0, 8.0));
758 pb.line_to(Pt::new(8.0, 8.0));
759 let path = res!(pb.finish());
760 let base = res!(Stroke::new(4.0));
761
762 let butt = res!(path.stroke(&base.clone().with_cap(Cap::Butt)));
763 assert!(butt.is_empty(), "a butt cap on a point reaches nowhere, so there is no ink");
764
765 let round = res!(ink(&path, &base.clone().with_cap(Cap::Round), 16, 16));
766 assert!(dark(&round, 8, 8) > 0.99, "a round cap on a point is a dot");
767 assert!(dark(&round, 8, 4) < 0.01, "and no larger than the pen");
768 assert!(
769 dark(&round, 6, 6) < 0.5,
770 "and round, so its corner is bitten off, found {}", dark(&round, 6, 6),
771 );
772
773 let sq = res!(ink(&path, &base.with_cap(Cap::Square), 16, 16));
774 assert!(dark(&sq, 8, 8) > 0.99, "a square cap on a point is a square");
775 assert!(dark(&sq, 6, 6) > 0.99, "with its corner still on, found {}", dark(&sq, 6, 6));
776 Ok(())
777 }
778
779 #[test]
780 fn test_a_closed_contour_is_joined_all_the_way_round_05() -> Outcome<()> {
781 // The corner at the contour's first point is the one that tells the tale. Closed, the pen
782 // turns it and the miter fills the outer corner. Open, the pen starts and stops there, and
783 // two butt caps leave the outer corner bare.
784 let pen = res!(Stroke::new(2.0));
785 let shut = res!(ink(&res!(square(4.0, 4.0, 12.0, 12.0, true)), &pen, 16, 16));
786 let open = res!(ink(&res!(square(4.0, 4.0, 12.0, 12.0, false)), &pen, 16, 16));
787 assert!(
788 dark(&shut, 3, 3) > 0.99,
789 "a closed contour joins its first corner, found {}", dark(&shut, 3, 3),
790 );
791 assert!(
792 dark(&open, 3, 3) < 0.01,
793 "an open one caps it instead, found {}", dark(&open, 3, 3),
794 );
795 // The other three corners are turned either way, so they must agree.
796 assert!(dark(&shut, 12, 3) > 0.99, "the far corner, closed");
797 assert!(dark(&open, 12, 3) > 0.99, "the far corner, open");
798 Ok(())
799 }
800
801 #[test]
802 fn test_a_miter_over_the_limit_falls_back_to_a_bevel_06() -> Outcome<()> {
803 let path = res!(vee());
804 let base = res!(Stroke::new(4.0));
805
806 // Under a limit of six the apex mitres, throwing the ink well above the apex at y = 14.
807 let long = res!(ink(&path, &base.clone().with_miter_limit(6.0), 40, 40));
808 let far = match top_row(&long) {
809 Some(y) => y,
810 None => return Err(err!("The stroke of a V must leave some ink."; Bug)),
811 };
812 assert!(far < 9, "a miter should reach far past the apex, but stopped at row {}", far);
813
814 // Under the default limit of four the same apex is over the limit, and is bevelled: the ink
815 // stops within half a line width of the apex.
816 let cut = res!(ink(&path, &base.clone(), 40, 40));
817 let near = match top_row(&cut) {
818 Some(y) => y,
819 None => return Err(err!("The stroke of a V must leave some ink."; Bug)),
820 };
821 assert!(
822 near >= 12,
823 "a miter over its limit should be bevelled back, but reached row {}", near,
824 );
825
826 // A bevel asked for outright must land in the same place as the miter that fell back to one.
827 let bevel = res!(ink(&path, &base.with_join(Join::Bevel), 40, 40));
828 assert_eq!(cut, bevel, "a miter over its limit must be exactly a bevel");
829 Ok(())
830 }
831
832 #[test]
833 fn test_a_round_join_stays_within_half_a_width_of_the_corner_07() -> Outcome<()> {
834 // A round join is a wedge of a disc of half the line width, so however sharp the corner, it
835 // can never reach further than that. The apex is at y = 14 and the pen is 4 wide.
836 let path = res!(vee());
837 let pen = res!(Stroke::new(4.0)).with_join(Join::Round);
838 let pm = res!(ink(&path, &pen, 40, 40));
839 let top = match top_row(&pm) {
840 Some(y) => y,
841 None => return Err(err!("The stroke of a V must leave some ink."; Bug)),
842 };
843 assert!(top >= 11, "a round join cannot reach past row 12, but reached row {}", top);
844 assert!(top <= 13, "nor should it fall short of the corner, found row {}", top);
845 Ok(())
846 }
847
848 #[test]
849 fn test_a_hairpin_is_rounded_off_but_not_mitred_08() -> Outcome<()> {
850 // The pen runs out to (20, 8) and doubles straight back. A round join must put a half disc
851 // past the turn, bulging the way the pen was going, not the way it came. A miter there
852 // would reach to infinity, so it must fall back to a bevel, which at a hairpin is nothing.
853 let mut pb = PathBuilder::new();
854 pb.move_to(Pt::new(4.0, 8.0));
855 pb.line_to(Pt::new(20.0, 8.0));
856 pb.line_to(Pt::new(4.0, 8.0));
857 let path = res!(pb.finish());
858 let base = res!(Stroke::new(4.0));
859
860 // The pen is 4 wide, so the half disc has a radius of 2. The pixel at (20, 8) lies wholly
861 // inside it, and the one at (21, 8) hangs over its rim.
862 let round = res!(ink(&path, &base.clone().with_join(Join::Round), 24, 16));
863 assert!(
864 dark(&round, 20, 8) > 0.99,
865 "a round join must bulge past the turn, found {}", dark(&round, 20, 8),
866 );
867 assert!(
868 dark(&round, 21, 8) > 0.8,
869 "and reach nearly a full radius past it, found {}", dark(&round, 21, 8),
870 );
871
872 let mitre = res!(ink(&path, &base.with_join(Join::Miter), 24, 16));
873 assert!(
874 dark(&mitre, 20, 8) < 0.01,
875 "a miter at a hairpin must fall back to a bevel, and so to nothing, found {}",
876 dark(&mitre, 20, 8),
877 );
878 Ok(())
879 }
880
881 #[test]
882 fn test_the_seams_between_the_pieces_do_not_show_09() -> Outcome<()> {
883 // The outline is a heap of pieces, and the joins between them run right through the ink. If
884 // the pieces were composited the seams would show as light or dark lines; because the
885 // rasteriser adds areas, they cannot. Every pixel well inside the band must be solid.
886 let mut pb = PathBuilder::new();
887 pb.move_to(Pt::new(4.0, 8.0));
888 pb.line_to(Pt::new(20.0, 8.0));
889 pb.line_to(Pt::new(20.0, 24.0));
890 let path = res!(pb.finish());
891 let pen = res!(Stroke::new(6.0)).with_join(Join::Round);
892 let pm = res!(ink(&path, &pen, 32, 32));
893 for x in 5..19 {
894 assert!(dark(&pm, x, 8) > 0.99, "a seam shows at ({}, 8): {}", x, dark(&pm, x, 8));
895 }
896 for y in 10..22 {
897 assert!(dark(&pm, 20, y) > 0.99, "a seam shows at (20, {}): {}", y, dark(&pm, 20, y));
898 }
899 // And the corner itself, which is where three pieces meet.
900 assert!(dark(&pm, 19, 9) > 0.99, "the corner is not solid: {}", dark(&pm, 19, 9));
901 Ok(())
902 }
903
904 #[test]
905 fn test_a_curve_is_stroked_10() -> Outcome<()> {
906 // The pen follows the flattened curve, so the ink must be a band about it and nothing more.
907 let mut pb = PathBuilder::new();
908 pb.move_to(Pt::new(4.0, 28.0));
909 pb.quad_to(Pt::new(16.0, 0.0), Pt::new(28.0, 28.0));
910 let path = res!(pb.finish());
911 let pen = res!(Stroke::new(3.0)).with_cap(Cap::Round);
912 let pm = res!(ink(&path, &pen, 32, 32));
913 assert!(dark(&pm, 16, 14) > 0.9, "the crown of the arc, found {}", dark(&pm, 16, 14));
914 assert!(dark(&pm, 16, 26) < 0.01, "under the arc, found {}", dark(&pm, 16, 26));
915 assert!(dark(&pm, 16, 8) < 0.01, "over the arc, found {}", dark(&pm, 16, 8));
916 Ok(())
917 }
918
919 #[test]
920 fn test_a_dash_breaks_the_line_11() -> Outcome<()> {
921 // Four on, four off, from x = 0.
922 let path = res!(line(Pt::new(0.0, 8.0), Pt::new(32.0, 8.0)));
923 let pen = res!(Stroke::new(4.0)).with_dash(Dash::new(vec![4.0, 4.0]));
924 let pm = res!(ink(&path, &pen, 32, 16));
925 assert!(dark(&pm, 2, 8) > 0.99, "the first dash");
926 assert!(dark(&pm, 6, 8) < 0.01, "the first gap, found {}", dark(&pm, 6, 8));
927 assert!(dark(&pm, 10, 8) > 0.99, "the second dash");
928 assert!(dark(&pm, 14, 8) < 0.01, "the second gap");
929 Ok(())
930 }
931
932 #[test]
933 fn test_an_odd_dash_pattern_is_walked_twice_12() -> Outcome<()> {
934 // A pattern of one length means four on, four off, as if it had been written out in full.
935 let path = res!(line(Pt::new(0.0, 8.0), Pt::new(32.0, 8.0)));
936 let pen = res!(Stroke::new(4.0));
937 let odd = res!(ink(&path, &pen.clone().with_dash(Dash::new(vec![4.0])), 32, 16));
938 let even = res!(ink(&path, &pen.with_dash(Dash::new(vec![4.0, 4.0])), 32, 16));
939 assert_eq!(odd, even, "an odd pattern must be walked twice over");
940 Ok(())
941 }
942
943 #[test]
944 fn test_a_dash_offset_shifts_the_pattern_13() -> Outcome<()> {
945 let path = res!(line(Pt::new(0.0, 8.0), Pt::new(32.0, 8.0)));
946 let dash = Dash::new(vec![4.0, 4.0]).with_offset(4.0);
947 let pen = res!(Stroke::new(4.0)).with_dash(dash);
948 let pm = res!(ink(&path, &pen, 32, 16));
949 assert!(dark(&pm, 2, 8) < 0.01, "the pattern begins in a gap, found {}", dark(&pm, 2, 8));
950 assert!(dark(&pm, 6, 8) > 0.99, "and the first dash follows it");
951 Ok(())
952 }
953
954 #[test]
955 fn test_a_dash_that_spans_a_closed_contour_leaves_it_closed_14() -> Outcome<()> {
956 // The ring is 32 round and the ink runs for 1000, so the pattern never turns off. The ring
957 // must come back whole, joined at its first corner and not cut open and capped there.
958 let path = res!(square(4.0, 4.0, 12.0, 12.0, true));
959 let pen = res!(Stroke::new(2.0)).with_dash(Dash::new(vec![1000.0]));
960 let pm = res!(ink(&path, &pen, 16, 16));
961 assert!(
962 dark(&pm, 3, 3) > 0.99,
963 "the first corner must still be joined, found {}", dark(&pm, 3, 3),
964 );
965 Ok(())
966 }
967
968 #[test]
969 fn test_a_dash_wrapping_a_closed_contour_is_sewn_back_together_15() -> Outcome<()> {
970 // The ring is 32 round. Ten on, five off, walked from its first corner, ends with the ink
971 // still on as the walk comes back round to where it began. That last run and the first are
972 // one run, on either side of the corner, and must be sewn into one: sewn, the corner is
973 // joined; unsewn, it comes out as two butt caps with a notch between them.
974 let path = res!(square(4.0, 4.0, 12.0, 12.0, true));
975 let pen = res!(Stroke::new(2.0)).with_dash(Dash::new(vec![10.0, 5.0]));
976 let pm = res!(ink(&path, &pen, 16, 16));
977 assert!(
978 dark(&pm, 3, 3) > 0.99,
979 "the wrapping dash must be sewn back into one, found {}", dark(&pm, 3, 3),
980 );
981 Ok(())
982 }
983
984 #[test]
985 fn test_a_dash_pattern_that_never_inks_is_refused_16() -> Outcome<()> {
986 let path = res!(line(Pt::new(0.0, 8.0), Pt::new(32.0, 8.0)));
987 let base = res!(Stroke::new(4.0));
988 let empty = base.clone().with_dash(Dash::new(vec![]));
989 assert!(path.stroke(&empty).is_err(), "a pattern of no lengths");
990 let zeros = base.clone().with_dash(Dash::new(vec![0.0, 0.0]));
991 assert!(path.stroke(&zeros).is_err(), "a pattern that is all zeroes");
992 let neg = base.clone().with_dash(Dash::new(vec![4.0, -1.0]));
993 assert!(path.stroke(&neg).is_err(), "a pattern with a negative length");
994 let nan = base.with_dash(Dash::new(vec![4.0, 4.0]).with_offset(f32::NAN));
995 assert!(path.stroke(&nan).is_err(), "an offset that is not a number");
996 Ok(())
997 }
998
999 #[test]
1000 fn test_an_empty_path_strokes_to_nothing_17() -> Outcome<()> {
1001 let pen = res!(Stroke::new(4.0)).with_cap(Cap::Round);
1002 let empty = res!(PathBuilder::new().finish());
1003 assert!(res!(empty.stroke(&pen)).is_empty());
1004 // A move with nothing after it goes nowhere and leaves nothing, where a move closed on
1005 // itself is a path asking for a dot.
1006 let mut pb = PathBuilder::new();
1007 pb.move_to(Pt::new(8.0, 8.0));
1008 let lone = res!(pb.finish());
1009 assert!(res!(lone.stroke(&pen)).is_empty(), "a lone move leaves nothing");
1010 Ok(())
1011 }
1012
1013 #[test]
1014 fn test_the_outline_is_filled_non_zero_not_even_odd_18() -> Outcome<()> {
1015 // The pieces overlap, so an even-odd fill of the outline would eat holes out of it. This
1016 // pins the contract that [`Path::stroke`] documents.
1017 use crate::raster::FillRule;
1018 let mut pb = PathBuilder::new();
1019 pb.move_to(Pt::new(4.0, 8.0));
1020 pb.line_to(Pt::new(20.0, 8.0));
1021 pb.line_to(Pt::new(20.0, 24.0));
1022 let path = res!(pb.finish());
1023 let pen = res!(Stroke::new(6.0)).with_join(Join::Round);
1024 let outline = res!(path.stroke(&pen));
1025
1026 let mut nz = res!(Pixmap::filled(32, 32, Rgba::WHITE));
1027 res!(nz.fill_path(&outline, &Transform::IDENTITY, Rgba::BLACK, None));
1028 let mut eo = res!(Pixmap::filled(32, 32, Rgba::WHITE));
1029 res!(eo.fill_path_with(
1030 &outline, &Transform::IDENTITY, Rgba::BLACK, None, FillRule::EvenOdd,
1031 ));
1032 assert!(dark(&nz, 19, 9) > 0.99, "the corner is solid under the non-zero rule");
1033 assert!(
1034 dark(&eo, 19, 9) < 0.5,
1035 "and eaten away under the even-odd rule, found {}", dark(&eo, 19, 9),
1036 );
1037 Ok(())
1038 }
1039}