Skip to content

Fix array rule corner overhang: outer vertical rules flush with horizontal rules#261

Open
kostub wants to merge 4 commits into
masterfrom
fix/array-rule-corners
Open

Fix array rule corner overhang: outer vertical rules flush with horizontal rules#261
kostub wants to merge 4 commits into
masterfrom
fix/array-rule-corners

Conversation

@kostub

@kostub kostub commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Summary

Two related fixes to array rule rendering, exposed while wiring up the array-environment examples.

Bug 1 — corner overhang

\begin{array}{|c|c|} \hline a & b \\ \hline c & d \\ \hline \end{array} rendered with the horizontal rules overhanging the outer vertical rules — the \hlines poked past the box corners instead of meeting them. MathJax/LaTeX draw a clean box.

Root cause. columnOffsetsForTable: added 2·padding around every vertical rule, including outside the outermost ones. Nothing sits outside the outer rules, so that padding just inset them from the box edges the horizontal rules span — instrumenting the example (padding = 4.0, thickness = 0.8) showed v-rules confined to [4.0, 63.38] while h-rules spanned the full content box [0, 67.38].

Fix. Drop the outside padding on the outer boundaries (i == 0, i == numColumns); padding now lives only between a rule and cell content. The outermost rules sit flush at x == 0 / x == contentWidth — the same edges the horizontals span — so corners meet. Interior boundaries keep padding on both sides. The matrix / existing-env path is untouched: the new padLeft/padRight logic only fires when verticalLines[i] > 0, which is empty for every non-array table.

Bug 2 — rule bounds off by half a thickness

A stroked MTRuleDisplay path straddles its centre-line by thickness/2, but the display recorded position on that centre-line while reporting width == thickness (vertical) / ascent == thickness (horizontal). So recomputeDimensions (max(position + extent)) counted the box as reaching centre + thickness, over-reporting the parent table.width by thickness/2 on the right.

This is not cosmetic: layout advances the pen by a child's reported width, so every ruled array pushed following content thickness/2 too far right (and surrounding delimiters thickness/2 too tall).

Fix. Record position as the box's lower-left origin (inset by thickness/2 on the thickness axis) and re-derive the stroke centre-line in -draw:. Drawn pixels are byte-identical; only the reported metrics tighten to exactly cover the stroke.

Tests

  • testArrayRuleGeometryIsDeterministic — pins the flush-corner invariant: leftmost v-rule stroke edge at x=0, and each h-rule's ends coincide with the outer v-rules' stroke edges rather than overhanging. Fails against the old code, passes after Bug 1.
  • testRuleDisplayHorizontalAndVerticalMetrics — updated to the straddling-box semantics (position is the box origin, offset by thickness/2 from the centre-line).
  • Full suite green: 407/407.

🤖 Generated with Claude Code

Horizontal rules span the full content box [0, contentWidth], but
columnOffsetsForTable: added padding on *both* sides of every vertical
rule — including outside the outermost rules. That inset the outer
verticals by `padding` from the box edges, so every \hline overhung the
outer `|` by `padding` on each side (visible as horizontal lines poking
past the box corners; MathJax/LaTeX meet flush).

Drop the outside padding on the outer boundaries (i==0, i==numColumns):
padding now lives only between a rule and the cell content, so the
outermost rules sit flush at x==0 / x==contentWidth — the same edges the
horizontal rules span. Interior boundaries keep padding on both sides.
Matrix path is untouched (fires only when verticalLines[i] > 0).

testArrayRuleGeometryIsDeterministic now pins the flush-corner invariant:
the leftmost v-rule stroke edge is at x=0 and each h-rule's ends coincide
with the outer v-rules' stroke edges instead of overhanging.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request updates the table layout logic in MTTypesetter.m to remove padding on the outer sides of the outermost vertical rules, allowing them to sit flush with the box edges and align perfectly with horizontal rules. Unit tests in MTTypesetterTest.m have been updated to assert this correct geometry. There are no review comments, so I have no feedback to provide.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

A stroked path straddles its centre-line by thickness/2 on each side, but
initWithStart: recorded the box with the thickness extending to only one
side of the line (position = the centre-line point, width/ascent = full
thickness). So the box sat half a thickness off the actual stroke on the
thickness axis. recomputeDimensions folds that in via max(position.x+width)
and max(position.y+ascent), so:

  - the rightmost vertical rule reported right edge = centre + thickness,
    over-reporting table.width by thickness/2 — and width drives advance,
    so every ruled array injected thickness/2 of trailing blank space;
  - the top/bottom \hline over-reported ascent/descent by thickness/2,
    inflating \left...\right delimiter height and vertical advance.

Record position as the box's lower-left origin (inset thickness/2 on the
thickness axis) so the bounds exactly cover the stroke; -draw: re-derives
the centre-line by offsetting thickness/2, so the drawn output is
byte-identical — only the reported metrics tighten.

testArrayRuleGeometryIsDeterministic now also asserts table.width equals
the rightmost rule's true right edge (no phantom half-thickness).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kostub

kostub commented Jul 12, 2026

Copy link
Copy Markdown
Owner Author

Code Review — array rule corner fixes

Verdict: Ready to merge ✅ (2 non-blocking minor comment fixes recommended)

Review verified the two central claims by algebra and by building/running the full suite in an isolated worktree — not taken from the commit messages at face value.

Strengths (verified)

  • Draw output is provably byte-identical. For both orientations, the +t/2 re-derivation in -draw: exactly cancels the −t/2 inset applied in the initializer, landing on the original moveTo point. Only reported metrics change.
  • Tightened bounds are correct against the codebase's +y-up convention (recomputeDimensions, MTMathListDisplay.m:264-273). A vertical rule now reports x-box [sx−t/2, sx+t/2] centred on the stroke, fixing the old table.width over-report of t/2.
  • "Matrix path untouched" holds. verticalLines defaults to @[] (MTMathList.m:1203); only arrayTableWithAlignments: populates it, so the new padLeft/padRight branch never fires for matrix/pmatrix — the else { x += base; } path is byte-for-byte unchanged.
  • padLeft/padRight geometry is correct for all traced cases: outer-left box-left at x==0, outer-right box-right at x==contentWidth (flush), interior boundaries retain 2·padding, adjacent/multiple rules handled by the unchanged ruleBlock.
  • Only two MTRuleDisplay construction sites exist, both in the array path. Fraction bars / \overline / \underline use the separate MTLineDisplay class — no collateral impact.
  • Tests pin real invariants, not tautologies. testArrayRuleGeometryIsDeterministic asserts absolute geometry (vLeft==0, vRight==tableDisp.width, h-rule ends coincide with v-rule box edges); testRuleDisplayHorizontalAndVerticalMetrics pins the exact half-thickness inset per axis.
  • Full suite green: 407/407 (built at head SHA in a throwaway worktree, swift test).

Issues

Critical: none.
Important: none.

Minor:

  1. Stale header doc commentiosMath/render/internal/MTMathListDisplayInternal.h:143-145. Still reads that callers must offset start by thickness/2 where edge alignment matters. The initializer now performs that inset itself, and both callers pass the raw centre-line. If a future caller follows the comment, they'd double-inset by t/2. Update it to state that start is a point on the stroke centre-line and the initializer records the box origin.
  2. Stale function-header commentiosMath/render/internal/MTTypesetter.m:2341-2344 (above columnOffsetsForTable:). Still says "widen the gap by 2·padding + the rule block", now only true for interior boundaries. The inline comment further down is correct; this is minor redundancy.

Notes

  • The leftmost vertical rule now has position.x = −t/2 (small negative) — correct and intentional (stroke straddles x==0). No consumer assumes position.x ≥ 0; suite confirms.

Assessment

Ready to merge: Yes — with the two trivial comment fixes recommended but non-blocking. Both geometry fixes are mathematically correct and verified; the draw round-trip is provably byte-identical, the tightened bounds exactly cover the stroke, the matrix path is genuinely untouched, and the full 407-test suite passes.

🤖 Review dispatched via superpowers:requesting-code-review

kostub and others added 2 commits July 12, 2026 21:26
The header comment still said callers must offset `start` by thickness/2
for edge alignment. Since "Tighten MTRuleDisplay bounds to the drawn
stroke", the initializer performs that inset itself and both callers pass
the raw centre-line — following the old comment would double-inset by
thickness/2. Rewrite it to state `start` is a centre-line point and the
initializer records the box origin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts:
#	iosMath/render/internal/MTMathListDisplayInternal.h
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.

1 participant