//! Layout geometry over a fixed formula corpus (`FakeMetrics`: width 0.5·size, ascent //! 0.75·size, descent 0.25·size, x-height 0.5·size, rule 0.04·size), all at 10 pt. use notescraft_math::{Error, Style, layout, parse, testutil::FakeMetrics}; fn lb(src: &str, size: f32, display: bool) -> notescraft_math::MathBox { match parse(src) { Ok(node) => layout(&node, &FakeMetrics, Style { size_pt: size, display }), Err(e) => panic!("{src:?}: {e}"), } } fn near(a: f32, b: f32) -> bool { (a - b).abs() < 1.0e-3 } fn count_items(b: ¬escraft_math::MathBox, is_path: bool) -> usize { b.items.iter().filter(|i| matches!(i, notescraft_math::Item::Path { .. }) == is_path).count() } #[test] fn atoms_and_spacing() { let cases: &[(&str, f32)] = &[ ("x", 5.0), ("xy", 10.0), ("a+b", 19.4444), ("a = b", 20.5556), ("a/b", 15.0), ("(x)", 15.0), ("a,b", 15.0), ("a\\,b", 11.6667), ("a\\:b", 12.2222), ("a\\;b", 12.7778), ("a\\!b", 8.3333), ("a\\quad b", 20.0), ("a\\qquad b", 30.0), ("{a}{b}", 10.0), ("\\infty", 5.0), ("\\alpha", 5.0), ("\\alpha + \\beta", 19.4444), ("a\\cdot b", 19.4444), ("f(x)", 20.0), ("x^2y", 14.0), ("1+2", 19.4444), ]; for (src, w) in cases { let b = lb(src, 10.0, false); assert!(near(b.width, *w), "{src}: {}", b.width); } } #[test] fn scripts_geometry() { let b = lb("x^2", 10.0, false); assert!(near(b.width, 9.0)); assert!(near(b.height, 9.0)); assert!(near(b.depth, 2.5)); let b = lb("x_2^3", 10.0, false); assert!(near(b.width, 9.0)); // Both scripts move apart until their ink boxes have four rule widths of clearance. assert!(near(b.depth, 4.8)); let b = lb("x'", 10.0, false); assert!(near(b.width, 9.0)); let b = lb("\\sum_{i}^{n}", 10.0, false); // Inline sum: scripts beside, with operator correction and vertical clearance. assert!(near(b.width, 9.0)); assert!(near(b.height, 10.7375)); assert!(near(b.depth, 4.8625)); } #[test] fn big_operators_limits_display() { let b = lb("\\sum", 10.0, true); assert!(near(b.width, 7.0)); let b = lb("\\sum_{i}", 10.0, true); assert!(near(b.width, 7.0)); assert!(near(b.depth, 12.5)); let b = lb("\\sum_{i}^{n}", 10.0, true); assert!(near(b.width, 7.0)); assert!(near(b.height, 17.5)); assert!(near(b.depth, 12.5)); let b = lb("\\int_0^1", 10.0, true); // ∫ keeps scripts beside even in display style. assert!(near(b.width, 11.0)); assert!(near(b.depth, 6.25)); let b = lb("\\lim_{a}^{b}", 10.0, true); assert!(near(b.width, 15.0), "{}", b.width); assert!(near(b.height, 15.5)); let b = lb("\\max_{c}", 10.0, true); assert!(near(b.width, 15.0)); assert!(near(b.depth, 10.5)); } #[test] fn binomial_and_sine_examples_typeset() -> Result<(), Error> { for source in [r"\sum_{k=0}^{n} \binom{n}{k} x^k = (1+x)^n", r"\lim_{x\to 0}\frac{\sin x}{x}=1"] { let node = parse(source)?; let math = layout(&node, &FakeMetrics, Style { size_pt: 10.0, display: true }); assert!(math.width > 0.0 && math.height > 0.0); } let node = parse(r"\binom{n}{k}")?; let math = layout(&node, &FakeMetrics, Style { size_pt: 10.0, display: true }); assert_eq!(math.items.iter().filter(|item| matches!(item, notescraft_math::Item::Rule { .. })).count(), 0); let node = parse(r"\sin^2 x")?; let math = layout(&node, &FakeMetrics, Style { size_pt: 10.0, display: true }); assert!(math.items.iter().any(|item| matches!(item, notescraft_math::Item::Glyphs { text, x, .. } if text == "2" && *x >= 15.0))); Ok(()) } #[test] fn fractions_geometry() { let cases: &[(&str, bool, f32)] = &[("\\frac12", false, 3.5), ("\\frac{a}{b}", false, 3.5), ("\\frac12", true, 5.0)]; for (src, display, w) in cases { let b = lb(src, 10.0, *display); // Inline `\frac` children run at the script size; display at full size. assert!(near(b.width, *w), "{src}: {}", b.width); } let b = lb("\\frac12", 10.0, false); assert!(near(b.height, 10.5)); assert!(near(b.depth, 10.5)); let b = lb("\\dfrac{a}{b}", 10.0, false); assert!(near(b.width, 5.0)); assert!(near(b.height, 13.5)); assert!(near(b.depth, 13.5)); let b = lb("\\tfrac{a}{b}", 10.0, true); assert!(near(b.width, 5.0)); assert!(near(b.height, 13.5)); let b = lb("\\frac{\\sqrt{a}}{2}", 10.0, false); // Sqrt at script size 7: rad max(0.75·(5.25+0.56), 3.5) = 4.3575 + 3.5 wide. assert!(near(b.width, 7.8575), "{}", b.width); } #[test] fn radicals_geometry() { let b = lb("2\\sqrt3", 10.0, false); // '2' + sqrt('3'): rad max(0.75·8.3, 5) = 6.225 + body 5. assert!(near(b.width, 5.0 + 11.225), "{}", b.width); let b = lb("\\sqrt{ab}", 10.0, false); assert!(near(b.width, 16.225)); assert!(near(b.height, 8.3)); assert_eq!(count_items(&b, true), 1); assert_eq!(count_items(&b, false), 2); let b = lb("\\sqrt[3]{a}", 10.0, false); assert!(near(b.height, 15.05)); let b = lb("\\sqrt{\\frac{a}{b}}", 10.0, false); // Body fraction (children at 0.7 → 3.5 wide): h 11.3; rad 8.475 + 3.5. assert!(near(b.width, 11.975), "{}", b.width); assert!(near(b.height, 11.3), "{}", b.height); assert!(near(b.depth, 10.5)); } #[test] fn text_and_fonts() { let b = lb("\\text{hi}", 10.0, false); assert!(near(b.width, 10.0)); let b = lb("\\text{a b}", 10.0, false); assert!(near(b.width, 15.0)); let b = lb("\\mathrm{x}", 10.0, false); assert!(near(b.width, 5.0)); let runs: Vec<_> = b .items .iter() .filter_map(|i| match i { notescraft_math::Item::Glyphs { face, .. } => Some(*face), _ => None, }) .collect(); assert_eq!(runs.len(), 1); let b = lb("\\mathbf{x}", 10.0, false); assert!(near(b.width, 5.0)); let b = lb("\\mathbb{R}", 10.0, false); let text: Vec<_> = b .items .iter() .filter_map(|i| match i { notescraft_math::Item::Glyphs { text, .. } => Some(text.as_str()), _ => None, }) .collect(); assert_eq!(text, ["ℝ"]); let b = lb("\\mathcal{F}", 10.0, false); assert!(near(b.width, 5.0)); let b = lb("\\mathit{x}", 10.0, false); assert!(near(b.width, 5.0)); } #[test] fn accents_geometry() { for src in ["\\hat{x}", "\\bar{x}", "\\vec{v}", "\\dot{x}", "\\tilde{x}", "\\ddot{x}"] { let b = lb(src, 10.0, false); assert!(near(b.width, 5.0), "{src}: {}", b.width); assert!(near(b.height, 8.5 + 5.25), "{src}: {}", b.height); assert!(near(b.depth, 2.5)); assert_eq!(b.items.len(), 2); } } #[test] fn delimiters_geometry() { let b = lb("\\left(x\\right)", 10.0, false); assert!(near(b.width, 15.0)); let b = lb("\\left( x+y \\right)", 10.0, false); // Body fits a normal glyph extent → glyph delimiters, no paths. assert!(near(b.width, 29.4444), "{}", b.width); assert_eq!(count_items(&b, true), 0); let b = lb("\\left(x^2\\right)", 10.0, false); // Body 9 tall > ascent 7.5 → stretched parens: 0.35·(9+2.5) each. assert!(near(b.width, 17.05), "{}", b.width); assert_eq!(count_items(&b, true), 2); let b = lb("\\left|f\\right|", 10.0, false); assert!(near(b.width, 15.0)); let b = lb("\\left. a \\right.", 10.0, false); assert!(near(b.width, 5.0)); let b = lb("\\left\\langle a\\right\\rangle", 10.0, false); assert!(near(b.width, 15.0)); let b = lb("\\left(\\frac{a}{b}\\right)", 10.0, true); assert!(near(b.width, 23.9), "{}", b.width); assert_eq!(count_items(&b, true), 2); } #[test] fn matrices_geometry() { let b = lb("\\begin{matrix} a \\\\ b \\end{matrix}", 10.0, false); assert!(near(b.width, 5.0)); assert!(near(b.height, 14.0)); assert!(near(b.depth, 9.0)); let b = lb("\\begin{matrix} a & b \\\\ c & d \\end{matrix}", 10.0, false); assert!(near(b.width, 13.0)); assert!(near(b.height, 14.0)); assert!(near(b.depth, 9.0)); let b = lb("\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix}", 10.0, false); assert!(near(b.width, 29.1), "{}", b.width); let b = lb("\\begin{bmatrix} a \\\\ b \\end{bmatrix}", 10.0, false); assert_eq!(count_items(&b, true), 2); let b = lb("\\begin{cases} a \\\\ b \\end{cases}", 10.0, false); assert_eq!(count_items(&b, true), 1); let b = lb("\\begin{aligned} a &= b \\\\ c &= d \\end{aligned}", 10.0, false); let xs: Vec<(f32, f32)> = b .items .iter() .filter_map(|i| match i { notescraft_math::Item::Glyphs { text, x, baseline_y, .. } if text == "=" => Some((*x, *baseline_y)), _ => None, }) .collect(); // Both `=` sit at the same cell origin (right-aligned first column); baselines // differ per row. assert_eq!(xs.len(), 2); assert!(near(xs[0].0, 8.0), "{xs:?}"); assert!(near(xs[0].0, xs[1].0)); } #[test] fn styles_scale_layout() { let b = lb("x", 20.0, false); assert!(near(b.width, 10.0)); let b = lb("\\frac{a}{b}", 5.0, true); // Display-style frac children stay at full size → 2.5 wide. assert!(near(b.width, 2.5)); // Script sizes: 0.7 and 0.5 ladders inside nested scripts. let b = lb("x^{y^{z}}", 10.0, false); // x + corr + y at 0.7 (5.6 wide with sup z at 0.5 of script size). assert!(near(b.width, 11.1), "{}", b.width); assert!(near(b.height, 9.0), "{}", b.height); } #[test] fn bad_style_falls_back() { for size in [f32::NAN, 0.0, -3.0, f32::INFINITY] { let good = lb("x", 12.0, false); let b = lb("x", size, false); assert_eq!(b.width, good.width); } } #[test] fn corpus_of_valid_formulas_all_layout() { let corpus = [ "x+y", "\\frac{a+b}{c+d}", "\\sqrt[n]{\\frac{a}{b}}", "\\frac{d}{dx}(x^2)", "\\sum_{k=1}^{n} k^2", "\\prod_{i=1}^{n} i", "\\coprod_{a\\in A}", "\\int_{0}^{1} x^2 dx", "\\oint_{C} F \\cdot dA", "\\bigcup_{i} A_i \\cap \\bigcap_{j} B_j", "\\limsup_{n\\to\\infty} a_n", "\\lim_{h\\to 0} \\frac{f(x+h) - f(x)}{h}", "\\begin{pmatrix} \\frac{1}{2} & 0 \\\\ 0 & 1 \\end{pmatrix}", "\\begin{vmatrix} a & b \\\\ c & d \\end{vmatrix}", "\\begin{aligned} x &= 1 + 2 \\\\ &= 3 \\end{aligned}", "\\left(\\sum_{i}^{n} a_i\\right)^{2}", "\\frac{\\partial f}{\\partial x}", "\\nabla \\times \\mathbf{F} = 0", "\\hat \\alpha \\neq \\tilde \\beta", "\\forall x \\exists y: x \\leq y", "\\mathbb{Q} \\subset \\mathbb{R}", "\\alpha \\wedge \\neg \\beta \\vee \\gamma", "e^{i\\pi} + 1 = 0", "\\varnothing \\in \\mathcal{P}(X)", "f: X \\to Y, x \\mapsto f(x)", "\\text{if } x > 0 \\text{ then } 1 \\text{ else } -1", "\\max(a,b) \\geq \\min(a,b)", "\\pm 1 \\mp 2", "\\hbar \\omega \\ell", "\\frac{1}{2} \\cdot \\frac{3}{4}", "\\sqrt{2} \\approx 1.414", "\\frac{a_1}{b_1} + \\frac{a_2}{b_2}", "\\left\\{ 1, 2, 3 \\right\\}", "\\mathbf{x}^\\top \\mathbf{y}", "\\vec{r} = \\dot{\\vec{r}} + \\ddot{\\vec{r}}", ]; for src in corpus { let node = parse(src); match node { Ok(node) => { let b = layout(&node, &FakeMetrics, Style { size_pt: 10.0, display: true }); assert!(b.width.is_finite(), "{src}"); assert!(b.height.is_finite(), "{src}"); assert!(b.depth.is_finite(), "{src}"); } Err(e @ (Error::Unknown { .. } | Error::Unexpected { .. })) => { // Known TeX commands must not error; anything else is a design bug. let what = e.to_string(); panic!("{src}: {what}"); } Err(_) => {} } } }