@@ -206,6 +206,11 @@ internal sealed partial class RichTextGlyphRenderer : BaseGlyphBuilder
206206 /// </summary>
207207 private PointF currentTransformedBoundsLocation ;
208208
209+ // The current glyph's metric origin before the drawing transform. Cached paint brushes
210+ // are re-created from paints expressed in this space, so the replay shifts them by the
211+ // difference between this origin and the build-time origin before the drawing transform.
212+ private Vector2 currentLocalBoundsLocation ;
213+
209214 /// <summary>
210215 /// Initializes a new instance of the <see cref="RichTextGlyphRenderer"/> class.
211216 /// </summary>
@@ -337,6 +342,7 @@ protected override bool BeginGlyph(in FontRectangle bounds, in GlyphRendererPara
337342 this . currentGlyphClip = RectangleF . FromLTRB ( min . X , min . Y , max . X , max . Y ) ;
338343 }
339344
345+ this . currentLocalBoundsLocation = bounds . Location ;
340346 if ( ! this . noCache )
341347 {
342348 // Transform the font-metric bounds by the drawing transform so that the size
@@ -886,16 +892,16 @@ private void EmitCachedGlyphOperations(GlyphRenderData renderData, PointF curren
886892 /// decoration-free cache hit when the font engine is told to skip the glyph entirely,
887893 /// so no outline is decoded and no path graph is built. Geometry replays from the
888894 /// anchored per-layer paths, group bounds and the glyph clip are recomputed per draw,
889- /// and paint brushes re-convert with the glyph's positional delta appended to the
890- /// drawing transform, because converted brushes bake device coordinates.
895+ /// and paint brushes re-convert from their paints, which are expressed in the space
896+ /// before the drawing transform, shifted by the glyph's positional delta in that space
897+ /// and then transformed like the build draw's geometry.
891898 /// </summary>
892899 /// <param name="entries">The cached entry stream recorded by the build draw.</param>
893900 /// <param name="currentBoundsLocation">The transformed bounding-box origin for the current glyph instance.</param>
894901 private void EmitCachedLayeredGlyphOperations ( List < GlyphRenderData > entries , PointF currentBoundsLocation )
895902 {
896- Vector2 currentOrigin = currentBoundsLocation ;
897- Vector2 delta = currentOrigin - entries [ 0 ] . SourceOrigin ;
898- Matrix4x4 paintTransform = this . drawingOptions . Transform * Matrix4x4 . CreateTranslation ( delta . X , delta . Y , 0F ) ;
903+ Vector2 delta = this . currentLocalBoundsLocation - entries [ 0 ] . SourceOrigin ;
904+ Matrix4x4 paintTransform = Matrix4x4 . CreateTranslation ( delta . X , delta . Y , 0F ) * this . drawingOptions . Transform ;
899905 int replayDepth = 0 ;
900906
901907 for ( int i = 0 ; i < entries . Count ; i ++ )
@@ -943,7 +949,7 @@ private void EmitCachedLayeredGlyphOperations(List<GlyphRenderData> entries, Poi
943949 /// </summary>
944950 /// <param name="entry">The cached layer entry.</param>
945951 /// <param name="currentBoundsLocation">The transformed bounding-box origin for the current glyph instance.</param>
946- /// <param name="paintTransform">The drawing transform with the glyph's positional delta appended .</param>
952+ /// <param name="paintTransform">The glyph's positional delta before the drawing transform, followed by the drawing transform .</param>
947953 /// <param name="replayDepth">The current group nesting depth.</param>
948954 private void EmitCachedLayerFill ( GlyphRenderData entry , PointF currentBoundsLocation , Matrix4x4 paintTransform , int replayDepth )
949955 {
@@ -1040,13 +1046,13 @@ private void RecordMarker(GlyphRenderData entry)
10401046 /// <summary>
10411047 /// Appends a <see cref="GlyphRenderData"/> entry to the private pending glyph list.
10421048 /// Creates the list on the first callback that produces cacheable data. Every entry
1043- /// is stamped with the glyph's build-time transformed metric origin so layered replays
1044- /// can derive the positional delta for paint brushes.
1049+ /// is stamped with the glyph's build-time metric origin before the drawing transform so
1050+ /// layered replays can derive the positional delta for paint brushes.
10451051 /// </summary>
10461052 /// <param name="renderData">The render data to append to the current key's entry list.</param>
10471053 private void UpdateCache ( GlyphRenderData renderData )
10481054 {
1049- renderData . SourceOrigin = this . currentTransformedBoundsLocation ;
1055+ renderData . SourceOrigin = this . currentLocalBoundsLocation ;
10501056
10511057 // Path bounds use a lazy nullable-struct field. Materialize it while the translated
10521058 // path is still private; later canvases may read these bounds concurrently.
0 commit comments