Selaa lähdekoodia

Implemented Geometry.GetWidenedGeometry.

Steven Kirk 2 vuotta sitten
vanhempi
sitoutus
45e5d25ccf

+ 22 - 2
src/Avalonia.Base/Media/Geometry.cs

@@ -21,6 +21,7 @@ namespace Avalonia.Media
             AvaloniaProperty.Register<Geometry, Transform?>(nameof(Transform));
 
         private bool _isDirty = true;
+        private bool _canInvaldate = true;
         private IGeometryImpl? _platformImpl;
 
         static Geometry()
@@ -30,9 +31,14 @@ namespace Avalonia.Media
 
         internal Geometry()
         {
-            
         }
-        
+
+        private protected Geometry(IGeometryImpl? platformImpl)
+        {
+            _platformImpl = platformImpl;
+           _isDirty = _canInvaldate = false;
+        }
+
         /// <summary>
         /// Raised when the geometry changes.
         /// </summary>
@@ -118,6 +124,17 @@ namespace Avalonia.Media
             return PlatformImpl?.StrokeContains(pen, point) == true;
         }
 
+        /// <summary>
+        /// Gets a <see cref="Geometry"/> that is the shape defined by the stroke on the Geometry
+        /// produced by the specified Pen.
+        /// </summary>
+        /// <param name="pen">The pen to use.</param>
+        /// <returns>The outlined geometry.</returns>
+        public Geometry GetWidenedGeometry(IPen pen)
+        {
+            return new ImmutableGeometry(PlatformImpl?.GetWidenedGeometry(pen));
+        }
+
         /// <summary>
         /// Marks a property as affecting the geometry's <see cref="PlatformImpl"/>.
         /// </summary>
@@ -146,6 +163,9 @@ namespace Avalonia.Media
         /// </summary>
         protected void InvalidateGeometry()
         {
+            if (!_canInvaldate)
+                return;
+
             _isDirty = true;
             _platformImpl = null;
             Changed?.Invoke(this, EventArgs.Empty);

+ 19 - 0
src/Avalonia.Base/Media/ImmutableGeometry.cs

@@ -0,0 +1,19 @@
+using System;
+using Avalonia.Platform;
+
+namespace Avalonia.Media;
+
+internal class ImmutableGeometry : Geometry
+{
+    public ImmutableGeometry(IGeometryImpl? platformImpl)
+        : base(platformImpl)
+    {
+    }
+
+    public override Geometry Clone() => new ImmutableGeometry(PlatformImpl);
+
+    private protected override IGeometryImpl? CreateDefiningGeometry()
+    {
+        return PlatformImpl;
+    }
+}

+ 8 - 0
src/Avalonia.Base/Platform/IGeometryImpl.cs

@@ -28,6 +28,14 @@ namespace Avalonia.Platform
         /// <returns>The bounding rectangle.</returns>
         Rect GetRenderBounds(IPen? pen);
 
+        /// <summary>
+        /// Gets a geometry that is the shape defined by the stroke on the geometry
+        /// produced by the specified Pen.
+        /// </summary>
+        /// <param name="pen">The pen to use.</param>
+        /// <returns>The outlined geometry.</returns>
+        IGeometryImpl GetWidenedGeometry(IPen pen);
+
         /// <summary>
         /// Indicates whether the geometry's fill contains the specified point.
         /// </summary>

+ 2 - 0
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@@ -182,6 +182,8 @@ namespace Avalonia.Headless
                 return Bounds.Inflate(pen.Thickness / 2);
             }
 
+            public IGeometryImpl GetWidenedGeometry(IPen pen) => this;
+
             public bool StrokeContains(IPen? pen, Point point)
             {
                 return false;

+ 4 - 18
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -6,6 +6,7 @@ using System.Threading;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering.Utilities;
+using Avalonia.Skia.Helpers;
 using Avalonia.Utilities;
 using SkiaSharp;
 using ISceneBrush = Avalonia.Media.ISceneBrush;
@@ -1252,25 +1253,10 @@ namespace Avalonia.Skia
 
             paint.StrokeMiter = (float) pen.MiterLimit;
 
-            if (pen.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
+            if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect))
             {
-                var srcDashes = pen.DashStyle.Dashes;
-
-                var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2;
-
-                var dashesArray = new float[count];
-
-                for (var i = 0; i < count; ++i)
-                {
-                    dashesArray[i] = (float) srcDashes[i % srcDashes.Count] * paint.StrokeWidth;
-                }
-
-                var offset = (float)(pen.DashStyle.Offset * pen.Thickness);
-
-                var pe = SKPathEffect.CreateDash(dashesArray, offset);
-
-                paint.PathEffect = pe;
-                rv.AddDisposable(pe);
+                paint.PathEffect = dashEffect;
+                rv.AddDisposable(dashEffect);
             }
 
             return rv;

+ 21 - 0
src/Skia/Avalonia.Skia/GeometryImpl.cs

@@ -2,6 +2,7 @@ using System;
 using System.Diagnostics.CodeAnalysis;
 using Avalonia.Media;
 using Avalonia.Platform;
+using Avalonia.Skia.Helpers;
 using SkiaSharp;
 
 namespace Avalonia.Skia
@@ -75,6 +76,22 @@ namespace Avalonia.Skia
             return _pathCache.RenderBounds;
         }
 
+        public IGeometryImpl GetWidenedGeometry(IPen pen)
+        {
+            var cache = new PathCache();
+            cache.UpdateIfNeeded(StrokePath, pen);
+
+            if (cache.ExpandedPath is { } path)
+            {
+                // The path returned to us by skia here does not have closed figures.
+                // Fix that by calling CreateClosedPath.
+                var closed = SKPathHelper.CreateClosedPath(path);
+                return new StreamGeometryImpl(closed, closed);
+            }
+            
+            return new StreamGeometryImpl(new SKPath(), null);
+        }
+
         /// <inheritdoc />
         public ITransformedGeometryImpl WithTransform(Matrix transform)
         {
@@ -191,6 +208,10 @@ namespace Avalonia.Skia
                 paint.StrokeCap = cap.ToSKStrokeCap();
                 paint.StrokeJoin = join.ToSKStrokeJoin();
                 paint.StrokeMiter = (float)miterLimit;
+
+                if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect))
+                    paint.PathEffect = dashEffect;
+
                 _path = new SKPath();
                 paint.GetFillPath(strokePath, _path);
 

+ 26 - 2
src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs

@@ -1,5 +1,6 @@
-using Avalonia.Platform;
-using Avalonia.Rendering;
+using System.Diagnostics.CodeAnalysis;
+using Avalonia.Media;
+using Avalonia.Platform;
 using SkiaSharp;
 
 namespace Avalonia.Skia.Helpers
@@ -26,5 +27,28 @@ namespace Avalonia.Skia.Helpers
             return new DrawingContextImpl(createInfo);
         }
         
+        public static bool TryCreateDashEffect(IPen? pen, [NotNullWhen(true)] out SKPathEffect? effect)
+        {
+            if (pen?.DashStyle?.Dashes != null && pen.DashStyle.Dashes.Count > 0)
+            {
+                var srcDashes = pen.DashStyle.Dashes;
+
+                var count = srcDashes.Count % 2 == 0 ? srcDashes.Count : srcDashes.Count * 2;
+
+                var dashesArray = new float[count];
+
+                for (var i = 0; i < count; ++i)
+                {
+                    dashesArray[i] = (float)srcDashes[i % srcDashes.Count] * (float)pen.Thickness;
+                }
+
+                var offset = (float)(pen.DashStyle.Offset * pen.Thickness);
+                effect = SKPathEffect.CreateDash(dashesArray, offset);
+                return true;
+            }
+
+            effect = null;
+            return false;
+        }
     }
 }

+ 32 - 0
src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs

@@ -0,0 +1,32 @@
+using SkiaSharp;
+
+namespace Avalonia.Skia.Helpers;
+
+internal static class SKPathHelper
+{
+    public static SKPath CreateClosedPath(SKPath path)
+    {
+        using var iter = path.CreateIterator(true);
+        SKPathVerb verb;
+        var points = new SKPoint[4];
+        var rv = new SKPath();
+        while ((verb = iter.Next(points)) != SKPathVerb.Done)
+        {
+            if (verb == SKPathVerb.Move)
+                rv.MoveTo(points[0]);
+            else if (verb == SKPathVerb.Line)
+                rv.LineTo(points[1]);
+            else if (verb == SKPathVerb.Close)
+                rv.Close();
+            else if (verb == SKPathVerb.Quad)
+                rv.QuadTo(points[1], points[2]);
+            else if (verb == SKPathVerb.Cubic)
+                rv.CubicTo(points[1], points[2], points[3]);
+            else if (verb == SKPathVerb.Conic)
+                rv.ConicTo(points[1], points[2], iter.ConicWeight());
+
+        }
+
+        return rv;
+    }
+}

+ 17 - 0
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@@ -47,6 +47,23 @@ namespace Avalonia.Direct2D1.Media
             }
         }
 
+        public IGeometryImpl GetWidenedGeometry(IPen pen)
+        {
+            var result = new PathGeometry(Direct2D1Platform.Direct2D1Factory);
+
+            using (var sink = result.Open())
+            {
+                Geometry.Widen(
+                    (float)pen.Thickness,
+                    pen.ToDirect2DStrokeStyle(Direct2D1Platform.Direct2D1Factory),
+                    0.25f,
+                    sink);
+                sink.Close();
+            }
+
+            return new StreamGeometryImpl(result);
+        }
+
         /// <inheritdoc/>
         public bool FillContains(Point point)
         {

+ 50 - 0
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@@ -434,5 +434,55 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
             await RenderToFile(target);
             CompareImages();
         }
+
+        [Fact]
+        public async Task GetWidenedPathGeometry_Line()
+        {
+            var pen = new Pen(Brushes.Black, 10);
+            var geometry = StreamGeometry.Parse("M 0,0 L 180,180").GetWidenedGeometry(pen);
+
+            Decorator target = new Decorator
+            {
+                Width = 200,
+                Height = 200,
+                Child = new Path
+                {
+                    Stroke = Brushes.Red,
+                    StrokeThickness = 1,
+                    Fill = Brushes.Green,
+                    HorizontalAlignment = HorizontalAlignment.Center,
+                    VerticalAlignment = VerticalAlignment.Center,
+                    Data = geometry,
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
+        [Fact]
+        public async Task GetWidenedPathGeometry_Line_Dash()
+        {
+            var pen = new Pen(Brushes.Black, 10, DashStyle.Dash);
+            var geometry = StreamGeometry.Parse("M 0,0 L 180,180").GetWidenedGeometry(pen);
+
+            Decorator target = new Decorator
+            {
+                Width = 200,
+                Height = 200,
+                Child = new Path
+                {
+                    Stroke = Brushes.Red,
+                    StrokeThickness = 1,
+                    Fill = Brushes.Green,
+                    HorizontalAlignment = HorizontalAlignment.Center,
+                    VerticalAlignment = VerticalAlignment.Center,
+                    Data = geometry,
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
     }
 }

BIN
tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png


BIN
tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png


BIN
tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png


BIN
tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png