Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/src/svg.rs

32.6 KiB, 56 runs

created by r1870400018:14443, 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//! SVG path data: the `d` attribute, read into a [`Path`].
2//!
3//! A vector mark -- an icon, a logo -- is drawn in a drawing program and leaves it as SVG, where all
4//! of the geometry sits in the `d` attribute of a `<path>` element: a terse string of one-letter
5//! commands and numbers. This module reads that string with [`path_data`] and writes it back with
6//! [`write_path_data`], and nothing else. No XML, no styling, no document: the caller keeps whatever
7//! it wants of the file and hands the geometry here.
8//!
9//! That boundary is the point. Path data is a small, closed, fully specified grammar, and it is the
10//! part every drawing program agrees on. Everything above it -- elements, attributes, gradients,
11//! filters, referenced content -- is a document format, and a caller that wants an icon does not want
12//! a document.
13//!
14//! The one concession above bare geometry is [`presentation`], which renders the paint a
15//! [`crate::stroke::Stroke`] and an [`Rgba`] already model -- fill, stroke, width, caps, joins,
16//! dashes -- as the attribute string a `<path>` carries alongside its `d`. It writes the attributes
17//! and no element around them, for the same reason the reader stops at the `d`: the element tree is
18//! the caller's format.
19//!
20//! Every command in the grammar is read, including elliptical arcs. An arc has no [`crate::path::Seg`]
21//! of its own, so it is converted to cubic béziers on the way in and no caller has to know it was
22//! ever an arc.
23//!
24//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
25//! Anthropic Claude
26
27use crate::colour::Rgba;
28use crate::path::{
29 Path,
30 PathBuilder,
31 Pt,
32 Seg,
33};
34use crate::stroke::{
35 Cap,
36 Join,
37 Stroke,
38};
39
40use oxedyne_fe2o3_core::prelude::*;
41
42use std::f64::consts::PI;
43
44// The most cubic segments one elliptical arc becomes. An arc is cut at quadrant boundaries and its
45// sweep cannot exceed a full turn, so four pieces always suffice.
46const ARC_SEGS: usize = 4;
47
48/// The ellipse an arc travels on, and which of the four arcs between the endpoints to take.
49///
50/// These are the five arguments the `A` command carries before its endpoint. They travel together
51/// because they mean nothing apart: a radius without its flags does not pick out an arc.
52#[derive(Clone, Copy)]
53struct Arc {
54 rx: f32, // horizontal radius; the sign is ignored, and one too small to span is grown
55 ry: f32, // vertical radius
56 rot: f32, // the ellipse's x-axis rotation, in degrees
57 large: bool, // take the sweep greater than a half turn
58 sweep: bool, // take the sweep in the direction of increasing angle
59}
60
61/// The last curve's trailing control point, which `S` and `T` reflect.
62///
63/// The kind matters: `S` reflects only a cubic's control point and `T` only a quadratic's. After any
64/// other command, or a curve of the other kind, the reflected point is the current point instead --
65/// so a bare [`Self::None`] is not enough and the kind must be carried.
66#[derive(Clone, Copy)]
67enum Last {
68 None, // not a curve, or a curve of the other kind
69 Cubic(Pt), // after C, c, S or s, carrying its second control point
70 Quad(Pt), // after Q, q, T or t, carrying its control point
71}
72
73/// Reads SVG path data -- the `d` attribute of a `<path>` element -- into a [`Path`].
74pub fn path_data(d: &str) -> Outcome<Path> {
75 let mut sc = Scan::new(d);
76 let mut pb = PathBuilder::new();
77 let mut cur = Pt::new(0.0, 0.0); // Where the pen is.
78 let mut start = Pt::new(0.0, 0.0); // Where this contour began, which `Z` returns to.
79 let mut prev = 0u8; // The last command, for the implicit-repeat rule.
80 let mut last = Last::None;
81 let mut open = false; // Whether a contour is under way.
82 loop {
83 if sc.done() {
84 break;
85 }
86 // A command letter may be left out to repeat the last one. A repeated `moveto` is a
87 // `lineto`, which is the grammar's one irregularity.
88 let cmd = match sc.cmd() {
89 Some(c) => c,
90 None => match prev {
91 0 => return Err(err!(
92 "Path data must begin with a command letter, found '{}'.",
93 sc.rest(); Invalid, Input)),
94 b'M' => b'L',
95 b'm' => b'l',
96 c => c,
97 },
98 };
99 // The grammar opens with a moveto, and nothing else will do: until one has been read there is
100 // no pen for a drawing command to draw from.
101 if prev == 0 && !matches!(cmd, b'M' | b'm') {
102 return Err(err!(
103 "Path data must begin with a moveto, found '{}'.", cmd as char; Invalid, Input));
104 }
105 prev = cmd;
106 // A `Z` leaves the pen on the contour's first point but closes the contour. The
107 // specification has the next subpath begin at that same point, so a drawing command
108 // following a close opens one there rather than drawing from nowhere.
109 if !open && !matches!(cmd, b'M' | b'm' | b'Z' | b'z') {
110 pb.move_to(cur);
111 open = true;
112 }
113 let rel = cmd.is_ascii_lowercase();
114 match cmd {
115 b'M' | b'm' => {
116 let p = res!(sc.point(rel, cur));
117 pb.move_to(p);
118 cur = p;
119 start = p;
120 last = Last::None;
121 open = true;
122 },
123 b'L' | b'l' => {
124 let p = res!(sc.point(rel, cur));
125 pb.line_to(p);
126 cur = p;
127 last = Last::None;
128 },
129 b'H' | b'h' => {
130 let x = res!(sc.num());
131 let p = Pt::new(if rel { cur.x + x } else { x }, cur.y);
132 pb.line_to(p);
133 cur = p;
134 last = Last::None;
135 },
136 b'V' | b'v' => {
137 let y = res!(sc.num());
138 let p = Pt::new(cur.x, if rel { cur.y + y } else { y });
139 pb.line_to(p);
140 cur = p;
141 last = Last::None;
142 },
143 b'C' | b'c' => {
144 let c0 = res!(sc.point(rel, cur));
145 let c1 = res!(sc.point(rel, cur));
146 let p = res!(sc.point(rel, cur));
147 pb.cubic_to(c0, c1, p);
148 cur = p;
149 last = Last::Cubic(c1);
150 },
151 b'S' | b's' => {
152 let c1 = res!(sc.point(rel, cur));
153 let p = res!(sc.point(rel, cur));
154 let c0 = match last {
155 Last::Cubic(q) => reflect(cur, q),
156 _ => cur,
157 };
158 pb.cubic_to(c0, c1, p);
159 cur = p;
160 last = Last::Cubic(c1);
161 },
162 b'Q' | b'q' => {
163 let c = res!(sc.point(rel, cur));
164 let p = res!(sc.point(rel, cur));
165 pb.quad_to(c, p);
166 cur = p;
167 last = Last::Quad(c);
168 },
169 b'T' | b't' => {
170 let p = res!(sc.point(rel, cur));
171 let c = match last {
172 Last::Quad(q) => reflect(cur, q),
173 _ => cur,
174 };
175 pb.quad_to(c, p);
176 cur = p;
177 last = Last::Quad(c);
178 },
179 b'A' | b'a' => {
180 let rx = res!(sc.num());
181 let ry = res!(sc.num());
182 let rot = res!(sc.num());
183 let large = res!(sc.flag());
184 let sweep = res!(sc.flag());
185 let p = res!(sc.point(rel, cur));
186 arc(&mut pb, cur, Arc { rx, ry, rot, large, sweep }, p);
187 cur = p;
188 last = Last::None;
189 },
190 b'Z' | b'z' => {
191 pb.close();
192 cur = start;
193 last = Last::None;
194 open = false;
195 },
196 c => return Err(err!(
197 "'{}' is not an SVG path command.", c as char; Invalid, Input)),
198 }
199 }
200 pb.finish()
201}
202
203/// Reflects `q` through `p`, which is what `S` and `T` do to the previous control point to keep a
204/// curve smooth across the join.
205fn reflect(p: Pt, q: Pt) -> Pt {
206 Pt::new(2.0 * p.x - q.x, 2.0 * p.y - q.y)
207}
208
209/// Lays an elliptical arc onto `pb` as cubic béziers.
210///
211/// SVG states an arc by where it ends and which of the four candidate arcs to take; a bézier needs
212/// the centre and the angles spanned. The conversion between them is the one the SVG specification
213/// sets out in its implementation notes (F.6.5 for the centre, F.6.6 for radii too small to reach),
214/// after which each quadrant of the sweep takes one cubic.
215///
216/// The arithmetic runs in `f64` though the path is `f32`: the centre falls out of a difference of
217/// squares that cancels badly near the degenerate cases, and the wider type costs nothing here.
218///
219/// The pen is assumed to be at `p0`.
220fn arc(pb: &mut PathBuilder, p0: Pt, a: Arc, p1: Pt) {
221 // An arc whose ends coincide is dropped, and one with no radius is a straight line. Both are
222 // what the specification asks for, and both would otherwise divide by zero below.
223 if p0 == p1 {
224 return;
225 }
226 let (mut rx, mut ry) = ((a.rx as f64).abs(), (a.ry as f64).abs());
227 if rx == 0.0 || ry == 0.0 {
228 pb.line_to(p1);
229 return;
230 }
231 let (large, sweep) = (a.large, a.sweep);
232 let (x0, y0) = (p0.x as f64, p0.y as f64);
233 let (x1, y1) = (p1.x as f64, p1.y as f64);
234 let (sin_phi, cos_phi) = (a.rot as f64).to_radians().sin_cos();
235
236 // The ends in the ellipse's own frame, with their midpoint at the origin.
237 let dx = (x0 - x1) / 2.0;
238 let dy = (y0 - y1) / 2.0;
239 let xp = cos_phi * dx + sin_phi * dy;
240 let yp = -sin_phi * dx + cos_phi * dy;
241
242 // Radii too small to reach from one end to the other are grown until they just do (F.6.6).
243 let lam = (xp * xp) / (rx * rx) + (yp * yp) / (ry * ry);
244 if lam > 1.0 {
245 let s = lam.sqrt();
246 rx *= s;
247 ry *= s;
248 }
249
250 // The centre, in that frame and then back in the caller's (F.6.5).
251 let num = rx * rx * ry * ry - rx * rx * yp * yp - ry * ry * xp * xp;
252 let den = rx * rx * yp * yp + ry * ry * xp * xp;
253 // The max() holds the root real against rounding; lam has already made num non-negative.
254 let mut co = if den > 0.0 { (num / den).max(0.0).sqrt() } else { 0.0 };
255 if large == sweep {
256 co = -co;
257 }
258 let cxp = co * (rx * yp) / ry;
259 let cyp = -co * (ry * xp) / rx;
260 let cx = cos_phi * cxp - sin_phi * cyp + (x0 + x1) / 2.0;
261 let cy = sin_phi * cxp + cos_phi * cyp + (y0 + y1) / 2.0;
262
263 // Where the sweep starts and how far it goes.
264 let ux = (xp - cxp) / rx;
265 let uy = (yp - cyp) / ry;
266 let vx = (-xp - cxp) / rx;
267 let vy = (-yp - cyp) / ry;
268 let th0 = angle(1.0, 0.0, ux, uy);
269 let mut dth = angle(ux, uy, vx, vy);
270 if !sweep && dth > 0.0 {
271 dth -= 2.0 * PI;
272 }
273 if sweep && dth < 0.0 {
274 dth += 2.0 * PI;
275 }
276
277 // One cubic per quadrant of the sweep. A bézier meets a circular arc closely only over a short
278 // span, so the cut is what keeps the approximation honest.
279 let n = ((dth.abs() / (PI / 2.0)).ceil() as usize).clamp(1, ARC_SEGS);
280 let step = dth / n as f64;
281 // How far along the tangent a control point sits, for a piece spanning this angle. At a quarter
282 // turn this is the familiar 0.5523.
283 let k = (4.0 / 3.0) * (step / 4.0).tan();
284 // The point on the ellipse at an angle, and the derivative there.
285 let at = |t: f64| -> (f64, f64, f64, f64) {
286 let (s, c) = t.sin_cos();
287 (
288 cx + rx * cos_phi * c - ry * sin_phi * s,
289 cy + rx * sin_phi * c + ry * cos_phi * s,
290 -rx * cos_phi * s - ry * sin_phi * c,
291 -rx * sin_phi * s + ry * cos_phi * c,
292 )
293 };
294 for i in 0..n {
295 let t0 = th0 + step * i as f64;
296 let (px0, py0, dx0, dy0) = at(t0);
297 let (px1, py1, dx1, dy1) = at(t0 + step);
298 pb.cubic_to(
299 Pt::new((px0 + k * dx0) as f32, (py0 + k * dy0) as f32),
300 Pt::new((px1 - k * dx1) as f32, (py1 - k * dy1) as f32),
301 Pt::new(px1 as f32, py1 as f32),
302 );
303 }
304}
305
306/// The signed angle from one vector to another, which is what the arc conversion measures its sweep
307/// with.
308fn angle(ux: f64, uy: f64, vx: f64, vy: f64) -> f64 {
309 let len = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
310 if len == 0.0 {
311 return 0.0;
312 }
313 // The clamp holds acos in range against rounding, which a dot product of unit vectors can leave
314 // a hair outside.
315 let a = ((ux * vx + uy * vy) / len).clamp(-1.0, 1.0).acos();
316 if ux * vy - uy * vx < 0.0 {
317 -a
318 } else {
319 a
320 }
321}
322
323/// Writes a [`Path`] out as SVG path data -- the `d` attribute of a `<path>` element.
324///
325/// The exact inverse of [`path_data`]: every segment becomes the one command that names it, and a
326/// string this writes is one that reader reads back to the same geometry. Only absolute commands are
327/// emitted -- `M`, `L`, `Q`, `C`, `Z` -- since those are the segments the path types hold, and the
328/// relative and shorthand forms the reader also accepts are a convenience of hand-written data, not
329/// a distinction the geometry keeps.
330///
331/// The commands are separated by spaces, and the two coordinates of a point by a comma, which is the
332/// form drawing programs write and the eye reads most easily. No document, element or attribute is
333/// written -- only the path data -- for the reason [`path_data`] reads only the same: the structure
334/// above a `<path>` is the caller's format, not this crate's. An empty path writes an empty string.
335pub fn write_path_data(path: &Path) -> String {
336 let mut out = String::new();
337 for seg in path.segs() {
338 if !out.is_empty() {
339 out.push(' ');
340 }
341 match *seg {
342 Seg::MoveTo(p) => {
343 out.push('M');
344 point(&mut out, p);
345 },
346 Seg::LineTo(p) => {
347 out.push('L');
348 point(&mut out, p);
349 },
350 Seg::QuadTo(c, p) => {
351 out.push('Q');
352 point(&mut out, c);
353 out.push(' ');
354 point(&mut out, p);
355 },
356 Seg::CubicTo(c0, c1, p) => {
357 out.push('C');
358 point(&mut out, c0);
359 out.push(' ');
360 point(&mut out, c1);
361 out.push(' ');
362 point(&mut out, p);
363 },
364 Seg::Close => out.push('Z'),
365 }
366 }
367 out
368}
369
370/// Appends a point as `x,y`, each coordinate in its shortest exact form.
371fn point(out: &mut String, p: Pt) {
372 out.push_str(&num(p.x));
373 out.push(',');
374 out.push_str(&num(p.y));
375}
376
377/// One coordinate, in the shortest decimal that reads back to the same `f32`.
378///
379/// Rust's own float formatting already gives the shortest round-tripping form -- `10` for `10.0`,
380/// `0.15` for a fifth and a bit -- so a whole coordinate carries no trailing `.0` and the data stays
381/// terse, exactly as a drawing program would write it.
382fn num(v: f32) -> String {
383 fmt!("{}", v)
384}
385
386/// Renders the SVG presentation attributes for a fill and a stroke, as one attribute string.
387///
388/// This is the counterpart to [`write_path_data`] for everything that is not geometry: the colours,
389/// the pen width, the caps and joins and dashes that [`crate::stroke::Stroke`] and [`Rgba`] already
390/// model. It writes the attributes and their values -- `fill="#..."`, `stroke-width="2"`, and so on
391/// -- and nothing around them, so a caller drops the string straight into the `<path>` element its
392/// own format builds.
393///
394/// The fill and the stroke are each optional, because a shape may be filled, stroked, or both:
395/// * A fill of `Some(c)` writes `fill` and, where the colour is not opaque, `fill-opacity`. A fill
396/// of `None` writes `fill="none"`, since SVG fills black by default and a caller that wants no
397/// fill must say so.
398/// * A stroke of `Some((c, pen))` writes the stroke colour, its opacity where it is not opaque, the
399/// width, the cap, the join, the miter limit, and the dash pattern and offset where the pen
400/// carries one. A stroke of `None` writes nothing, and the shape is filled only.
401///
402/// The stroke colour travels with the pen because neither draws a stroke without the other: a width
403/// with no colour paints nothing, and a colour with no width has nothing to paint.
404pub fn presentation(fill: Option<Rgba>, stroke: Option<(Rgba, &Stroke)>) -> String {
405 let mut at: Vec<String> = Vec::new();
406 match fill {
407 None => at.push(fmt!("fill=\"none\"")),
408 Some(c) => {
409 at.push(fmt!("fill=\"{}\"", rgb(c)));
410 if !c.is_opaque() {
411 at.push(fmt!("fill-opacity=\"{}\"", opacity(c)));
412 }
413 },
414 }
415 if let Some((c, pen)) = stroke {
416 at.push(fmt!("stroke=\"{}\"", rgb(c)));
417 if !c.is_opaque() {
418 at.push(fmt!("stroke-opacity=\"{}\"", opacity(c)));
419 }
420 at.push(fmt!("stroke-width=\"{}\"", num(pen.width)));
421 at.push(fmt!("stroke-linecap=\"{}\"", cap(pen.cap)));
422 at.push(fmt!("stroke-linejoin=\"{}\"", join(pen.join)));
423 at.push(fmt!("stroke-miterlimit=\"{}\"", num(pen.miter_limit)));
424 if let Some(d) = &pen.dash {
425 let lens: Vec<String> = d.pattern.iter().map(|v| num(*v)).collect();
426 at.push(fmt!("stroke-dasharray=\"{}\"", lens.join(",")));
427 if d.offset != 0.0 {
428 at.push(fmt!("stroke-dashoffset=\"{}\"", num(d.offset)));
429 }
430 }
431 }
432 at.join(" ")
433}
434
435/// A colour's `#rrggbb`, the paint value an SVG attribute takes. The alpha, if any, is carried
436/// separately by an opacity attribute, which is the form every SVG renderer understands.
437fn rgb(c: Rgba) -> String {
438 fmt!("#{:02x}{:02x}{:02x}", c.r, c.g, c.b)
439}
440
441/// A colour's alpha as an opacity from 0 to 1, to three places, which resolves every one of the 256
442/// steps an eight-bit alpha can take.
443fn opacity(c: Rgba) -> String {
444 fmt!("{:.3}", (c.a as f32) / 255.0)
445}
446
447/// The SVG name of a line cap.
448fn cap(c: Cap) -> &'static str {
449 match c {
450 Cap::Butt => "butt",
451 Cap::Round => "round",
452 Cap::Square => "square",
453 }
454}
455
456/// The SVG name of a line join.
457fn join(j: Join) -> &'static str {
458 match j {
459 Join::Miter => "miter",
460 Join::Round => "round",
461 Join::Bevel => "bevel",
462 }
463}
464
465/// A cursor over path data.
466struct Scan<'a> {
467 s: &'a [u8], // ASCII throughout, so a byte index is always a character boundary
468 i: usize, // how far in the cursor has reached
469}
470
471impl<'a> Scan<'a> {
472 fn new(s: &'a str) -> Self {
473 Self { s: s.as_bytes(), i: 0 }
474 }
475
476 /// Steps over whitespace and commas, which separate numbers and mean nothing else.
477 fn sep(&mut self) {
478 while self.i < self.s.len() {
479 match self.s[self.i] {
480 b' ' | b'\t' | b'\n' | b'\r' | b'\x0C' | b',' => self.i += 1,
481 _ => break,
482 }
483 }
484 }
485
486 /// Is the data spent? Any separators are stepped over first.
487 fn done(&mut self) -> bool {
488 self.sep();
489 self.i >= self.s.len()
490 }
491
492 /// What is left, for an error to quote. Truncated, since path data runs long.
493 fn rest(&self) -> String {
494 let end = (self.i + 16).min(self.s.len());
495 String::from_utf8_lossy(&self.s[self.i..end]).into_owned()
496 }
497
498 /// Takes the next byte if it is a command letter, and leaves the cursor alone if not.
499 fn cmd(&mut self) -> Option<u8> {
500 self.sep();
501 if self.i < self.s.len() && self.s[self.i].is_ascii_alphabetic() {
502 self.i += 1;
503 Some(self.s[self.i - 1])
504 } else {
505 None
506 }
507 }
508
509 /// Reads one number.
510 ///
511 /// The grammar is looser than Rust's: a sign is optional, either side of the point may be empty,
512 /// and there is no separator requirement -- so `1.5.5` is two numbers and `-1-2` is two more.
513 /// The scanner therefore stops at the second point rather than trusting `parse` to complain.
514 fn num(&mut self) -> Outcome<f32> {
515 self.sep();
516 let from = self.i;
517 if self.i < self.s.len() && (self.s[self.i] == b'+' || self.s[self.i] == b'-') {
518 self.i += 1;
519 }
520 let mut any = false; // A number needs at least one digit, on one side or the other.
521 while self.i < self.s.len() && self.s[self.i].is_ascii_digit() {
522 self.i += 1;
523 any = true;
524 }
525 if self.i < self.s.len() && self.s[self.i] == b'.' {
526 self.i += 1;
527 while self.i < self.s.len() && self.s[self.i].is_ascii_digit() {
528 self.i += 1;
529 any = true;
530 }
531 }
532 if !any {
533 return Err(err!(
534 "Expected a number at byte {} of the path data, found '{}'.",
535 from, self.rest(); Invalid, Input));
536 }
537 // An exponent counts only if digits follow it. Otherwise the 'e' is not ours -- path data
538 // has no command by that name, but being strict here keeps the error at the right byte.
539 if self.i < self.s.len() && (self.s[self.i] == b'e' || self.s[self.i] == b'E') {
540 let mark = self.i;
541 self.i += 1;
542 if self.i < self.s.len() && (self.s[self.i] == b'+' || self.s[self.i] == b'-') {
543 self.i += 1;
544 }
545 if self.i < self.s.len() && self.s[self.i].is_ascii_digit() {
546 while self.i < self.s.len() && self.s[self.i].is_ascii_digit() {
547 self.i += 1;
548 }
549 } else {
550 self.i = mark;
551 }
552 }
553 let txt = res!(std::str::from_utf8(&self.s[from..self.i]));
554 match txt.parse::<f32>() {
555 Ok(v) => Ok(v),
556 Err(e) => Err(err!(e,
557 "'{}' at byte {} of the path data is not a number.", txt, from;
558 Invalid, Input)),
559 }
560 }
561
562 /// Reads an arc flag: a single `0` or `1`.
563 ///
564 /// A flag is one character and needs no separator, so `0 011` is two flags and the start of a
565 /// number. Reading it with [`Self::num`] would swallow the digits that follow it.
566 fn flag(&mut self) -> Outcome<bool> {
567 self.sep();
568 if self.i >= self.s.len() {
569 return Err(err!("The path data ended where an arc flag was expected."; Invalid, Input));
570 }
571 self.i += 1;
572 match self.s[self.i - 1] {
573 b'0' => Ok(false),
574 b'1' => Ok(true),
575 c => Err(err!(
576 "An arc flag is '0' or '1', found '{}' at byte {} of the path data.",
577 c as char, self.i - 1; Invalid, Input)),
578 }
579 }
580
581 /// Reads a coordinate pair, offset from `from` when the command was relative.
582 fn point(&mut self, rel: bool, from: Pt) -> Outcome<Pt> {
583 let x = res!(self.num());
584 let y = res!(self.num());
585 Ok(if rel {
586 Pt::new(from.x + x, from.y + y)
587 } else {
588 Pt::new(x, y)
589 })
590 }
591}
592
593#[cfg(test)]
594mod tests {
595 use super::*;
596 use crate::{
597 path::{
598 Seg,
599 TOLERANCE,
600 },
601 transform::Transform,
602 };
603
604 #[test]
605 fn test_a_moveto_and_a_lineto_place_the_pen_00() -> Outcome<()> {
606 let p = res!(path_data("M 10 20 L 30 40"));
607 assert_eq!(p.segs(), &[Seg::MoveTo(Pt::new(10.0, 20.0)), Seg::LineTo(Pt::new(30.0, 40.0))]);
608 Ok(())
609 }
610
611 #[test]
612 fn test_a_lower_case_command_is_relative_to_the_pen_01() -> Outcome<()> {
613 let p = res!(path_data("M 10 10 l 5 5 l 5 5"));
614 assert_eq!(p.segs(), &[
615 Seg::MoveTo(Pt::new(10.0, 10.0)),
616 Seg::LineTo(Pt::new(15.0, 15.0)),
617 Seg::LineTo(Pt::new(20.0, 20.0)),
618 ]);
619 Ok(())
620 }
621
622 #[test]
623 fn test_a_repeated_moveto_is_a_lineto_02() -> Outcome<()> {
624 // The grammar's one irregularity: extra pairs after a moveto are linetos, not movetos. Read
625 // as movetos they would be three contours of one point each, and nothing would be drawn.
626 let p = res!(path_data("M 0 0 1 1 2 2"));
627 assert_eq!(p.segs(), &[
628 Seg::MoveTo(Pt::new(0.0, 0.0)),
629 Seg::LineTo(Pt::new(1.0, 1.0)),
630 Seg::LineTo(Pt::new(2.0, 2.0)),
631 ]);
632 Ok(())
633 }
634
635 #[test]
636 fn test_a_command_letter_may_be_left_out_to_repeat_it_03() -> Outcome<()> {
637 let p = res!(path_data("M 0 0 L 1 1 2 2 3 3"));
638 assert_eq!(p.segs().len(), 4);
639 assert_eq!(p.segs()[3], Seg::LineTo(Pt::new(3.0, 3.0)));
640 Ok(())
641 }
642
643 #[test]
644 fn test_two_numbers_may_share_a_point_04() -> Outcome<()> {
645 // `1.5.5` is 1.5 then 0.5: the grammar needs no separator between numbers, so a second point
646 // ends the first number. A scanner that read greedily to the next separator would see one
647 // malformed number and reject a legal file.
648 let p = res!(path_data("M1.5.5L.5 1"));
649 assert_eq!(p.segs()[0], Seg::MoveTo(Pt::new(1.5, 0.5)));
650 assert_eq!(p.segs()[1], Seg::LineTo(Pt::new(0.5, 1.0)));
651 Ok(())
652 }
653
654 #[test]
655 fn test_a_sign_separates_numbers_05() -> Outcome<()> {
656 // `-1-2` is two numbers, for the same reason.
657 let p = res!(path_data("M0 0L-1-2"));
658 assert_eq!(p.segs()[1], Seg::LineTo(Pt::new(-1.0, -2.0)));
659 Ok(())
660 }
661
662 #[test]
663 fn test_an_exponent_is_read_06() -> Outcome<()> {
664 let p = res!(path_data("M 0 0 L 1e2 1.5e-1"));
665 assert_eq!(p.segs()[1], Seg::LineTo(Pt::new(100.0, 0.15)));
666 Ok(())
667 }
668
669 #[test]
670 fn test_an_arc_flag_needs_no_separator_07() -> Outcome<()> {
671 // `0 011 1` is two flags then the endpoint. A flag read as a number would swallow `011` whole
672 // and the arc would land somewhere else entirely -- silently, with no error to notice.
673 let a = res!(path_data("M 0 0 a 1 1 0 011 1"));
674 let b = res!(path_data("M 0 0 a 1 1 0 0 1 1 1"));
675 assert_eq!(a.segs(), b.segs());
676 Ok(())
677 }
678
679 #[test]
680 fn test_a_smooth_cubic_reflects_the_last_control_point_08() -> Outcome<()> {
681 // After C with its second control at (2,2) and the pen at (3,3), S's first control must be
682 // the reflection, (4,4).
683 let p = res!(path_data("M 0 0 C 1 1 2 2 3 3 S 5 5 6 6"));
684 match p.segs()[2] {
685 Seg::CubicTo(c0, _, _) => assert_eq!(c0, Pt::new(4.0, 4.0)),
686 s => return Err(err!("Expected a cubic, found {:?}.", s; Test, Invalid)),
687 }
688 Ok(())
689 }
690
691 #[test]
692 fn test_a_smooth_cubic_after_a_non_curve_uses_the_pen_09() -> Outcome<()> {
693 // There is nothing to reflect, so the first control coincides with the current point. A
694 // reader that reflected a stale control point would bend the curve the wrong way.
695 let p = res!(path_data("M 0 0 L 3 3 S 5 5 6 6"));
696 match p.segs()[2] {
697 Seg::CubicTo(c0, _, _) => assert_eq!(c0, Pt::new(3.0, 3.0)),
698 s => return Err(err!("Expected a cubic, found {:?}.", s; Test, Invalid)),
699 }
700 Ok(())
701 }
702
703 #[test]
704 fn test_a_smooth_cubic_does_not_reflect_a_quadratics_control_10() -> Outcome<()> {
705 // `S` reflects only a cubic's control point. After a `Q`, there is nothing of its kind to
706 // reflect, so the pen is used -- which is why the last control point carries its kind.
707 let p = res!(path_data("M 0 0 Q 1 1 3 3 S 5 5 6 6"));
708 match p.segs()[2] {
709 Seg::CubicTo(c0, _, _) => assert_eq!(c0, Pt::new(3.0, 3.0)),
710 s => return Err(err!("Expected a cubic, found {:?}.", s; Test, Invalid)),
711 }
712 Ok(())
713 }
714
715 #[test]
716 fn test_close_returns_the_pen_to_where_the_contour_began_11() -> Outcome<()> {
717 // The `l 1 0` after `Z` is relative to (2,2), where the contour started, not to (5,5) where
718 // the pen last drew. The close also ends the contour, so the next subpath opens at that same
719 // point -- which is what the implicit moveto records.
720 let p = res!(path_data("M 2 2 L 5 5 Z l 1 0"));
721 assert_eq!(p.segs(), &[
722 Seg::MoveTo(Pt::new(2.0, 2.0)),
723 Seg::LineTo(Pt::new(5.0, 5.0)),
724 Seg::Close,
725 Seg::MoveTo(Pt::new(2.0, 2.0)),
726 Seg::LineTo(Pt::new(3.0, 2.0)),
727 ]);
728 Ok(())
729 }
730
731 #[test]
732 fn test_horizontal_and_vertical_hold_the_other_axis_12() -> Outcome<()> {
733 let p = res!(path_data("M 1 2 H 5 V 8 h -1 v -1"));
734 assert_eq!(p.segs()[1], Seg::LineTo(Pt::new(5.0, 2.0)));
735 assert_eq!(p.segs()[2], Seg::LineTo(Pt::new(5.0, 8.0)));
736 assert_eq!(p.segs()[3], Seg::LineTo(Pt::new(4.0, 8.0)));
737 assert_eq!(p.segs()[4], Seg::LineTo(Pt::new(4.0, 7.0)));
738 Ok(())
739 }
740
741 #[test]
742 fn test_an_arc_stays_on_its_radius_13() -> Outcome<()> {
743 // Two half-turn arcs make a circle of radius 100 about the origin. Every flattened point must
744 // sit on that radius: this is the whole arc conversion -- centre, angles and all -- checked
745 // against geometry rather than against itself.
746 let p = res!(path_data("M 100 0 A 100 100 0 0 1 -100 0 A 100 100 0 0 1 100 0 Z"));
747 let cs = p.flatten(&Transform::IDENTITY, TOLERANCE);
748 let mut n = 0;
749 for c in &cs {
750 for q in c {
751 let r = (q.x * q.x + q.y * q.y).sqrt();
752 assert!((r - 100.0).abs() < 0.5, "point ({}, {}) sits at radius {}", q.x, q.y, r);
753 n += 1;
754 }
755 }
756 assert!(n > 16, "a circle of radius 100 flattened to only {} points", n);
757 Ok(())
758 }
759
760 #[test]
761 fn test_the_sweep_flag_picks_the_side_the_arc_bulges_14() -> Outcome<()> {
762 // The same ends and radii, opposite sweeps: one arc must bow above the chord and the other
763 // below. Getting this backwards mirrors every rounded shape in a drawing.
764 let up = res!(path_data("M 0 0 A 50 50 0 0 1 100 0"));
765 let dn = res!(path_data("M 0 0 A 50 50 0 0 0 100 0"));
766 let mid = |p: &Path| -> f32 {
767 let cs = p.flatten(&Transform::IDENTITY, TOLERANCE);
768 let mut y = 0.0;
769 for c in &cs {
770 for q in c {
771 if (q.x - 50.0).abs() < 2.0 {
772 y = q.y;
773 }
774 }
775 }
776 y
777 };
778 assert!(mid(&up) < -40.0, "sweep 1 should bow to negative y, reached {}", mid(&up));
779 assert!(mid(&dn) > 40.0, "sweep 0 should bow to positive y, reached {}", mid(&dn));
780 Ok(())
781 }
782
783 #[test]
784 fn test_an_arc_with_no_radius_is_a_straight_line_15() -> Outcome<()> {
785 let p = res!(path_data("M 0 0 A 0 0 0 0 1 10 10"));
786 assert_eq!(p.segs()[1], Seg::LineTo(Pt::new(10.0, 10.0)));
787 Ok(())
788 }
789
790 #[test]
791 fn test_an_arc_that_ends_where_it_starts_is_dropped_16() -> Outcome<()> {
792 // The specification says so, and the conversion would divide by zero otherwise.
793 let p = res!(path_data("M 5 5 A 10 10 0 1 1 5 5"));
794 assert_eq!(p.segs(), &[Seg::MoveTo(Pt::new(5.0, 5.0))]);
795 Ok(())
796 }
797
798 #[test]
799 fn test_radii_too_small_to_reach_are_grown_17() -> Outcome<()> {
800 // The ends are 100 apart but the radii say 10. The specification grows them rather than
801 // failing, so the arc must still land on its endpoint.
802 let p = res!(path_data("M 0 0 A 10 10 0 0 1 100 0"));
803 let end = match p.segs().last() {
804 Some(Seg::CubicTo(_, _, e)) => *e,
805 s => return Err(err!("Expected a cubic last, found {:?}.", s; Test, Invalid)),
806 };
807 assert!((end.x - 100.0).abs() < 0.01 && end.y.abs() < 0.01,
808 "the arc ended at ({}, {}) rather than (100, 0)", end.x, end.y);
809 Ok(())
810 }
811
812 #[test]
813 fn test_data_that_does_not_begin_with_a_command_is_refused_18() -> Outcome<()> {
814 assert!(path_data("10 20 L 30 40").is_err());
815 Ok(())
816 }
817
818 #[test]
819 fn test_an_unknown_command_is_refused_19() -> Outcome<()> {
820 assert!(path_data("M 0 0 X 1 1").is_err());
821 Ok(())
822 }
823
824 #[test]
825 fn test_a_command_missing_an_argument_is_refused_20() -> Outcome<()> {
826 assert!(path_data("M 0 0 L 5").is_err());
827 Ok(())
828 }
829
830 #[test]
831 fn test_an_arc_flag_that_is_not_zero_or_one_is_refused_21() -> Outcome<()> {
832 assert!(path_data("M 0 0 a 1 1 0 2 1 1 1").is_err());
833 Ok(())
834 }
835
836 #[test]
837 fn test_empty_data_is_an_empty_path_22() -> Outcome<()> {
838 let p = res!(path_data(" "));
839 assert!(p.is_empty());
840 Ok(())
841 }
842
843 #[test]
844 fn test_data_that_does_not_begin_with_a_moveto_is_refused_23() -> Outcome<()> {
845 // A drawing command has nowhere to draw from until a moveto has named a pen position.
846 // Quietly starting at the origin would put the shape somewhere the author never asked for.
847 assert!(path_data("L 30 40").is_err());
848 assert!(path_data("C 1 1 2 2 3 3").is_err());
849 Ok(())
850 }
851
852 #[test]
853 fn test_a_first_relative_moveto_is_absolute_24() -> Outcome<()> {
854 // It is measured from a pen at the origin, so it lands on its own coordinates.
855 let p = res!(path_data("m 10 20 l 1 1"));
856 assert_eq!(p.segs()[0], Seg::MoveTo(Pt::new(10.0, 20.0)));
857 Ok(())
858 }
859
860 #[test]
861 fn test_a_line_writes_the_expected_data_25() -> Outcome<()> {
862 // The hand-known case: a move to the origin and a line to (10, 0) is exactly "M0,0 L10,0".
863 // Whole coordinates carry no decimal point, and a comma joins each pair, a space each
864 // command.
865 let p = res!(path_data("M 0 0 L 10 0"));
866 assert_eq!(write_path_data(&p), "M0,0 L10,0");
867 Ok(())
868 }
869
870 #[test]
871 fn test_every_command_writes_its_letter_26() -> Outcome<()> {
872 // One of each segment kind, so the writer's whole command vocabulary is pinned to a known
873 // string.
874 let mut pb = PathBuilder::new();
875 pb.move_to(Pt::new(1.0, 2.0));
876 pb.line_to(Pt::new(3.0, 4.0));
877 pb.quad_to(Pt::new(5.0, 6.0), Pt::new(7.0, 8.0));
878 pb.cubic_to(Pt::new(9.0, 10.0), Pt::new(11.0, 12.0), Pt::new(13.0, 14.0));
879 pb.close();
880 let p = res!(pb.finish());
881 assert_eq!(write_path_data(&p), "M1,2 L3,4 Q5,6 7,8 C9,10 11,12 13,14 Z");
882 Ok(())
883 }
884
885 #[test]
886 fn test_an_empty_path_writes_an_empty_string_27() -> Outcome<()> {
887 let p = res!(PathBuilder::new().finish());
888 assert_eq!(write_path_data(&p), "");
889 Ok(())
890 }
891
892 #[test]
893 fn test_the_writer_round_trips_through_the_reader_28() -> Outcome<()> {
894 // The writer is the reader's inverse: a path written to data and read back is the path it
895 // began as, segment for segment. The reader is the external oracle here -- the geometry is
896 // checked against the module that already reads what every drawing program writes, not
897 // against the writer restated. Fractional coordinates are used deliberately, so the test
898 // bites on the number formatting and not only on round integers.
899 let mut pb = PathBuilder::new();
900 pb.move_to(Pt::new(1.5, -2.25));
901 pb.line_to(Pt::new(10.0, 0.5));
902 pb.quad_to(Pt::new(12.5, 3.75), Pt::new(20.0, -1.5));
903 pb.cubic_to(Pt::new(21.0, 2.0), Pt::new(23.5, 4.5), Pt::new(30.0, 0.0));
904 pb.close();
905 let p = res!(pb.finish());
906 let back = res!(path_data(&write_path_data(&p)));
907 assert_eq!(p.segs(), back.segs(), "the path did not survive the round trip");
908 Ok(())
909 }
910
911 #[test]
912 fn test_a_curved_shape_round_trips_29() -> Outcome<()> {
913 // A whole built shape -- a rounded rectangle, all lines and cubics -- survives the round
914 // trip through data and back, so the writer holds up on geometry it did not itself hand-pick.
915 use crate::path::Bounds;
916 let p = res!(Path::round_rect(Bounds::new(2.0, 3.0, 40.0, 25.0), 6.0));
917 let back = res!(path_data(&write_path_data(&p)));
918 assert_eq!(p.segs(), back.segs());
919 Ok(())
920 }
921
922 #[test]
923 fn test_presentation_writes_fill_and_stroke_attributes_30() -> Outcome<()> {
924 use crate::stroke::{
925 Cap,
926 Dash,
927 Join,
928 };
929 let pen = res!(Stroke::new(2.0))
930 .with_cap(Cap::Round)
931 .with_join(Join::Bevel)
932 .with_dash(Dash::new(vec![4.0, 2.0]).with_offset(1.0));
933 let attrs = presentation(Some(res!(Rgba::from_hex("#ff8800"))), Some((Rgba::BLACK, &pen)));
934 assert!(attrs.contains("fill=\"#ff8800\""), "the fill colour, found: {}", attrs);
935 assert!(attrs.contains("stroke=\"#000000\""), "the stroke colour");
936 assert!(attrs.contains("stroke-width=\"2\""), "the pen width");
937 assert!(attrs.contains("stroke-linecap=\"round\""), "the cap");
938 assert!(attrs.contains("stroke-linejoin=\"bevel\""), "the join");
939 assert!(attrs.contains("stroke-dasharray=\"4,2\""), "the dash pattern");
940 assert!(attrs.contains("stroke-dashoffset=\"1\""), "the dash offset");
941 Ok(())
942 }
943
944 #[test]
945 fn test_presentation_says_none_for_no_fill_and_carries_alpha_31() -> Outcome<()> {
946 // No fill must be stated outright, since SVG fills black by default. A translucent stroke
947 // splits into a colour and a separate opacity, the form every renderer reads.
948 let pen = res!(Stroke::new(1.0));
949 let attrs = presentation(None, Some((Rgba::new(0, 0, 0, 128), &pen)));
950 assert!(attrs.contains("fill=\"none\""), "no fill, found: {}", attrs);
951 assert!(attrs.contains("stroke=\"#000000\""), "the stroke colour without its alpha");
952 assert!(attrs.contains("stroke-opacity=\"0.502\""), "the alpha as an opacity, found: {}", attrs);
953 Ok(())
954 }
955}