Skip to main content

fmd_math/
paths.rs

1//! Output paths: resolve a [`Layout`] into pure quadratic contours, and
2//! dump them canonically for goldens.
3//!
4//! Glyph outlines come from fmd-font's glyf decoder — already quadratic —
5//! and are scaled from font units to ems and translated to their placed
6//! positions; rules become four-line rectangle contours; drawn paths pass
7//! through. The result is the shared path model every consumer speaks
8//! (franken_manim's geometry kernel builds VMobjects from it; fmd renders
9//! it to HTML/PDF vectors).
10//!
11//! **Determinism.** The transform is pure f64 multiply/add over the
12//! decoded integer coordinates, and the canonical dump prints fixed
13//! six-decimal fixed-point — same string + faces ⇒ identical bytes, on
14//! every platform.
15
16use crate::error::MathError;
17use crate::layout::Engine;
18use crate::mbox::{Layout, PathContour, PathSeg};
19use crate::node::Span;
20
21/// Resolve every primitive of a layout into closed quadratic contours, in
22/// ems, y-up, baseline at 0.
23///
24/// # Errors
25///
26/// [`MathError::UnmappedChar`] if a placed glyph's outline cannot be
27/// decoded (a face-table corruption surfaces as the glyph's character).
28pub fn resolve_paths(engine: &Engine, layout: &Layout) -> Result<Vec<PathContour>, MathError> {
29    let mut out = Vec::new();
30    for glyph in &layout.glyphs {
31        let Some(font) = engine.faces().font(glyph.face) else {
32            return Err(MathError::UnmappedChar {
33                ch: glyph.ch,
34                span: glyph.span,
35            });
36        };
37        let outline = font
38            .glyph_outline(glyph.gid)
39            .map_err(|_| MathError::UnmappedChar {
40                ch: glyph.ch,
41                span: glyph.span,
42            })?;
43        let upm = f64::from(font.units_per_em.max(1));
44        let s = glyph.size / upm;
45        for contour in &outline.contours {
46            let mut segments = Vec::with_capacity(contour.segments.len());
47            let start = (glyph.x + contour.start.x * s, glyph.y + contour.start.y * s);
48            for seg in &contour.segments {
49                segments.push(match seg {
50                    fmd_font::outline::Segment::Line { to } => PathSeg::Line {
51                        to: (glyph.x + to.x * s, glyph.y + to.y * s),
52                    },
53                    fmd_font::outline::Segment::Quad { ctrl, to } => PathSeg::Quad {
54                        ctrl: (glyph.x + ctrl.x * s, glyph.y + ctrl.y * s),
55                        to: (glyph.x + to.x * s, glyph.y + to.y * s),
56                    },
57                });
58            }
59            out.push(PathContour { start, segments });
60        }
61    }
62    for rule in &layout.rules {
63        let x0 = rule.x;
64        let y0 = rule.y;
65        let x1 = rule.x + rule.width;
66        let y1 = rule.y + rule.height;
67        out.push(PathContour {
68            start: (x0, y0),
69            segments: vec![
70                PathSeg::Line { to: (x1, y0) },
71                PathSeg::Line { to: (x1, y1) },
72                PathSeg::Line { to: (x0, y1) },
73                PathSeg::Line { to: (x0, y0) },
74            ],
75        });
76    }
77    for path in &layout.paths {
78        out.extend(path.contours.iter().cloned());
79    }
80    Ok(out)
81}
82
83/// The canonical text dump of resolved contours: one line per element,
84/// fixed six-decimal coordinates — the golden format, byte-stable across
85/// platforms and runs.
86#[must_use]
87pub fn canonical_dump(contours: &[PathContour]) -> String {
88    use core::fmt::Write as _;
89    let mut out = String::new();
90    for (i, contour) in contours.iter().enumerate() {
91        let _ = writeln!(
92            out,
93            "contour {} start {:.6} {:.6}",
94            i, contour.start.0, contour.start.1
95        );
96        for seg in &contour.segments {
97            match seg {
98                PathSeg::Line { to } => {
99                    let _ = writeln!(out, "  line {:.6} {:.6}", to.0, to.1);
100                }
101                PathSeg::Quad { ctrl, to } => {
102                    let _ = writeln!(
103                        out,
104                        "  quad {:.6} {:.6} {:.6} {:.6}",
105                        ctrl.0, ctrl.1, to.0, to.1
106                    );
107                }
108            }
109        }
110    }
111    out
112}
113
114/// A compact structural dump of a layout (glyph/rule inventory + metrics),
115/// for goldens that want placement without full outlines.
116#[must_use]
117pub fn layout_dump(layout: &Layout) -> String {
118    use core::fmt::Write as _;
119    let mut out = String::new();
120    let _ = writeln!(
121        out,
122        "layout w {:.6} h {:.6} d {:.6}",
123        layout.width, layout.height, layout.depth
124    );
125    for g in &layout.glyphs {
126        let _ = writeln!(
127            out,
128            "glyph face {} gid {} ch U+{:04X} x {:.6} y {:.6} size {:.6} span {}..{}",
129            g.face.0, g.gid, g.ch as u32, g.x, g.y, g.size, g.span.start, g.span.end
130        );
131    }
132    for r in &layout.rules {
133        let _ = writeln!(
134            out,
135            "rule x {:.6} y {:.6} w {:.6} h {:.6} span {}..{}",
136            r.x, r.y, r.width, r.height, r.span.start, r.span.end
137        );
138    }
139    for p in &layout.paths {
140        let _ = writeln!(
141            out,
142            "path contours {} span {}..{}",
143            p.contours.len(),
144            p.span.start,
145            p.span.end
146        );
147    }
148    out
149}
150
151/// Every primitive's span must sit inside the source string — the §11.3
152/// provenance invariant, checkable by consumers.
153#[must_use]
154pub fn spans_cover(layout: &Layout, source_len: usize) -> bool {
155    let ok = |s: &Span| s.end <= source_len && s.start <= s.end;
156    layout.glyphs.iter().all(|g| ok(&g.span))
157        && layout.rules.iter().all(|r| ok(&r.span))
158        && layout.paths.iter().all(|p| ok(&p.span))
159}