Skip to main content

odox_core/
draw.rs

1//! `draw:enhanced-geometry`: the outline of a custom shape, worked out.
2//!
3//! A custom shape does not state its outline as points. It states a *path* in a
4//! compact command language, in a coordinate space of its own, whose numbers may
5//! be references to named formulas that are themselves arithmetic over the
6//! space's edges and over adjustment values a person dragged. A rounded
7//! rectangle is `M ?f7 0 X 0 ?f8 L 0 ?f9 Y ?f7 21600 …`, and none of that means
8//! anything until the formulas are evaluated.
9//!
10//! What comes out of here is [`Geometry`]: the same outline as flat polylines in
11//! the shape's own space, ready for a renderer to map onto a rectangle. Curves
12//! and arcs are flattened here rather than passed on, because the number of
13//! segments a curve needs depends on the size of the coordinate space and not on
14//! the size of the window, and this is where the space is known.
15//!
16//! A `draw:path` states its outline the other way, in SVG's path notation in
17//! `svg:d`, and comes out of here as the same [`Geometry`]. The two languages
18//! have the same shape — move, line, curve, close — and share the pen below.
19//!
20//! # What is implemented
21//!
22//! The whole command language except `Q`'s smooth variants, and the formula
23//! grammar in full. What is *measured* is narrower: across the twenty-three
24//! presentation templates `LibreOffice` ships, the commands that occur are `M`,
25//! `L`, `C`, `Z`, `N`, `U`, `X`, `Y` and `V`, and the formulas use the four edge
26//! constants, `pi`, and `if`, `sin`, `cos` and `abs`. The arc commands `A`, `B`
27//! and `W` occur nowhere in that set and their sweep direction is taken from the
28//! specification rather than from a document.
29//
30// Author: David M. Anderson
31// Built with AI assistance (Claude, Anthropic)
32
33use std::cell::{Cell, RefCell};
34use std::collections::HashMap;
35
36use crate::xml::{Element, Ns};
37
38/// The coordinate space a shape states its outline in.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct ViewBox {
41    /// The left edge.
42    pub x: f32,
43    /// The top edge.
44    pub y: f32,
45    /// How wide.
46    pub width: f32,
47    /// How tall.
48    pub height: f32,
49}
50
51impl ViewBox {
52    fn parse(text: &str) -> Option<Self> {
53        let numbers: Vec<f32> = text
54            .split_whitespace()
55            .filter_map(|n| n.parse().ok())
56            .collect();
57        let [x, y, width, height] = numbers[..] else {
58            return None;
59        };
60        (width > 0.0 && height > 0.0).then_some(Self {
61            x,
62            y,
63            width,
64            height,
65        })
66    }
67}
68
69/// One stroke of the pen: a run of points, and what is done with them.
70#[derive(Debug, Clone, PartialEq)]
71pub struct SubPath {
72    /// The points, in the shape's own coordinate space.
73    pub points: Vec<(f32, f32)>,
74    /// Whether the last point joins the first.
75    pub closed: bool,
76    /// Whether the inside is filled. `F` in the path turns it off for the rest
77    /// of the path, which is how a shape draws a detail over its own body.
78    pub fill: bool,
79    /// Whether the outline is drawn. `S` turns it off.
80    pub stroke: bool,
81}
82
83/// A custom shape's outline.
84#[derive(Debug, Clone, PartialEq)]
85pub struct Geometry {
86    /// The space the points are in.
87    pub view: ViewBox,
88    /// The strokes, in the order they are drawn.
89    pub paths: Vec<SubPath>,
90}
91
92/// How finely a curve is broken into straight lines.
93///
94/// Sixteen segments for a whole cubic and one for every six degrees of an arc,
95/// which at the sizes ODF's coordinate spaces use — twenty-one thousand six
96/// hundred units across, drawn into a few hundred pixels — is below what a
97/// screen can show.
98const CURVE_SEGMENTS: usize = 16;
99const DEGREES_PER_SEGMENT: f32 = 6.0;
100
101impl Geometry {
102    /// Read a `draw:path` element, whose outline is SVG path data in `svg:d`.
103    ///
104    /// `None` where it states no path or no coordinate space.
105    ///
106    /// **Arcs are drawn as the straight line to where they end.** `A` and `a`
107    /// occur in none of the 352 paths across the presentation templates
108    /// `LibreOffice` ships, and turning an endpoint-parameterised elliptical arc
109    /// into a centre and a sweep is a page of trigonometry to serve nothing that
110    /// exists. A line keeps the outline closed and roughly where it belongs,
111    /// which is the failure worth having.
112    pub fn read_path(path: &Element) -> Option<Self> {
113        let view = ViewBox::parse(path.attr(&Ns::Svg, "viewBox")?)?;
114        let data = path.attr(&Ns::Svg, "d")?;
115        let mut pen = Pen::new();
116        pen.svg(data);
117        Some(Self {
118            view,
119            paths: pen.finish(),
120        })
121    }
122
123    /// Refit the view box to the outline inside it.
124    ///
125    /// A `draw:connector` is the shape that needs this. Its route is written in
126    /// the page's own coordinates while the `svg:viewBox` beside it states an
127    /// origin of zero, so the two do not agree and the declared box places the
128    /// points nowhere near the shape. The outline's own bounding box does place
129    /// them, and for a shape whose box was already right it says the same thing.
130    ///
131    /// An outline with no width or no height keeps a box one unit across, so
132    /// that a straight horizontal connector divides by something.
133    pub fn refit(&mut self) {
134        let points = || self.paths.iter().flat_map(|path| path.points.iter());
135        let Some(&(x, y)) = points().next() else {
136            return;
137        };
138        let (mut left, mut right, mut top, mut bottom) = (x, x, y, y);
139        for &(x, y) in points() {
140            left = left.min(x);
141            right = right.max(x);
142            top = top.min(y);
143            bottom = bottom.max(y);
144        }
145        self.view = ViewBox {
146            x: left,
147            y: top,
148            width: (right - left).max(f32::EPSILON),
149            height: (bottom - top).max(f32::EPSILON),
150        };
151    }
152
153    /// Read a `draw:enhanced-geometry` element.
154    ///
155    /// `None` where it states no path or no coordinate space, which is a shape
156    /// nothing can draw.
157    pub fn read(geometry: &Element) -> Option<Self> {
158        let view = ViewBox::parse(geometry.attr(&Ns::Svg, "viewBox")?)?;
159        let path = geometry.attr(&Ns::Draw, "enhanced-path")?;
160
161        let formulas = Formulas::new(geometry, view);
162        let mut pen = Pen::new();
163        pen.run(path, &formulas);
164        let mut paths = pen.finish();
165
166        // A mirrored shape states its outline once and is drawn flipped.
167        let flip_x = geometry.attr(&Ns::Draw, "mirror-horizontal") == Some("true");
168        let flip_y = geometry.attr(&Ns::Draw, "mirror-vertical") == Some("true");
169        if flip_x || flip_y {
170            for path in &mut paths {
171                for (x, y) in &mut path.points {
172                    if flip_x {
173                        *x = view.x + view.width - (*x - view.x);
174                    }
175                    if flip_y {
176                        *y = view.y + view.height - (*y - view.y);
177                    }
178                }
179            }
180        }
181
182        Some(Self { view, paths })
183    }
184}
185
186/// The named formulas of one shape, and the values they are over.
187struct Formulas<'a> {
188    by_name: HashMap<&'a str, &'a str>,
189    modifiers: Vec<f32>,
190    view: ViewBox,
191    /// Evaluated formulas, kept because a chain of thirty each referring to the
192    /// one before is ordinary and re-evaluating it per reference is not.
193    known: RefCell<HashMap<String, f32>>,
194    depth: Cell<u32>,
195}
196
197/// How deep a formula may refer before it is called a cycle.
198///
199/// No writer produces one; a hand-edited file can, and the alternative to a
200/// limit is a window that stops responding.
201const MAX_DEPTH: u32 = 64;
202
203impl<'a> Formulas<'a> {
204    fn new(geometry: &'a Element, view: ViewBox) -> Self {
205        let by_name = geometry
206            .elements()
207            .filter(|e| e.is(&Ns::Draw, "equation"))
208            .filter_map(|e| Some((e.attr(&Ns::Draw, "name")?, e.attr(&Ns::Draw, "formula")?)))
209            .collect();
210        let modifiers = geometry
211            .attr(&Ns::Draw, "modifiers")
212            .unwrap_or_default()
213            .split_whitespace()
214            .filter_map(|n| n.parse().ok())
215            .collect();
216        Self {
217            by_name,
218            modifiers,
219            view,
220            known: RefCell::new(HashMap::new()),
221            depth: Cell::new(0),
222        }
223    }
224
225    /// The value of a named formula.
226    fn named(&self, name: &str) -> f32 {
227        if let Some(value) = self.known.borrow().get(name) {
228            return *value;
229        }
230        if self.depth.get() >= MAX_DEPTH {
231            return 0.0;
232        }
233        let Some(text) = self.by_name.get(name) else {
234            return 0.0;
235        };
236        self.depth.set(self.depth.get() + 1);
237        let value = self.eval(text);
238        self.depth.set(self.depth.get() - 1);
239        self.known.borrow_mut().insert(name.to_owned(), value);
240        value
241    }
242
243    /// A modifier, which is an adjustment a person dragged and the document
244    /// stored.
245    fn modifier(&self, index: usize) -> f32 {
246        self.modifiers.get(index).copied().unwrap_or(0.0)
247    }
248
249    /// One of the constants a formula may name.
250    fn constant(&self, name: &str) -> Option<f32> {
251        Some(match name {
252            "left" => self.view.x,
253            "top" => self.view.y,
254            "right" => self.view.x + self.view.width,
255            "bottom" => self.view.y + self.view.height,
256            "width" | "logwidth" => self.view.width,
257            "height" | "logheight" => self.view.height,
258            "pi" => std::f32::consts::PI,
259            // A shape may ask whether it is being stroked or filled and draw
260            // differently. Nothing here answers no.
261            "hasstroke" | "hasfill" => 1.0,
262            "xstretch" | "ystretch" => 0.0,
263            _ => return None,
264        })
265    }
266
267    fn eval(&self, text: &str) -> f32 {
268        Expression {
269            text: text.as_bytes(),
270            at: 0,
271            formulas: self,
272        }
273        .expression()
274    }
275}
276
277/// A formula, read left to right.
278struct Expression<'a, 'f> {
279    text: &'a [u8],
280    at: usize,
281    formulas: &'a Formulas<'f>,
282}
283
284impl Expression<'_, '_> {
285    fn skip(&mut self) {
286        while self.at < self.text.len() && self.text[self.at].is_ascii_whitespace() {
287            self.at += 1;
288        }
289    }
290
291    fn peek(&mut self) -> Option<u8> {
292        self.skip();
293        self.text.get(self.at).copied()
294    }
295
296    fn take(&mut self, byte: u8) -> bool {
297        if self.peek() == Some(byte) {
298            self.at += 1;
299            return true;
300        }
301        false
302    }
303
304    fn expression(&mut self) -> f32 {
305        let mut value = self.term();
306        loop {
307            if self.take(b'+') {
308                value += self.term();
309            } else if self.take(b'-') {
310                value -= self.term();
311            } else {
312                return value;
313            }
314        }
315    }
316
317    fn term(&mut self) -> f32 {
318        let mut value = self.factor();
319        loop {
320            if self.take(b'*') {
321                value *= self.factor();
322            } else if self.take(b'/') {
323                let divisor = self.factor();
324                // A formula dividing by zero is a shape somebody edited by hand.
325                // Nought is a point that can be drawn; infinity is not.
326                value = if divisor == 0.0 { 0.0 } else { value / divisor };
327            } else {
328                return value;
329            }
330        }
331    }
332
333    fn factor(&mut self) -> f32 {
334        if self.take(b'-') {
335            return -self.factor();
336        }
337        if self.take(b'+') {
338            return self.factor();
339        }
340        if self.take(b'(') {
341            let value = self.expression();
342            self.take(b')');
343            return value;
344        }
345        if self.take(b'?') {
346            let name = self.word();
347            return self.formulas.named(&name);
348        }
349        if self.take(b'$') {
350            let index = self.word().parse().unwrap_or(0);
351            return self.formulas.modifier(index);
352        }
353        match self.peek() {
354            Some(byte) if byte.is_ascii_alphabetic() => {
355                let name = self.word();
356                if self.take(b'(') {
357                    let arguments = self.arguments();
358                    return call(&name, &arguments);
359                }
360                self.formulas.constant(&name).unwrap_or(0.0)
361            }
362            _ => self.number(),
363        }
364    }
365
366    fn arguments(&mut self) -> Vec<f32> {
367        let mut arguments = Vec::new();
368        if self.take(b')') {
369            return arguments;
370        }
371        loop {
372            arguments.push(self.expression());
373            if !self.take(b',') {
374                self.take(b')');
375                return arguments;
376            }
377        }
378    }
379
380    /// A name or a run of digits, whichever is under the cursor.
381    fn word(&mut self) -> String {
382        self.skip();
383        let start = self.at;
384        while self
385            .text
386            .get(self.at)
387            .is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_')
388        {
389            self.at += 1;
390        }
391        String::from_utf8_lossy(&self.text[start..self.at]).into_owned()
392    }
393
394    fn number(&mut self) -> f32 {
395        self.skip();
396        let start = self.at;
397        while self
398            .text
399            .get(self.at)
400            .is_some_and(|b| b.is_ascii_digit() || *b == b'.')
401        {
402            self.at += 1;
403        }
404        if start == self.at {
405            // Nothing readable here: step over it so that a malformed formula
406            // ends rather than spins.
407            self.at += 1;
408            return 0.0;
409        }
410        String::from_utf8_lossy(&self.text[start..self.at])
411            .parse()
412            .unwrap_or(0.0)
413    }
414}
415
416fn call(name: &str, arguments: &[f32]) -> f32 {
417    let argument = |n: usize| arguments.get(n).copied().unwrap_or(0.0);
418    match name {
419        "abs" => argument(0).abs(),
420        "sqrt" => argument(0).max(0.0).sqrt(),
421        // Radians. A formula that means degrees writes the conversion itself,
422        // as `sin($0 * (pi/180))`, which is how every one in the corpus does it.
423        "sin" => argument(0).sin(),
424        "cos" => argument(0).cos(),
425        "tan" => argument(0).tan(),
426        "atan" => argument(0).atan(),
427        "atan2" => argument(0).atan2(argument(1)),
428        "min" => argument(0).min(argument(1)),
429        "max" => argument(0).max(argument(1)),
430        // Greater than nought is true, which is the specification's rule and not
431        // the usual one.
432        "if" => {
433            if argument(0) > 0.0 {
434                argument(1)
435            } else {
436                argument(2)
437            }
438        }
439        _ => 0.0,
440    }
441}
442
443/// The state of drawing one path: where the pen is and what it has drawn.
444struct Pen {
445    done: Vec<SubPath>,
446    points: Vec<(f32, f32)>,
447    closed: bool,
448    fill: bool,
449    stroke: bool,
450}
451
452impl Pen {
453    fn new() -> Self {
454        Self {
455            done: Vec::new(),
456            points: Vec::new(),
457            closed: false,
458            fill: true,
459            stroke: true,
460        }
461    }
462
463    fn at(&self) -> (f32, f32) {
464        self.points.last().copied().unwrap_or((0.0, 0.0))
465    }
466
467    /// Finish the run of points in hand and begin another.
468    fn brk(&mut self) {
469        if self.points.len() >= 2 {
470            self.done.push(SubPath {
471                points: std::mem::take(&mut self.points),
472                closed: self.closed,
473                fill: self.fill,
474                stroke: self.stroke,
475            });
476        } else {
477            self.points.clear();
478        }
479        self.closed = false;
480    }
481
482    fn finish(mut self) -> Vec<SubPath> {
483        self.brk();
484        self.done
485    }
486
487    /// Walk the path, command by command.
488    fn run(&mut self, path: &str, formulas: &Formulas<'_>) {
489        let mut tokens = Tokens {
490            text: path.as_bytes(),
491            at: 0,
492            formulas,
493            pushed: None,
494        };
495        let mut command = None;
496        loop {
497            match tokens.next() {
498                Some(Token::Command(letter)) => {
499                    command = Some(letter);
500                    self.command(letter, &mut tokens);
501                }
502                // A command's arguments may repeat: `L x y x y x y` is three
503                // lines, and the letter is written once.
504                Some(Token::Number(first)) => match command {
505                    Some(letter) => {
506                        tokens.pushed = Some(first);
507                        self.command(letter, &mut tokens);
508                    }
509                    None => return,
510                },
511                None => return,
512            }
513        }
514    }
515
516    fn command(&mut self, letter: char, tokens: &mut Tokens<'_, '_>) {
517        match letter {
518            'M' => {
519                let point = tokens.point();
520                self.brk();
521                self.points.push(point);
522            }
523            'L' => {
524                let point = tokens.point();
525                self.points.push(point);
526            }
527            'C' => {
528                let (a, b, end) = (tokens.point(), tokens.point(), tokens.point());
529                self.cubic(a, b, end);
530            }
531            'Q' => {
532                let (control, end) = (tokens.point(), tokens.point());
533                // A quadratic is a cubic whose two controls sit two thirds of
534                // the way from each end towards the single one.
535                let from = self.at();
536                let third = |a: f32, b: f32| a + 2.0 / 3.0 * (b - a);
537                self.cubic(
538                    (third(from.0, control.0), third(from.1, control.1)),
539                    (third(end.0, control.0), third(end.1, control.1)),
540                    end,
541                );
542            }
543            'Z' => {
544                self.closed = true;
545                self.brk();
546            }
547            'N' => self.brk(),
548            'F' => self.fill = false,
549            'S' => self.stroke = false,
550            'T' | 'U' => {
551                let (centre, radii) = (tokens.point(), tokens.point());
552                let (from, to) = (tokens.number(), tokens.number());
553                if letter == 'U' {
554                    self.brk();
555                }
556                self.arc(centre, radii, from, to);
557            }
558            'X' | 'Y' => {
559                let to = tokens.point();
560                self.quadrant(to, letter == 'X');
561            }
562            'A' | 'B' | 'W' | 'V' => {
563                let (corner, opposite) = (tokens.point(), tokens.point());
564                let (from, to) = (tokens.point(), tokens.point());
565                if letter == 'B' || letter == 'V' {
566                    self.brk();
567                }
568                self.box_arc(corner, opposite, from, to, letter == 'W' || letter == 'V');
569            }
570            _ => {}
571        }
572    }
573
574    /// The four curve commands, which differ only in where their two control
575    /// points come from. Returns where a `smooth` curve after this one
576    /// continues from, or `None` where the data ran out mid-command.
577    fn svg_curve(
578        &mut self,
579        lower: u8,
580        scan: &mut Numbers,
581        offset: impl Fn((f32, f32)) -> (f32, f32),
582        reflected: Option<(f32, f32)>,
583    ) -> Option<(f32, f32)> {
584        let here = self.at();
585        let (first, second, end) = match lower {
586            b'c' => {
587                let (a, b, e) = (scan.point()?, scan.point()?, scan.point()?);
588                (offset(a), offset(b), offset(e))
589            }
590            b's' => {
591                let (b, e) = (scan.point()?, scan.point()?);
592                // With no curve before it the first control sits on the current
593                // point, which is what SVG says.
594                (reflected.unwrap_or(here), offset(b), offset(e))
595            }
596            b'q' => {
597                let (c, e) = (scan.point()?, scan.point()?);
598                let (c, e) = (offset(c), offset(e));
599                (quadratic(here, c), quadratic(e, c), e)
600            }
601            // A smooth quadratic, whose one control point is the last one
602            // reflected.
603            _ => {
604                let e = offset(scan.point()?);
605                let c = reflected.unwrap_or(here);
606                (quadratic(here, c), quadratic(e, c), e)
607            }
608        };
609        self.cubic(first, second, end);
610        Some((2.0 * end.0 - second.0, 2.0 * end.1 - second.1))
611    }
612
613    /// Walk SVG path data.
614    ///
615    /// A command letter is followed by as many argument groups as are written,
616    /// and a lower-case letter means its numbers are offsets from where the pen
617    /// is. After a `moveto` the implied repeat is a `lineto`, which is SVG's one
618    /// irregularity and the reason `implied` exists.
619    fn svg(&mut self, data: &str) {
620        let mut scan = Numbers {
621            text: data.as_bytes(),
622            at: 0,
623        };
624        let mut command = b' ';
625        // Where the previous curve's second control point was, reflected, which
626        // is what a smooth curve continues from.
627        let mut reflected: Option<(f32, f32)> = None;
628        let mut start = (0.0, 0.0);
629
630        loop {
631            if let Some(letter) = scan.command() {
632                command = letter;
633            } else if scan.peek_number().is_none() {
634                return;
635            }
636            let lower = command.to_ascii_lowercase();
637            let relative = command.is_ascii_lowercase();
638            let here = self.at();
639            let offset = |point: (f32, f32)| {
640                if relative {
641                    (here.0 + point.0, here.1 + point.1)
642                } else {
643                    point
644                }
645            };
646
647            match lower {
648                b'm' => {
649                    let Some(to) = scan.point() else { return };
650                    let to = offset(to);
651                    self.brk();
652                    self.points.push(to);
653                    start = to;
654                    reflected = None;
655                    // The pairs after a moveto are lines, not more moves.
656                    command = if relative { b'l' } else { b'L' };
657                }
658                b'l' => {
659                    let Some(to) = scan.point() else { return };
660                    self.points.push(offset(to));
661                    reflected = None;
662                }
663                b'h' => {
664                    let Some(x) = scan.number() else { return };
665                    let x = if relative { here.0 + x } else { x };
666                    self.points.push((x, here.1));
667                    reflected = None;
668                }
669                b'v' => {
670                    let Some(y) = scan.number() else { return };
671                    let y = if relative { here.1 + y } else { y };
672                    self.points.push((here.0, y));
673                    reflected = None;
674                }
675                b'c' | b's' | b'q' | b't' => {
676                    let Some(next) = self.svg_curve(lower, &mut scan, offset, reflected) else {
677                        return;
678                    };
679                    reflected = Some(next);
680                }
681                b'a' => {
682                    // The flags and radii are read and dropped; see `read_path`.
683                    for _ in 0..3 {
684                        if scan.number().is_none() {
685                            return;
686                        }
687                    }
688                    let (Some(_), Some(_)) = (scan.number(), scan.number()) else {
689                        return;
690                    };
691                    let Some(to) = scan.point() else { return };
692                    self.points.push(offset(to));
693                    reflected = None;
694                }
695                b'z' => {
696                    self.closed = true;
697                    self.brk();
698                    // A path may carry on after closing, from where the last
699                    // sub-path began.
700                    self.points.push(start);
701                    reflected = None;
702                }
703                _ => return,
704            }
705        }
706    }
707
708    fn cubic(&mut self, a: (f32, f32), b: (f32, f32), end: (f32, f32)) {
709        let from = self.at();
710        for step in 1..=CURVE_SEGMENTS {
711            #[allow(clippy::cast_precision_loss)]
712            let t = step as f32 / CURVE_SEGMENTS as f32;
713            let u = 1.0 - t;
714            let blend = |p0: f32, p1: f32, p2: f32, p3: f32| {
715                u * u * u * p0 + 3.0 * u * u * t * p1 + 3.0 * u * t * t * p2 + t * t * t * p3
716            };
717            self.points.push((
718                blend(from.0, a.0, b.0, end.0),
719                blend(from.1, a.1, b.1, end.1),
720            ));
721        }
722    }
723
724    /// A run of points along an ellipse, from one angle to another in degrees.
725    fn arc(&mut self, centre: (f32, f32), radii: (f32, f32), from: f32, to: f32) {
726        // A sweep that would be nothing or negative is the long way round, which
727        // is what `0 360` means and what every full ellipse in the corpus says.
728        let mut sweep = to - from;
729        if sweep <= 0.0 {
730            sweep += 360.0;
731        }
732        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
733        let steps = ((sweep / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
734        for step in 0..=steps {
735            #[allow(clippy::cast_precision_loss)]
736            let angle = (from + sweep * step as f32 / steps as f32).to_radians();
737            self.points.push((
738                centre.0 + radii.0 * angle.cos(),
739                centre.1 + radii.1 * angle.sin(),
740            ));
741        }
742    }
743
744    /// A quarter of an ellipse from where the pen is to a point.
745    ///
746    /// `X` leaves horizontally and arrives vertically, `Y` the other way round.
747    /// Between them they are how every rounded corner in ODF is written.
748    fn quadrant(&mut self, to: (f32, f32), x_first: bool) {
749        let from = self.at();
750        let centre = if x_first {
751            (to.0, from.1)
752        } else {
753            (from.0, to.1)
754        };
755        let radii = ((to.0 - from.0).abs(), (to.1 - from.1).abs());
756        if radii.0 == 0.0 || radii.1 == 0.0 {
757            self.points.push(to);
758            return;
759        }
760        let angle_of = |p: (f32, f32)| (p.1 - centre.1).atan2(p.0 - centre.0).to_degrees();
761        let (start, end) = (angle_of(from), angle_of(to));
762        // The quarter that joins the two, taken the short way.
763        let mut sweep = end - start;
764        while sweep > 180.0 {
765            sweep -= 360.0;
766        }
767        while sweep < -180.0 {
768            sweep += 360.0;
769        }
770        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
771        let steps = ((sweep.abs() / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
772        for step in 1..=steps {
773            #[allow(clippy::cast_precision_loss)]
774            let angle = (start + sweep * step as f32 / steps as f32).to_radians();
775            self.points.push((
776                centre.0 + radii.0 * angle.cos(),
777                centre.1 + radii.1 * angle.sin(),
778            ));
779        }
780    }
781
782    /// An arc of the ellipse that fills a box, between two points on it.
783    ///
784    /// **The sweep direction here is the specification's and not a measurement.**
785    /// `V` occurs four times in the twenty-three templates surveyed and `A`, `B`
786    /// and `W` occur in none of them, so nothing in the corpus tells these apart.
787    fn box_arc(
788        &mut self,
789        corner: (f32, f32),
790        opposite: (f32, f32),
791        from: (f32, f32),
792        to: (f32, f32),
793        clockwise: bool,
794    ) {
795        let centre = (
796            f32::midpoint(corner.0, opposite.0),
797            f32::midpoint(corner.1, opposite.1),
798        );
799        let radii = (
800            (opposite.0 - corner.0).abs() / 2.0,
801            (opposite.1 - corner.1).abs() / 2.0,
802        );
803        if radii.0 == 0.0 || radii.1 == 0.0 {
804            self.points.push(to);
805            return;
806        }
807        let angle_of = |p: (f32, f32)| {
808            ((p.1 - centre.1) / radii.1)
809                .atan2((p.0 - centre.0) / radii.0)
810                .to_degrees()
811        };
812        let (start, end) = (angle_of(from), angle_of(to));
813        let sweep = if clockwise { start - end } else { end - start };
814        let sweep = if sweep <= 0.0 { sweep + 360.0 } else { sweep };
815        let (a, b) = if clockwise {
816            (start, start - sweep)
817        } else {
818            (start, start + sweep)
819        };
820        self.arc_between(centre, radii, a, b);
821    }
822
823    fn arc_between(&mut self, centre: (f32, f32), radii: (f32, f32), from: f32, to: f32) {
824        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
825        let steps = (((to - from).abs() / DEGREES_PER_SEGMENT).ceil() as usize).max(2);
826        for step in 0..=steps {
827            #[allow(clippy::cast_precision_loss)]
828            let angle = (from + (to - from) * step as f32 / steps as f32).to_radians();
829            self.points.push((
830                centre.0 + radii.0 * angle.cos(),
831                centre.1 + radii.1 * angle.sin(),
832            ));
833        }
834    }
835}
836
837/// A quadratic curve's single control point, as one of a cubic's two: two
838/// thirds of the way from the end towards it.
839fn quadratic(end: (f32, f32), control: (f32, f32)) -> (f32, f32) {
840    (
841        end.0 + 2.0 / 3.0 * (control.0 - end.0),
842        end.1 + 2.0 / 3.0 * (control.1 - end.1),
843    )
844}
845
846/// SVG path data, read one number or command at a time.
847///
848/// SVG separates numbers with whitespace, with a comma, or with nothing at all
849/// where the next one begins unambiguously: `0-571` is two numbers and so is
850/// `.5.5`. That is why this is a scanner and not a `split_whitespace`.
851struct Numbers<'a> {
852    text: &'a [u8],
853    at: usize,
854}
855
856impl Numbers<'_> {
857    fn skip(&mut self) {
858        while self
859            .text
860            .get(self.at)
861            .is_some_and(|b| b.is_ascii_whitespace() || *b == b',')
862        {
863            self.at += 1;
864        }
865    }
866
867    /// The next command letter, if one is there.
868    fn command(&mut self) -> Option<u8> {
869        self.skip();
870        let byte = *self.text.get(self.at)?;
871        if byte.is_ascii_alphabetic() && !matches!(byte, b'e' | b'E') {
872            self.at += 1;
873            return Some(byte);
874        }
875        None
876    }
877
878    fn peek_number(&mut self) -> Option<u8> {
879        self.skip();
880        self.text
881            .get(self.at)
882            .copied()
883            .filter(|b| b.is_ascii_digit() || matches!(b, b'-' | b'+' | b'.'))
884    }
885
886    fn number(&mut self) -> Option<f32> {
887        self.peek_number()?;
888        let start = self.at;
889        if matches!(self.text.get(self.at), Some(b'-' | b'+')) {
890            self.at += 1;
891        }
892        let mut seen_point = false;
893        while let Some(byte) = self.text.get(self.at) {
894            match byte {
895                b'0'..=b'9' => self.at += 1,
896                // A second point begins the next number: `.5.5` is two.
897                b'.' if !seen_point => {
898                    seen_point = true;
899                    self.at += 1;
900                }
901                b'e' | b'E' => {
902                    self.at += 1;
903                    if matches!(self.text.get(self.at), Some(b'-' | b'+')) {
904                        self.at += 1;
905                    }
906                }
907                _ => break,
908            }
909        }
910        String::from_utf8_lossy(&self.text[start..self.at])
911            .parse()
912            .ok()
913    }
914
915    fn point(&mut self) -> Option<(f32, f32)> {
916        Some((self.number()?, self.number()?))
917    }
918}
919
920enum Token {
921    Command(char),
922    Number(f32),
923}
924
925/// The path, read one token at a time, with every reference already resolved.
926struct Tokens<'a, 'f> {
927    text: &'a [u8],
928    at: usize,
929    formulas: &'a Formulas<'f>,
930    // Set when a repeated argument group put a number back.
931    pushed: Option<f32>,
932}
933
934impl Tokens<'_, '_> {
935    fn next(&mut self) -> Option<Token> {
936        if let Some(number) = self.pushed.take() {
937            return Some(Token::Number(number));
938        }
939        while self
940            .text
941            .get(self.at)
942            .is_some_and(|b| b.is_ascii_whitespace() || *b == b',')
943        {
944            self.at += 1;
945        }
946        let byte = *self.text.get(self.at)?;
947        if byte.is_ascii_alphabetic() {
948            self.at += 1;
949            return Some(Token::Command(char::from(byte)));
950        }
951        Some(Token::Number(self.value()))
952    }
953
954    /// One number, which may be written as a reference to a formula or to a
955    /// modifier rather than as digits.
956    fn value(&mut self) -> f32 {
957        let mut expression = Expression {
958            text: self.text,
959            at: self.at,
960            formulas: self.formulas,
961        };
962        // A path's numbers are single values and never arithmetic, so a factor
963        // is the whole of what may appear — and `?f7` and `$0` are factors.
964        let value = expression.factor();
965        self.at = expression.at;
966        value
967    }
968
969    fn number(&mut self) -> f32 {
970        match self.next() {
971            Some(Token::Number(number)) => number,
972            // A command short of its arguments: nought keeps the pen somewhere
973            // rather than ending the shape.
974            _ => 0.0,
975        }
976    }
977
978    fn point(&mut self) -> (f32, f32) {
979        (self.number(), self.number())
980    }
981}