Skip to content

Add symmetry test for O and o outline; fix glyph definitions#138

Open
terryspitz wants to merge 14 commits into
mainfrom
claude/glyph-symmetry-clean
Open

Add symmetry test for O and o outline; fix glyph definitions#138
terryspitz wants to merge 14 commits into
mainfrom
claude/glyph-symmetry-clean

Conversation

@terryspitz
Copy link
Copy Markdown
Owner

Add FontTests.O_And_o_Outline_IsHorizontallyAndVerticallySymmetric which
verifies that every outline knot of 'O' and 'o' has a corresponding mirror
point within 1 unit, both horizontally (about the bounding-box cx) and
vertically (about cy).

The test revealed that the DactylSpline solver's Nelder-Mead optimiser was
perturbing the initial tangent angles and converging to a slightly asymmetric
solution for these four-point oval glyphs. Fixed by adding explicit cardinal
tangents (N/E/S/W) to the 'O' and 'o' backbone definitions so the solver
treats the tangent directions as fixed and only optimises handle lengths.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3

claude and others added 11 commits May 15, 2026 22:24
Add FontTests.O_And_o_Outline_IsHorizontallyAndVerticallySymmetric which
verifies that every outline knot of 'O' and 'o' has a corresponding mirror
point within 1 unit, both horizontally (about the bounding-box cx) and
vertically (about cy).

The test revealed that the DactylSpline solver's Nelder-Mead optimiser was
perturbing the initial tangent angles and converging to a slightly asymmetric
solution for these four-point oval glyphs. Fixed by adding explicit cardinal
tangents (N/E/S/W) to the 'O' and 'o' backbone definitions so the solver
treats the tangent directions as fixed and only optimises handle lengths.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
'0' and 'Q' share the same four-point symmetric oval (hl~tc~hr~bc~) as 'O'.
Without explicit directions the Nelder-Mead optimiser perturbs the initial
tangent angles and converges to a slightly asymmetric solution, for the same
reason as was fixed for 'O' and 'o'.

Adds N/E/S/W tangent hints to the oval part of each definition so the
DactylSpline solver treats those directions as fixed, producing a
geometrically correct symmetric oval outline.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
Any point using bracket notation (fitted coordinates) should carry an
explicit cardinal tangent — this pins the optimizer's direction at extremal
points and prevents the Nelder-Mead solver from drifting off the symmetric
solution.

Geometric rule: if y is fitted (y_fit=true) the point is a left/right
extremum → tangent is vertical (S); if x is fitted (x_fit=true) the point
is a top/bottom extremum → tangent is horizontal (W or E).

- 'a': x(c) → x(c)W  (top of bowl, going right→left)
- 'B': (bh)r → (bh)rS, (th)r → (th)rS  (rightmost of each bowl)
- '@': te(c) → te(c)W, be(c) → be(c)E  (top/bottom of inner loop)

'e', 'G', 'n', 'P', 's' already followed this rule correctly.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
Fitted coordinates (bracket notation) mark extremal points where the
solver optimises one coordinate. The correct tangent direction follows
mechanically from which coordinate is fitted and the direction of
travel through the point, so there is no reason to repeat it in every
glyph string definition.

Rule added to parse_curve:
- y_fit=true (point slides along fixed x, a left/right extremum)
  → vertical tangent: S if prev.y > next.y, else N
- x_fit=true (point slides along fixed y, a top/bottom extremum)
  → horizontal tangent: E if next.x > prev.x, else W
Only applied to interior points (or all points of a closed curve)
where both neighbours are available.

Explicit tangents already in the string definitions always take
precedence; the auto-assignment is a fallback for None slots.

Remove the now-redundant explicit tangent suffixes from the eight
affected glyph definitions: @, a, B, e, G, n, P, s.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
Extends the bracket-notation pattern to every glyph with extremal
curve points: bowl letters b/c/d/p/q, capitals C/D/G/R, arches h/J/U/u,
descenders g/j/y, digits 2/3/6/8/9, S/$/?/t.

At each geometric extremum (top, bottom, left, right of an arc), the
fixed coordinate is replaced with a fitted coord so the DactylSpline
solver optimises it, and the auto-tangent rule assigns the correct
cardinal direction — eliminating the need for explicit N/S/E/W suffixes
at these points.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
Replaces explicit cardinal tangents (hlN, tcE, hrS, bcW) with bracket
notation ((h)l, t(c), (h)r, b(c)) so the auto-assignment rule handles
the N/E/S/W directions. Consistent with the approach applied to all
other curved glyphs.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
s: (xxb)l left extremum of upper S, (xbb)r right extremum of lower S,
   b(c) bottom; midpoint xbcE keeps explicit E at the S-inflection.
5: ttb(c) top of bowl, (bbt)r right extremum, b(c) bottom.
&: (hb)rS right extremum (start point, explicit S kept), b(c) bottom,
   (hb)l left extremum.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
When a point had x fixed (non-NaN) but y free (NaN) — e.g. `(xb)l`
with x_fit=false, y_fit=true — the copy condition `if IsNaN result.x`
was false so the solver's optimised y was never written back, leaving
y=NaN in the output. Extend the guard to `IsNaN x || IsNaN y`.

Add BracketFittingTests to verify:
- x(c) and x(cr) produce identical solved results (parsed value inside
  brackets is correctly discarded in favour of the solver-driven init)
- A bracket-free point keeps its fixed x=C after solving

https://claude.ai/code/session_013FzUDrEU4omtyJJtnSaqrP
Two bugs in the parseGlyph response handler:
1. Missing setSolveResult(null) meant the previous glyph's SVG path
   persisted on screen until the new solve completed, making it appear
   the tab didn't update.
2. Errors for id=-3 (parseGlyph) were silently ignored; now logged.

https://claude.ai/code/session_015jzWn7EFDMMYsmWwYsKn8F
When a knot has a fitted y-coordinate (y=null) but a fixed x-coordinate,
the result BezierPoint's y stayed NaN after solving because the copy
condition only checked isNaN(x). Affected glyphs like 'o', 'p', 'q' that
use parenthesised y-coordinates (e.g. "(xb)l") for extremum positions.

Change the combined if/then to two independent checks so each coordinate
is independently populated from the solver's output when NaN.

https://claude.ai/code/session_015jzWn7EFDMMYsmWwYsKn8F
claude added 3 commits May 16, 2026 08:25
…onePoints

O/o: now tests both solved backbone bezier points and stroke-expanded outline.
C/c: tests backbone only — end caps on open arcs intentionally break outline
symmetry, so CharToOutline is unsuitable for these open-arc glyphs.

Font.charToSolvedBackbonePoints runs the DactylSpline solver on the reduced
glyph element and returns (x, y) positions of all solved bezier knots.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
xGuides are [L, C, N, R, W]. The last entry is the em-width W (~450), not the
glyph right R (~300). Using W gave cx≈225 instead of the correct (L+R)/2≈150
when turning off Auto on a fitted coordinate. Now uses the second-to-last guide
(R) so the default x placed there reflects the glyph centre.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
Previously used the guide midpoint (cx/cy), which gave wrong results like
x=225 for W/2. Now the priority is:
1. Current solved bezier position (the "value inside the brackets" — what the
   solver found for that fitted coord).
2. Midpoint of the two direct neighbours (using their solved positions if they
   are also fitted).
3. Guide midpoint as a last resort.

https://claude.ai/code/session_013obEC5m12xpz7Rcoz1NKH3
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.

2 participants