Skip to content

Fix math spacing, delimiters, radicals, and script fractions#156

Open
AshtonSBradley wants to merge 11 commits into
Kolaru:masterfrom
AshtonSBradley:align-fixes
Open

Fix math spacing, delimiters, radicals, and script fractions#156
AshtonSBradley wants to merge 11 commits into
Kolaru:masterfrom
AshtonSBradley:align-fixes

Conversation

@AshtonSBradley
Copy link
Copy Markdown

@AshtonSBradley AshtonSBradley commented May 7, 2026

Fix math spacing, delimiters, radicals, and script fractions

Summary

This PR improves several related math layout regressions that show up in labels, scripts, and multi-font rendering:

  • Fixes italic/upright boundary spacing such as f(t), g(x), (f)x, and η(t).
  • Treats lower-case Greek math symbols as slanted for spacing decisions, so Greek/script combinations do not crowd roman delimiters.
  • Tracks script nesting with LayoutState.script_level, allowing script-style fractions and operator spacing to be tuned separately from top-level math.
  • Improves subscript and superscript placement around italic cores, tall cores, and scaled delimiters.
  • Shortens and recenters fraction rules in scripts, especially cases such as x^{\frac{1}{1+2}} and x_{\frac{1}{1+2}}.
  • Selects usable square-root variants and tunes radical/vinculum spacing for ordinary and fractional radicands.
  • Uses default math glyphs for delimiters and missing math symbols across font families, including \langle, \rangle, vertical bars, and integrals.
  • Restores the longer NewComputerModern integral glyph used by Makie while avoiding delimiter over-scaling around lone display operators.
  • Adds regression tests and optional visual/reference sheets for multi-font inspection.

Closes #9.
Closes #95.
Closes #129.
Closes #142.

Explanation

The main layout change is to carry script depth in LayoutState. Decorated expressions now lay out their lower and upper scripts in an incremented script state, which lets nested fractions and spaced operators use script-style rules without changing top-level layout.

Subscript and superscript anchors are now deliberately asymmetric. Subscripts are driven by the core glyph: slanted cores use the advance-width anchor so subscripts can tuck under italic lean, while upright/non-slanted cores use max(hadvance(core), rightinkbound(core)) to avoid ink collisions. Superscripts continue to use max(hadvance(core), rightinkbound(core)), preserving the desired V^1_2 stagger where the subscript tucks inward but the superscript stays farther out. This also removes the old lower-case-Greek-subscript special case, so N_\nu and J_\nu are handled the same way as other subscripts on slanted capital cores.

Italic/upright spacing still uses glyph ink bounds rather than only advance widths. Lower-case Greek symbols are marked as slanted for layout purposes, including symbols loaded through a font family's special-character path. Adjacent slanted glyphs also get a small ink-gap correction, covering cramped cases such as k\xi without adding a broad operator-space rule.

Delimiter and radical rendering now falls back to the default math font when the selected font lacks a usable math glyph or has text-shaped delimiter glyphs. Delimiter sizing uses the visible contents to avoid over-scaling brackets and bars around a lone display operator, which allows the NewComputerModern long integral glyph to be restored without bringing back the oversized-delimiter artifact. The latest delimiter pass computes a visual axis from non-operator, non-delimiter content so simple and nested brackets sit on the apparent expression midline; rule-containing delimiters are scaled around the chosen axis, with a small brace-specific cap to avoid over-growing {α/β} solely because of the beta descender.

Fraction rules are positioned from combined numerator/denominator ink bounds. This fixes the offset vinculum in reference fractions and keeps shortened script-fraction rules centered over the actual script contents. The latest update also lengthens top-level fraction rules slightly and gives square-root vincula a small right padding so \sqrt{2} and \sqrt{\frac{1}{2}} do not look clipped or left-centered.

The parser change is small: manual_texexpr now treats delimiter and punctuation tuple heads as leaves, matching isleaf. That keeps layout code from needing to normalize parser/test helper artifacts.

Visual Regression

This PR uses an extensive visual regression sheet as part of the debugging workflow, not just as a final artifact. MathTeX layout bugs are often coupled: fixing a subscript can shift tall delimiters, changing a missing glyph fallback can expose radical or fraction placement issues, and behavior can differ substantially across font families. A broad visual sheet made it possible to inspect many representative expressions at once, catch second-order regressions quickly, and iterate on the small numerical layout constants with the same before/after context each time.

The generated multi-font visual regression sheet compares this branch against a clean baseline worktree. Blue marks the current branch, red marks the baseline. The cases intentionally cover issue-specific examples plus neighboring stress cases: italic/upright boundaries, lower-case Greek, nested scripts, primes, roman text, operators, delimiters, fractions, radicals, integrals, and nested expressions across NewComputerModern, TeXGyreHeros, TeXGyrePagella, and LucioleMath.

Open the full-size visual regression PNG

Spacing visual regression sheet

The updated reference sheets are also linked as generated artifacts on the same artifact branch:

Tests

  • julia --project=/Users/braas09p/Dropbox/Julia/Dev/MathTeXEngine.jl test/runtests.jl
  • Local result: all package tests passed.
  • Observed test summaries: TeXExpr 1 pass, Parser 128 pass, Fonts 5 pass, Layout 163 pass, Generate elements 4 pass.
  • julia --project=@runic -m Runic --check src/engine/layout.jl test/layout.jl reference/spacing_visuals.jl passed.
  • Generated the optional visual spacing sheet and reference sheets linked above.

Notes

This does not attempt a full OpenType MATH-table implementation. The visual sheet includes related cases from #93, #105, #110, #126, and PR #151 so they can be inspected, but this PR should not claim to fully close all of those issues.

This partially overlaps #61 by improving unary/operator spacing, but #61 also discusses superscript vertical placement, so I would not close it from this PR alone.

No new dependencies are added.
No public exports are added.

@AshtonSBradley AshtonSBradley changed the title Fix math spacing around italic boundaries, Greek scripts, and operators Fix math spacing, delimiters, radicals, and script fractions May 8, 2026
@AshtonSBradley
Copy link
Copy Markdown
Author

Hi @Kolaru

This tests quite a lot of corner cases (coverage inspired by other font packages) and took quite a lot of manual iteration loops to converge on the current visual regression state where all fonts look pretty consistent and not obviously broken. Quite a few things were just broken for some fonts, or used weird glyphs. I also went to a fair bit of effort to make this a compact change to code.

I hope it is useful to close a bunch of issues all at once.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 93.93939% with 12 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.91%. Comparing base (efdbaab) to head (d03417e).
⚠️ Report is 5 commits behind head on master.

Files with missing lines Patch % Lines
src/engine/layout.jl 97.03% 4 Missing ⚠️
src/engine/layout_context.jl 63.63% 4 Missing ⚠️
src/engine/fonts.jl 50.00% 1 Missing ⚠️
src/engine/texelements.jl 96.00% 1 Missing ⚠️
src/parser/parser.jl 95.45% 1 Missing ⚠️
src/parser/texexpr.jl 50.00% 1 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #156      +/-   ##
==========================================
+ Coverage   79.93%   82.91%   +2.97%     
==========================================
  Files          10       10              
  Lines         658      796     +138     
==========================================
+ Hits          526      660     +134     
- Misses        132      136       +4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@AshtonSBradley
Copy link
Copy Markdown
Author

AshtonSBradley commented May 9, 2026

Looks like the main change in the ReferenceTests is the integral symbol become smaller. A minor shift in space between top of characters and bottom of square root vinculum. The rest look either unchanged or a little better to my eye.

Expansion of the package ReferenceTest to improve coverage could also be a useful direction, but I didn't go there in this PR yet.

Copy link
Copy Markdown
Owner

@Kolaru Kolaru left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @AshtonSBradley, this is fantastic!

Overall, things looks good to me, and I could even follow you change to the monster parser.

I have to admit that for the layouting part, I haven't necessarily looked deep into the formula, but I trust your reference sheet and the reference tests regarding position tweaks :)

I still don't fully understand why the script level needs to be tracked, I feel like that shrinking everything down should do the trick, but well... it doesn't.

Regarding the reference tests, it is expected to fail since you are fixing some of them.

I inspected them, and I think that there are only few regressions:

  1. Subscripts
Image

Was it a conscious choice to align the sub and superscript for cases like V^1_2 and U_{ij}? LaTeX is definitely closer to the current version.

Also x_{y \leftarrow 0} subscript is aligned too low. I assume that it may be related to the fact that you changed both subscripts and spaced symbol logic.

  1. Square root
Image

The \sqrt{\frac{1}{2}} looks really cramped. However, I think that this one can be fixed later, comparing with MathJax, it seems like already on master the alignment is off. So I'm fine with having both fixed at a later point.

Image

Comment thread src/engine/layout_context.jl
Comment thread src/engine/new_computer_modern_data.jl Outdated
@@ -1,5 +1,5 @@
_latex_to_new_computer_modern = Dict(
raw"\int" => 5930,
raw"\int" => 878,
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope nobody will complain about this change.

I agree with the change, considering that the proper way to handle it would be to differentiate inline mode (math part of a line) and equation mode (where the full string is a single equation). However, this is outside the scope of the current PR.

Comment thread src/engine/texelements.jl
Comment thread src/engine/texelements.jl
Comment thread src/engine/texelements.jl
Comment thread src/MathTeXEngine.jl Outdated
export @L_str

const _italic_correction_enabled = Ref(true)
const _unspace_binary_operators_heuristic_enabled = Ref(true)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would remove the leading _: as far as I understand, it only makes sense to have them have Ref if they are intended to be modified by user, so I wouldn't mark them as private.

Still we should not export them, as they are advanced settings.

@AshtonSBradley
Copy link
Copy Markdown
Author

Thanks again for the detailed pass. I pushed an update addressing each point:

  • script_level: added a comment explaining that this tracks sub/superscript nesting so script-style spacing and fraction rules can be chosen independently of geometric scaling.
  • Group.slanted: added the missing field documentation.
  • advanced switches: removed the leading underscores from italic_correction_enabled and unspace_binary_operators_heuristic_enabled, while keeping them unexported.
  • integral glyph: restored the longer NewComputerModern integral glyph used by Makie, and instead fixed delimiter sizing so brackets/bars do not over-scale around a lone display operator.
  • V^1_2, U_{ij}, W^{(i+j)}, and x_{y \rightarrow 0}: split subscript and superscript anchors so subscripts can tuck under italic overhangs without pulling superscripts left; simple grouped scripts now keep the expected script scale.
  • square roots/fractions: recentered fraction vincula using numerator/denominator ink bounds, shortened script-fraction rules, and adjusted \sqrt{\frac{1}{2}} so the radical descends less and the sqrt vinculum is not pulled too far right.
  • kept lower-case Greek slant detection local for now, as suggested, rather than doing a parser-facing cleanup in this PR.

Updated visual artifacts:

Local checks pass:

  • full package tests pass, including Layout 132
  • Runic check passes on the touched Julia files

@AshtonSBradley AshtonSBradley requested a review from Kolaru May 12, 2026 02:30
@AshtonSBradley
Copy link
Copy Markdown
Author

Pushed a follow-up that makes subscript placement more generally slant-aware.

The subscript rule is now based on the core glyph, not the subscript character class:

  • slanted cores use hadvance(core) for subscripts, so V_2, N_\nu, and J_\nu can tuck under the italic lean;
  • non-slanted/upright cores use max(hadvance(core), rightinkbound(core)), so upright glyphs still avoid ink collisions;
  • superscripts still use max(hadvance(core), rightinkbound(core)), preserving the staggered V^1_2 behavior where the subscript tucks inward but the superscript remains farther out.

This removes the previous special case that pushed lower-case Greek subscripts outward just because the subscript itself was Greek. The tests now cover V^1_2, N_\nu, J_\nu, V_\nu, upright roman cores, and bounded overlap for lower-case slanted cores such as x_{\alpha(k)}.

Updated artifacts:

Local checks pass:

  • full package tests pass, including Layout 150
  • Runic check passes on the touched Julia files

@AshtonSBradley
Copy link
Copy Markdown
Author

@Kolaru i think I have addressed all of your comments, I hope this is converging.

@AshtonSBradley
Copy link
Copy Markdown
Author

AshtonSBradley commented May 12, 2026

Final small update from me: I will stop tweaking now, having finished the additional pedantic fixes to delimiter vertical placement relative to the expression midline. I also reverted the experimental shorter \sqrt{\frac{}{}} radical selection and restored the larger radical behavior, which is the safer visual choice for this PR.

The latest pushes refine delimiter centering for simple and nested expressions, keep fraction-containing brackets tall enough around the chosen axis, cap curly-brace growth so Greek descenders such as \beta do not make braces look over-scaled, and leave fractional square roots on the larger radical variant rather than forcing a new bottom-alignment heuristic.

Updated visual artifacts:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Italic text and conjugate alignment Math operator missing space lower case greek spacing inconsistent Cursive / normal text boundaries

3 participants