Skip to main content

fmd_math/
mbox.rs

1//! The layout node model — the engine shape the G0-3 ratification froze —
2//! and the public output types of the frozen API sketch.
3//!
4//! Two layers:
5//!
6//! 1. The **public output**: [`Layout`] — flat, positioned
7//!    [`PlacedGlyph`]/[`PlacedRule`]/[`PlacedPath`] lists in em units, y-up,
8//!    baseline at 0 — is what consumers (franken_manim's fmn-tex; fmd's
9//!    HTML/PDF renderers) read. Every glyph names its face (multi-face
10//!    layout is structural: `∑ ∫ ∏` resolve through the math-symbol
11//!    fallback face) and carries its source span (§11.3 span provenance).
12//! 2. The **internal node model**: [`MBox`]/[`MNode`] — boxes, glue, and
13//!    kerns, built bottom-up by the Appendix-G constructions and flattened
14//!    into a [`Layout`] at the end. Fixed here (this bead) so the placement
15//!    mathematics and the extension beads build on one shape.
16//!
17//! `typeset` itself lands with the placement bead (fm-hk9); this module
18//! fixes the shapes both sides compile against.
19
20use crate::node::Span;
21
22/// Identifies one of the faces handed to the engine (an index into the
23/// engine's face list, in construction order). Multi-face layout is
24/// structural: face selection is data on every glyph, never a rendering
25/// afterthought.
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
27pub struct FaceId(pub usize);
28
29/// A positioned glyph in the final layout. Units are ems of the base
30/// (text-style) size; `y` is the baseline-relative vertical position,
31/// positive up; `size` is the glyph's own size factor (1.0 / 0.7 / 0.5 per
32/// style).
33#[derive(Clone, Debug, PartialEq)]
34pub struct PlacedGlyph {
35    /// The face the glyph comes from.
36    pub face: FaceId,
37    /// Glyph id in that face.
38    pub gid: u16,
39    /// The character the glyph renders.
40    pub ch: char,
41    /// Horizontal position of the glyph origin, in ems.
42    pub x: f64,
43    /// Baseline-relative vertical position, in ems, y-up.
44    pub y: f64,
45    /// Size factor relative to the base size.
46    pub size: f64,
47    /// Source span of the construct that produced the glyph.
48    pub span: Span,
49}
50
51/// A positioned rectangular rule (fraction bars, `\overline`s, radical
52/// overbars). `x`/`y` name the rule's left-bottom corner in ems, y-up.
53#[derive(Clone, Debug, PartialEq)]
54pub struct PlacedRule {
55    /// Left edge, ems.
56    pub x: f64,
57    /// Bottom edge, baseline-relative ems, y-up.
58    pub y: f64,
59    /// Width, ems.
60    pub width: f64,
61    /// Height (thickness), ems.
62    pub height: f64,
63    /// Source span of the construct that produced the rule.
64    pub span: Span,
65}
66
67/// One segment of a drawn-path contour.
68#[derive(Clone, Copy, Debug, PartialEq)]
69pub enum PathSeg {
70    /// A straight line to the endpoint.
71    Line {
72        /// Endpoint, ems.
73        to: (f64, f64),
74    },
75    /// A quadratic Bézier through one control point.
76    Quad {
77        /// Control point, ems.
78        ctrl: (f64, f64),
79        /// Endpoint, ems.
80        to: (f64, f64),
81    },
82}
83
84/// One closed contour of a drawn path: a start point plus segments; the
85/// contour closes back to the start implicitly.
86#[derive(Clone, Debug, PartialEq)]
87pub struct PathContour {
88    /// Start point, ems.
89    pub start: (f64, f64),
90    /// The segments.
91    pub segments: Vec<PathSeg>,
92}
93
94/// A positioned drawn-path construction (parametric delimiters past the
95/// glyph-scaling threshold, the drawn radical, braces): quadratic contours
96/// in ems, y-up, the same path model franken_manim's geometry kernel and
97/// fmd-font outlines share.
98#[derive(Clone, Debug, PartialEq)]
99pub struct PlacedPath {
100    /// The closed contours.
101    pub contours: Vec<PathContour>,
102    /// Source span of the construct that produced the path.
103    pub span: Span,
104}
105
106/// The final layout of a formula: flat positioned primitives plus overall
107/// metrics. Everything is in ems of the base size, y-up, baseline at 0;
108/// `width` spans the whole formula, `height` rises above the baseline,
109/// `depth` extends below it (a positive number).
110#[derive(Clone, Debug, PartialEq, Default)]
111pub struct Layout {
112    /// Every glyph, positioned.
113    pub glyphs: Vec<PlacedGlyph>,
114    /// Every rule, positioned.
115    pub rules: Vec<PlacedRule>,
116    /// Every drawn path, positioned.
117    pub paths: Vec<PlacedPath>,
118    /// Total advance width, ems.
119    pub width: f64,
120    /// Extent above the baseline, ems.
121    pub height: f64,
122    /// Extent below the baseline, ems (positive).
123    pub depth: f64,
124}
125
126// ── The internal node model (crate-visible; the placement bead's input) ──
127//
128// The `allow(dead_code)` on these items is deliberate and temporary: the
129// engine shape is fixed by THIS bead (franken_manim fm-wgl) so the
130// Appendix-G placement bead (fm-hk9) builds on it; until that bead lands,
131// the lib target only reaches the model through its tests.
132
133/// TeX-style glue: a natural width with stretch and shrink allowances, in
134/// ems. The inter-atom spaces of the spacing table are glue (thin space
135/// stretches, medium and thick spaces stretch and shrink per plain TeX);
136/// this bead records natural widths, and the placement bead applies
137/// stretching when alignment demands it.
138#[allow(dead_code)]
139#[derive(Clone, Copy, Debug, PartialEq, Default)]
140pub(crate) struct GlueSpec {
141    /// Natural width, ems.
142    pub(crate) natural: f64,
143    /// Stretch allowance, ems.
144    pub(crate) stretch: f64,
145    /// Shrink allowance, ems.
146    pub(crate) shrink: f64,
147}
148
149/// A child of a box, positioned relative to the box's origin (its left
150/// edge, on its baseline).
151#[allow(dead_code)]
152#[derive(Clone, Debug, PartialEq)]
153pub(crate) struct Positioned<T> {
154    /// Horizontal offset from the box origin, ems.
155    pub(crate) dx: f64,
156    /// Vertical offset from the box baseline, ems, y-up.
157    pub(crate) dy: f64,
158    /// The child.
159    pub(crate) node: T,
160}
161
162/// One node of the internal layout tree.
163#[allow(dead_code)]
164#[derive(Clone, Debug, PartialEq)]
165pub(crate) enum MNode {
166    /// A glyph at the box-local origin.
167    Glyph {
168        face: FaceId,
169        gid: u16,
170        ch: char,
171        size: f64,
172        span: Span,
173    },
174    /// A rule.
175    Rule { width: f64, height: f64, span: Span },
176    /// A drawn path.
177    Path {
178        contours: Vec<PathContour>,
179        span: Span,
180    },
181    /// A fixed horizontal advance.
182    Kern(f64),
183    /// Stretchable space.
184    Glue(GlueSpec),
185    /// A nested box.
186    Box(MBox),
187}
188
189/// How a box stacks its children.
190#[allow(dead_code)]
191#[derive(Clone, Copy, Debug, PartialEq, Eq)]
192pub(crate) enum BoxKind {
193    /// Children advance horizontally along the baseline.
194    Horizontal,
195    /// Children stack vertically (the Appendix-G constructions).
196    Vertical,
197}
198
199/// A layout box: dimensions plus positioned children. The Appendix-G
200/// constructions build these bottom-up; flattening walks the tree
201/// accumulating offsets into a [`Layout`].
202#[allow(dead_code)]
203#[derive(Clone, Debug, PartialEq)]
204pub(crate) struct MBox {
205    pub(crate) kind: BoxKind,
206    /// Advance width, ems.
207    pub(crate) width: f64,
208    /// Extent above the baseline, ems.
209    pub(crate) height: f64,
210    /// Extent below the baseline, ems, positive.
211    pub(crate) depth: f64,
212    /// The children.
213    pub(crate) children: Vec<Positioned<MNode>>,
214}
215
216#[allow(dead_code)]
217impl MBox {
218    /// An empty box of the given kind.
219    pub(crate) fn empty(kind: BoxKind) -> Self {
220        Self {
221            kind,
222            width: 0.0,
223            height: 0.0,
224            depth: 0.0,
225            children: Vec::new(),
226        }
227    }
228
229    /// Flatten the box tree into a [`Layout`], accumulating offsets from
230    /// `(x, y)`.
231    pub(crate) fn flatten_into(&self, x: f64, y: f64, out: &mut Layout) {
232        for child in &self.children {
233            let cx = x + child.dx;
234            let cy = y + child.dy;
235            match &child.node {
236                MNode::Glyph {
237                    face,
238                    gid,
239                    ch,
240                    size,
241                    span,
242                } => out.glyphs.push(PlacedGlyph {
243                    face: *face,
244                    gid: *gid,
245                    ch: *ch,
246                    x: cx,
247                    y: cy,
248                    size: *size,
249                    span: *span,
250                }),
251                MNode::Rule {
252                    width,
253                    height,
254                    span,
255                } => out.rules.push(PlacedRule {
256                    x: cx,
257                    y: cy,
258                    width: *width,
259                    height: *height,
260                    span: *span,
261                }),
262                MNode::Path { contours, span } => {
263                    let moved = contours
264                        .iter()
265                        .map(|c| PathContour {
266                            start: (c.start.0 + cx, c.start.1 + cy),
267                            segments: c
268                                .segments
269                                .iter()
270                                .map(|s| match s {
271                                    PathSeg::Line { to } => PathSeg::Line {
272                                        to: (to.0 + cx, to.1 + cy),
273                                    },
274                                    PathSeg::Quad { ctrl, to } => PathSeg::Quad {
275                                        ctrl: (ctrl.0 + cx, ctrl.1 + cy),
276                                        to: (to.0 + cx, to.1 + cy),
277                                    },
278                                })
279                                .collect(),
280                        })
281                        .collect();
282                    out.paths.push(PlacedPath {
283                        contours: moved,
284                        span: *span,
285                    });
286                }
287                MNode::Kern(_) | MNode::Glue(_) => {}
288                MNode::Box(inner) => inner.flatten_into(cx, cy, out),
289            }
290        }
291    }
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn flatten_covers_the_whole_node_model() {
300        // A vertical construction holding a rule, a drawn path, a kern, and
301        // glue: kerns and glue contribute spacing only (they must not emit
302        // primitives), everything else translates by the accumulated offset.
303        let mut vbox = MBox::empty(BoxKind::Vertical);
304        vbox.width = 1.0;
305        vbox.height = 1.0;
306        vbox.children = vec![
307            Positioned {
308                dx: 0.1,
309                dy: 0.4,
310                node: MNode::Rule {
311                    width: 0.8,
312                    height: 0.04,
313                    span: Span::new(0, 4),
314                },
315            },
316            Positioned {
317                dx: 0.0,
318                dy: 0.0,
319                node: MNode::Kern(0.25),
320            },
321            Positioned {
322                dx: 0.0,
323                dy: 0.0,
324                node: MNode::Glue(GlueSpec {
325                    natural: 3.0 / 18.0,
326                    stretch: 1.5 / 18.0,
327                    shrink: 1.0 / 18.0,
328                }),
329            },
330            Positioned {
331                dx: 0.2,
332                dy: -0.3,
333                node: MNode::Path {
334                    contours: vec![PathContour {
335                        start: (0.0, 0.0),
336                        segments: vec![
337                            PathSeg::Line { to: (0.5, 0.0) },
338                            PathSeg::Quad {
339                                ctrl: (0.5, 0.5),
340                                to: (0.0, 0.5),
341                            },
342                        ],
343                    }],
344                    span: Span::new(4, 9),
345                },
346            },
347        ];
348        let mut layout = Layout::default();
349        MBox {
350            kind: BoxKind::Horizontal,
351            width: 2.0,
352            height: 1.0,
353            depth: 0.0,
354            children: vec![Positioned {
355                dx: 1.0,
356                dy: 0.5,
357                node: MNode::Box(vbox),
358            }],
359        }
360        .flatten_into(0.0, 0.0, &mut layout);
361        assert!(layout.glyphs.is_empty());
362        assert_eq!(layout.rules.len(), 1);
363        assert_eq!(layout.rules[0].x, 1.1);
364        assert_eq!(layout.rules[0].y, 0.9);
365        assert_eq!(layout.paths.len(), 1);
366        assert_eq!(layout.paths[0].contours[0].start, (1.2, 0.2));
367        match layout.paths[0].contours[0].segments[1] {
368            PathSeg::Quad { ctrl, to } => {
369                assert_eq!(ctrl, (1.7, 0.7));
370                assert_eq!(to, (1.2, 0.7));
371            }
372            PathSeg::Line { .. } => unreachable!("second segment is the quad"),
373        }
374    }
375
376    #[test]
377    fn flatten_accumulates_offsets() {
378        let inner = MBox {
379            kind: BoxKind::Horizontal,
380            width: 1.0,
381            height: 0.7,
382            depth: 0.0,
383            children: vec![Positioned {
384                dx: 0.25,
385                dy: 0.0,
386                node: MNode::Glyph {
387                    face: FaceId(0),
388                    gid: 7,
389                    ch: 'x',
390                    size: 1.0,
391                    span: Span::new(0, 1),
392                },
393            }],
394        };
395        let outer = MBox {
396            kind: BoxKind::Horizontal,
397            width: 2.0,
398            height: 0.7,
399            depth: 0.0,
400            children: vec![Positioned {
401                dx: 1.0,
402                dy: 0.5,
403                node: MNode::Box(inner),
404            }],
405        };
406        let mut layout = Layout::default();
407        outer.flatten_into(0.0, 0.0, &mut layout);
408        assert_eq!(layout.glyphs.len(), 1);
409        assert_eq!(layout.glyphs[0].x, 1.25);
410        assert_eq!(layout.glyphs[0].y, 0.5);
411    }
412}