@@ -26,6 +26,8 @@ internal static class WebGPUSceneEncoder
2626 private const int StyleWordCount = 5 ;
2727 private const int TileWidth = 16 ;
2828 private const int TileHeight = 16 ;
29+ private const float PointStrokeSegmentHalfLength = 1F / 128F ;
30+ private const float StrokeMicroSegmentEpsilon = 1F / 64F ;
2931 private static readonly GraphicsOptions DefaultClipGraphicsOptions = new ( ) ;
3032
3133 /// <summary>
@@ -1459,10 +1461,22 @@ private static int EncodeStrokePath(
14591461 float pointTranslateX = destinationOffset . X - rootTargetBounds . X ;
14601462 float pointTranslateY = destinationOffset . Y - rootTargetBounds . Y ;
14611463 lineCount = EstimateStrokeLineCount ( geometry , pen , widthScale ) ;
1464+ int encodedContourCount = 0 ;
14621465
14631466 for ( int i = 0 ; i < geometry . Contours . Count ; i ++ )
14641467 {
14651468 LinearContour contour = geometry . Contours [ i ] ;
1469+ if ( TryGetPointStrokeContour ( geometry , contour , out PointF point ) )
1470+ {
1471+ EncodePointStrokeContour (
1472+ point ,
1473+ pointTranslateX ,
1474+ pointTranslateY ,
1475+ ref pathTags ,
1476+ ref pathData ) ;
1477+ encodedContourCount ++ ;
1478+ continue ;
1479+ }
14661480
14671481 int markerWordCount = contour . IsClosed ? 2 : 4 ;
14681482 Span < uint > contourData = pathData . GetAppendSpan ( 2 + ( contour . SegmentCount * 2 ) + markerWordCount ) ;
@@ -1501,9 +1515,10 @@ private static int EncodeStrokePath(
15011515
15021516 pathData . Advance ( contourData . Length ) ;
15031517 pathTags . Advance ( contourTags . Length ) ;
1518+ encodedContourCount ++ ;
15041519 }
15051520
1506- if ( geometry . Info . SegmentCount == 0 )
1521+ if ( encodedContourCount == 0 )
15071522 {
15081523 return 0 ;
15091524 }
@@ -1512,6 +1527,36 @@ private static int EncodeStrokePath(
15121527 return 1 ;
15131528 }
15141529
1530+ /// <summary>
1531+ /// Encodes one point-like stroke contour as a tiny centered open segment.
1532+ /// </summary>
1533+ private static void EncodePointStrokeContour (
1534+ PointF point ,
1535+ float pointTranslateX ,
1536+ float pointTranslateY ,
1537+ ref OwnedStream < byte > pathTags ,
1538+ ref OwnedStream < uint > pathData )
1539+ {
1540+ PointF start = new ( point . X - PointStrokeSegmentHalfLength , point . Y ) ;
1541+ PointF end = new ( point . X + PointStrokeSegmentHalfLength , point . Y ) ;
1542+ PointF tangentEnd = GetStrokeMarkerTangentPoint ( start , end ) ;
1543+
1544+ Span < uint > contourData = pathData . GetAppendSpan ( 8 ) ;
1545+ Span < byte > contourTags = pathTags . GetAppendSpan ( 2 ) ;
1546+ contourData [ 0 ] = BitcastSingle ( start . X + pointTranslateX ) ;
1547+ contourData [ 1 ] = BitcastSingle ( start . Y + pointTranslateY ) ;
1548+ contourData [ 2 ] = BitcastSingle ( end . X + pointTranslateX ) ;
1549+ contourData [ 3 ] = BitcastSingle ( end . Y + pointTranslateY ) ;
1550+ contourTags [ 0 ] = PackPathTag ( PathTag . LineToF32 ) ;
1551+ contourData [ 4 ] = BitcastSingle ( start . X + pointTranslateX ) ;
1552+ contourData [ 5 ] = BitcastSingle ( start . Y + pointTranslateY ) ;
1553+ contourData [ 6 ] = BitcastSingle ( tangentEnd . X + pointTranslateX ) ;
1554+ contourData [ 7 ] = BitcastSingle ( tangentEnd . Y + pointTranslateY ) ;
1555+ contourTags [ 1 ] = PackPathTag ( PathTag . QuadToF32 | PathTag . SubpathEnd ) ;
1556+ pathData . Advance ( contourData . Length ) ;
1557+ pathTags . Advance ( contourTags . Length ) ;
1558+ }
1559+
15151560 /// <summary>
15161561 /// Encodes one explicit open two-point stroke centerline into Vello-style path tags and path data in target-local space.
15171562 /// </summary>
@@ -1528,6 +1573,7 @@ private static int EncodeOpenSegmentStrokePath(
15281573 {
15291574 float pointTranslateX = destinationOffset . X - rootTargetBounds . X ;
15301575 float pointTranslateY = destinationOffset . Y - rootTargetBounds . Y ;
1576+ NormalizePointStrokeSegment ( ref start , ref end ) ;
15311577 PointF firstTangentEndPoint = GetStrokeMarkerTangentPoint ( start , end ) ;
15321578
15331579 Span < uint > contourData = pathData . GetAppendSpan ( 8 ) ;
@@ -1549,6 +1595,45 @@ private static int EncodeOpenSegmentStrokePath(
15491595 return 1 ;
15501596 }
15511597
1598+ /// <summary>
1599+ /// Gets whether a contour collapses to the point-stroke path used by the CPU rasterizer.
1600+ /// </summary>
1601+ private static bool TryGetPointStrokeContour ( LinearGeometry geometry , LinearContour contour , out PointF point )
1602+ {
1603+ point = default ;
1604+ if ( contour . PointCount == 0 )
1605+ {
1606+ return false ;
1607+ }
1608+
1609+ point = geometry . Points [ contour . PointStart ] ;
1610+ for ( int i = 1 ; i < contour . PointCount ; i ++ )
1611+ {
1612+ PointF next = geometry . Points [ contour . PointStart + i ] ;
1613+ if ( Vector2 . DistanceSquared ( next , point ) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon )
1614+ {
1615+ return false ;
1616+ }
1617+ }
1618+
1619+ return true ;
1620+ }
1621+
1622+ /// <summary>
1623+ /// Replaces a point-like explicit stroke segment with the tiny tangent segment used for point strokes.
1624+ /// </summary>
1625+ private static void NormalizePointStrokeSegment ( ref PointF start , ref PointF end )
1626+ {
1627+ if ( Vector2 . DistanceSquared ( start , end ) > StrokeMicroSegmentEpsilon * StrokeMicroSegmentEpsilon )
1628+ {
1629+ return ;
1630+ }
1631+
1632+ PointF center = new ( ( start . X + end . X ) * 0.5F , ( start . Y + end . Y ) * 0.5F ) ;
1633+ start = new PointF ( center . X - PointStrokeSegmentHalfLength , center . Y ) ;
1634+ end = new PointF ( center . X + PointStrokeSegmentHalfLength , center . Y ) ;
1635+ }
1636+
15521637 /// <summary>
15531638 /// Encodes a rectangle as a fixed-size closed path.
15541639 /// </summary>
0 commit comments