Files
swiftui-math/MULTILINE_IMPLEMENTATION_NOTES.md
Nicolas Guillot cd9c3f7a37 add documentation
2025-11-13 15:08:55 +01:00

374 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Multiline/Line Breaking Implementation Notes
## Overview
SwiftMath now supports automatic line breaking (multiline display) for mathematical equations. This document provides technical details about the implementation, supported cases, limitations, and potential areas for improvement.
## Implementation Architecture
### Two-Tier Breaking System
#### 1. **Interatom Line Breaking** (Primary - NEW)
**Location**: `MTTypesetter.swift:845-846`
**Mechanism**:
- Checks **before** adding each atom to the current line
- Calculates projected width: `currentLineWidth + atomWidth + interElementSpacing`
- If projected width > maxWidth: flushes current line, moves down, starts new line
- Line spacing: `fontSize × 1.5`
**Applies to atom types**:
- `.ordinary` - Variables, text, regular symbols
- `.binaryOperator` - `+`, `-`, `×`, `÷`
- `.relation` - `=`, `<`, `>`, `≤`, `≥`
- `.open` - Opening brackets `(`
- `.close` - Closing brackets `)`
- `.placeholder` - Placeholder squares
- `.punctuation` - Commas, periods
**Advantages**:
- ✅ Clean semantic breaks between mathematical elements
- ✅ Respects TeX inter-element spacing rules
- ✅ Fast width calculations using Core Text
- ✅ Preserves mathematical structure
#### 2. **Universal Line Breaking** (Fallback - EXISTING)
**Location**: `MTTypesetter.swift:877-950`
**Mechanism**:
- Checks **after** adding atom (for simple atoms without scripts)
- Uses Core Text's `CTTypesetterSuggestLineBreak` for Unicode-aware breaking
- Protects numbers from splitting (3.14, 1,000, etc.)
- Supports multiple locales (EN, FR, CH)
**Applies when**:
- Atoms have no superscripts/subscripts
- Used for very long single text atoms
- Fallback for cases where interatom breaking doesn't apply
## Fully Supported Cases
### ✅ Simple Equations
```swift
"a + b + c + d + e + f"
"x = 1, y = 2, z = 3"
"α + β + γ + δ"
```
**Works perfectly**: Breaks between operators and variables.
### ✅ Mixed Text and Math
```swift
"\\text{Calculate } Δ = b^{2} - 4ac \\text{ with } a=1"
```
**Works perfectly**: Breaks between text and math atoms naturally.
### ✅ Long Sequences
```swift
"1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10"
```
**Works perfectly**: Breaks between numbers and operators.
### ✅ Relational Expressions
```swift
"a < b, b > c, c ≤ d, d ≥ e"
```
**Works perfectly**: Breaks after punctuation and relations.
## Limited Support Cases
### ⚠️ Atoms with Scripts
```swift
"a^{2} + b^{2} + c^{2} + d^{2}"
```
**Works but suboptimal**: Falls back to universal breaking which breaks within accumulated text rather than at clean atom boundaries.
**Why**: Atoms with scripts still trigger line flushing for script positioning, which interrupts the interatom breaking flow.
**Impact**: May not break at the most aesthetically pleasing positions.
### ⚠️ Very Long Text Atoms
```swift
"\\text{This is an extremely long piece of text within a single text command}"
```
**Works**: Uses Core Text's word boundary breaking with number protection.
**Limitation**: Breaks within the text atom, not between atoms.
## Unsupported Cases (Forced Line Breaks)
These atom types **always** flush the current line before rendering, meaning they start on their own line:
### ❌ Fractions
**Code location**: `MTTypesetter.swift:669-682`
```swift
"a + \\frac{1}{2} + b"
// Results in 3 lines:
// Line 1: "a +"
// Line 2: "½"
// Line 3: "+ b"
```
**Why**: Fractions require complex vertical layout (numerator/denominator) and force a line flush.
**Impact**: Expressions with multiple fractions have excessive line breaks.
### ❌ Radicals (Square Roots)
**Code location**: `MTTypesetter.swift:645-668`
```swift
"x + \\sqrt{2} + y"
// Results in 3 lines
```
**Why**: Radicals require special rendering (radical sign + vinculum) and force line flush.
### ❌ Large Operators
**Code location**: `MTTypesetter.swift:684-693`
```swift
"\\sum_{i=1}^{n} x_i + \\int_{0}^{1} f(x)dx"
```
**Why**: Large operators (∑, ∫, ∏, lim) with subscripts/superscripts require special vertical positioning.
**Impact**: Each operator gets its own line.
### ❌ Inner Lists (Delimiters)
**Code location**: `MTTypesetter.swift:694-709`
```swift
"a + \\left( \\frac{b}{c} \\right) + d"
```
**Why**: `\left...\right` pairs create inner lists that flush the line for proper delimiter sizing.
### ❌ Matrices/Tables
**Code location**: `MTTypesetter.swift:757-770`
```swift
"A = \\begin{pmatrix} 1 & 2 \\\\ 3 & 4 \\end{pmatrix}"
```
**Why**: Matrices require complex 2D layout.
### ❌ Colored Expressions
**Code locations**:
- `MTTypesetter.swift:590-600` (`.color`)
- `MTTypesetter.swift:602-630` (`.textcolor`)
- `MTTypesetter.swift:632-643` (`.colorBox`)
```swift
"a + \\color{red}{b + c} + d"
```
**Why**: Color atoms recursively create displays and flush the line.
### ❌ Accents
**Code location**: `MTTypesetter.swift:711-755`
```swift
"\\hat{x} + \\tilde{y}"
```
**Why**: Accents require special vertical positioning and may flush lines.
## Potential Issues and Edge Cases
### 1. Over-Breaking with Complex Atoms
**Problem**: Expressions mixing simple and complex atoms have too many breaks.
**Example**:
```swift
"a + \\frac{1}{2} + b + \\sqrt{3} + c"
// Becomes 5 lines instead of ideally 1-2
```
**Root cause**: Each complex atom flushes the line independently.
**Possible solution**: Check if complex atom + current line width fits within constraint before flushing.
### 2. No Look-Ahead Optimization
**Problem**: Greedy algorithm breaks immediately without considering slightly better break points nearby.
**Example**:
```swift
"abc + defgh"
// With narrow width might break: "abc +"
// "defgh"
// Better might be: "abc"
// "+ defgh"
```
**Root cause**: Algorithm doesn't look ahead to see if next few atoms would create a better break point.
**Possible solution**: Implement k-atom look-ahead with break quality scoring.
### 3. Fixed Line Height
**Problem**: All lines use `fontSize × 1.5` regardless of content height.
**Example**: A line with a fraction is much taller than a line with just variables, but spacing is uniform.
**Possible solution**: Calculate actual line height based on ascent/descent of atoms on each line.
### 4. Scripts Disable Interatom Breaking
**Problem**: Atoms with superscripts/subscripts fall back to universal breaking.
**Example**:
```swift
"a^{2} + b^{2} + c^{2}"
```
**Root cause**: Scripts cause line flushing for vertical positioning (line 892-908), interrupting interatom flow.
**Possible solution**: Refactor script handling to not require immediate line flush, or handle scripted atoms specially in interatom breaking.
### 5. No Break Quality Scoring
**Problem**: All break points are treated equally - no preference for breaking after operators vs. before.
**Example**: Breaking after `+` is generally better than breaking before it for readability.
**Possible solution**: Implement break penalty system:
- Low penalty: after binary operators, after relations, after punctuation
- Medium penalty: after ordinary atoms
- High penalty: after opening brackets, before closing brackets
### 6. No Widow/Orphan Control
**Problem**: Single atoms can end up alone on lines.
**Example**:
```swift
// Last line might just be: "+ e"
```
**Possible solution**: Minimum atoms per line constraint.
### 7. Inconsistent Behavior with Recursion
**Problem**: Nested math lists (inner, color, etc.) create their own displays recursively, potentially without width constraints.
**Example**:
```swift
"\\color{red}{a + b + c + d + e + f + g}"
// The entire colored portion might render on one line even if too wide
```
**Root cause**: Recursive calls to `createLineForMathList` at lines 596, 608, 638 don't pass `maxWidth`.
**Possible solution**: Propagate `maxWidth` to recursive calls.
## Future Enhancement Opportunities
### Priority 1: Fix Complex Atom Line Flushing
**Goal**: Allow fractions, radicals, etc. to coexist on lines with other atoms.
**Approach**:
1. Check if complex atom width + current line width fits
2. If yes, add to line without flushing
3. If no, flush current line, add complex atom to new line
**Implementation**: Modify switch cases for `.fraction`, `.radical`, `.largeOperator` to check width before flushing.
**Impact**: ⭐⭐⭐⭐⭐ (Huge improvement for mathematical expressions)
### Priority 2: Improve Script Handling
**Goal**: Make atoms with scripts work with interatom breaking.
**Approach**:
1. Calculate total width including scripts
2. Include in interatom breaking decision
3. Defer script positioning until after line breaking decision
**Implementation**: Refactor `makeScripts` to be non-flushing.
**Impact**: ⭐⭐⭐⭐ (Significant improvement for common cases)
### Priority 3: Implement Break Quality Scoring
**Goal**: Prefer better break points (e.g., after operators).
**Approach**:
1. Assign penalty scores to different break point types
2. When projected width slightly exceeds maxWidth, look ahead 1-3 atoms
3. Choose break point with lowest penalty within acceptable width range
**Implementation**: Add `calculateBreakPenalty()` method, modify `checkAndPerformInteratomLineBreak()`.
**Impact**: ⭐⭐⭐ (Nice aesthetic improvement)
### Priority 4: Dynamic Line Height
**Goal**: Adjust vertical spacing based on actual line content height.
**Approach**:
1. Track maximum ascent/descent for each line
2. Use actual measurements for vertical positioning
3. Add configurable minimum line spacing
**Implementation**: Modify `addDisplayLine()` to calculate and store line height.
**Impact**: ⭐⭐ (Better vertical spacing)
### Priority 5: Width Constraint Propagation
**Goal**: Apply width constraints to nested/recursive displays.
**Approach**:
1. Pass `maxWidth` to all recursive `createLineForMathList` calls
2. Adjust for nesting level (reduce maxWidth for inner content)
**Implementation**: Update all recursive calls with `maxWidth` parameter.
**Impact**: ⭐⭐ (More consistent behavior)
## Testing Strategy
### Current Test Coverage
✅ Simple equations (6 tests in `MTTypesetterTests.swift:1577-1709`)
✅ Text and math mixing
✅ Atoms at boundaries
✅ Superscripts (limited)
✅ No breaking when not needed
✅ Breaking after operators
### Recommended Additional Tests
- [ ] Fractions in equations
- [ ] Radicals in equations
- [ ] Large operators with breaking
- [ ] Nested expressions
- [ ] Colored sections
- [ ] Very narrow widths (edge cases)
- [ ] Very wide atoms (overflow handling)
- [ ] Mixed scripts and non-scripts
- [ ] Matrices with surrounding content
- [ ] Multiple line breaks (3+ lines)
- [ ] Unicode text wrapping
- [ ] Number protection across languages
## Performance Considerations
### Current Performance
- Width calculations use Core Text (relatively fast)
- No caching of calculated widths
- Greedy algorithm is O(n) where n = number of atoms
### Potential Optimizations
1. **Width caching**: Cache calculated atom widths
2. **Batch processing**: Calculate multiple atom widths together
3. **Early exit**: Stop processing if remaining content definitely fits
## Conclusion
The current implementation provides **excellent support** for:
- ✅ Simple equations with operators
- ✅ Text and math mixing
- ✅ Long sequences of variables/numbers
**Limitations exist** for:
- ⚠️ Expressions with fractions, radicals, large operators
- ⚠️ Nested/colored expressions
- ⚠️ Scripted atoms (superscripts/subscripts)
The most impactful improvements would be:
1. **Fix complex atom flushing** (allow fractions/radicals inline)
2. **Improve script handling** (include in interatom breaking)
3. **Add break quality scoring** (prefer better break points)
These enhancements would significantly expand the range of expressions that break naturally and aesthetically across multiple lines.