Add symmetry test for O and o outline; fix glyph definitions#138
Open
terryspitz wants to merge 14 commits into
Open
Add symmetry test for O and o outline; fix glyph definitions#138terryspitz wants to merge 14 commits into
terryspitz wants to merge 14 commits into
Conversation
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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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