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}