Fix glyphs without triangles for self-overlapping font contours (Bahnschrift Light "e") - #352
Merged
Merged
Conversation
Variable fonts like Bahnschrift Light, Roboto Flex, Source Sans 3 or Recursive draw glyphs such as "e", "6", "9" as a single contour that overlaps itself (bowl and bar in one path, the eye is only formed by the overlap). Face.MakeFace/Triangulate cannot triangulate such a border, so FontCache.Get produced an empty display list, CloseList returned null, the glyph was missing and, because null was not cached, it was triangulated again on every repaint. - New GlyphShapeBuilder (GDI-free, testable): converts the GDI+ path data to Path2D and builds the glyph area with the non-zero winding rule. Self intersecting contours are split at their intersections into a planar graph, the faces with non-zero winding number are united. Separate overlapping contours are united (same orientation as the outer contour) or subtracted (opposite orientation) instead of being ignored. Nested contours are handled exactly as before. - FontCache.Get: checks the triangulation of every part, paints the outline of a part without triangles (or of the whole glyph when no face could be made), catches exceptions of a single glyph, closes the display list in any case and caches the result even when the list is null. - FontCache: the three copies of the shape construction (Get, Get with IPaintTo3D, GetCenterLines) share GlyphShapeBuilder.MakeShape. - Tests: GlyphShapeBuilderTests (synthetic contours and the "e" of Roboto Flex Light, no font needed), FontCacheGlyphTests (all characters 0x20 to 0x7E of installed fonts through FontCache.Get, inconclusive when the font or GDI is not available). CADability.Tests is signed with the CADability key and gets InternalsVisibleTo. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QL5zfiEqxsCepsdBixLDS1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QL5zfiEqxsCepsdBixLDS1
This branch was successfully deployed
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.
Problem
With
TriangulateText == truesome glyphs are missing in rendered text, observed with "Bahnschrift Light" and the letter "e":FontCache.Getproduced an empty display list,CloseListreturnednull, and becausenullwas not treated as a cached result the glyph was triangulated again on every repaint.Cause
Variable fonts (Bahnschrift, Roboto Flex, Source Sans 3, Recursive, Inter, …) keep overlapping contours. Glyphs like "e", "6", "9", "P", "&" are drawn as a single contour that overlaps itself: bowl and bar are one closed path, the eye of the "e" is not a contour of its own but only the region enclosed by the overlap.
GetOutline2Ddelivers this contour correctly as one closed path,SimpleShape/Face.MakeFaceaccept it, butFace.Triangulatecannot triangulate a self-intersecting border and returns no triangles. Nothing inFontCache.Getnoticed this.Reproduced on Linux with the glyph outlines of Roboto Flex Light, Source Sans 3 Light and Recursive Light (extracted with fontTools, fed through the same path conversion as GDI+ data): "e", "6", "9" (Roboto Flex, Source Sans 3), "B", "D", "e" (Recursive) and "P" (Inter) had 0 triangles with the old code. Bahnschrift itself is not available outside Windows; its "e" uses the same construction (the glyph has one contour with an overlap flag), so this is the failure you traced.
A second, previously ignored case: separate contours that overlap each other (e.g. "K", "A", "R" of Roboto Flex, the "raumTalk" case mentioned in the code). The old code only subtracted contained contours and ignored intersecting ones.
Fix
GlyphShapeBuilder(internal, GDI-free): converts the GDI+ path data toPath2Dand builds the glyph area with the non-zero winding rule of fonts:FontCache.Get(…, IPaintTo3D)safety net (logic of0002-fontcache-outline-fallback-for-glyphs-without-face.patchfrom LogiCal, extended): the triangulation of every part is checked withFace.GetTriangulation; a part without triangles is painted by its outline, a glyph without any face by its paths; exceptions of a single glyph are caught; the display list is closed in afinallyand the result is cached even when the list isnull(DictVal.listCreated), so a failure is not repeated every frame.FontCache(Get,GetwithIPaintTo3D,GetCenterLines) now share one implementation. As a side effect theIPaintTo3Dvariant no longer loses islands inside holes (like the "c" of "©"), which theGetCenterLinesvariant had already fixed.Tests
GlyphShapeBuilderTests: synthetic contours (hole, island in hole, overlapping "+", opposite orientation, keyhole hole, figure eight) and the real "e" of Roboto Flex Light (OFL) at all three font precisions. Run on Linux, all pass.FontCacheGlyphTests: sends 0x20–0x7E of Arial, Calibri, Bahnschrift, Bahnschrift Light, Segoe UI Variable, Segoe UI throughFontCache.Getwith a recordingIPaintTo3Dand expects triangles for every glyph with a closed outline, plus the cache check;Inconclusivewhen GDI or the font is not available. An ignored survey test reports all installed fonts. These need a Windows machine with the fonts, they could not be executed here.CADability.Testsis signed withCADabilityKey.snkand getsInternalsVisibleToin CADability.Font check (item 4 of the task)
Outside Windows only substitutes were possible. Result with the new code, all 94 printable ASCII glyphs each: Barlow Light, DejaVu Sans, FreeSans, Liberation Sans, Noto Sans Light, Open Sans Light/Bold, Oswald Light, Inter Light/Regular, Recursive Sans Linear Light, Roboto Flex Light/Regular, Source Sans 3 Light: 0 glyphs without triangles, 0 area mismatches against skia-pathops as reference. Before the change: Roboto Flex "69e", Source Sans 3 "69e", Recursive "BDe", Inter "P" without triangles. Bahnschrift, Segoe UI Variable, Calibri and Arial must be checked with
FontCacheGlyphTestson Windows.The version is bumped to 1.1.10 in a separate commit on this branch.
🤖 Generated with Claude Code
https://claude.ai/code/session_01QL5zfiEqxsCepsdBixLDS1
Generated by Claude Code