From 4e011d4fedb75304f4b29eea6f45bf4af553164a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Aug 2023 11:56:53 +0200 Subject: [PATCH] Refactor creating a stroked path. - Move the logic into `SKPathHelper.CreateStrokedPath`; called from `PathHelper` and `GetWidenedGeometry` - Use a pen hash code to check if we're up-to-date in `PathHelper` and include the dash style in that --- src/Skia/Avalonia.Skia/GeometryImpl.cs | 59 +++++-------------- src/Skia/Avalonia.Skia/Helpers/PenHelper.cs | 38 ++++++++++++ .../Avalonia.Skia/Helpers/SKPathHelper.cs | 37 +++++++++++- 3 files changed, 89 insertions(+), 45 deletions(-) create mode 100644 src/Skia/Avalonia.Skia/Helpers/PenHelper.cs diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs index cd6fd76b3c..a5797d7fd5 100644 --- a/src/Skia/Avalonia.Skia/GeometryImpl.cs +++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Skia.Helpers; +using Avalonia.Utilities; using SkiaSharp; namespace Avalonia.Skia @@ -78,10 +79,7 @@ namespace Avalonia.Skia public IGeometryImpl GetWidenedGeometry(IPen pen) { - using var cache = new PathCache(); - cache.UpdateIfNeeded(StrokePath, pen, includeDashStyle: true); - - if (cache.ExpandedPath is { } path) + if (StrokePath is not null && SKPathHelper.CreateStrokedPath(StrokePath, pen) is { } path) { // The path returned to us by skia here does not have closed figures. // Fix that by calling CreateClosedPath. @@ -162,60 +160,34 @@ namespace Avalonia.Skia private struct PathCache : IDisposable { - private double _width, _miterLimit; - private PenLineCap _cap; - private PenLineJoin _join; + private int _penHash; private SKPath? _path, _cachedFor; private Rect? _renderBounds; private static readonly SKPath s_emptyPath = new(); - public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect(); public SKPath ExpandedPath => _path ?? s_emptyPath; - public void UpdateIfNeeded(SKPath? strokePath, IPen? pen, bool includeDashStyle = false) + public void UpdateIfNeeded(SKPath? strokePath, IPen? pen) { - var strokeWidth = pen?.Thickness ?? 0; - var miterLimit = pen?.MiterLimit ?? 0; - var cap = pen?.LineCap ?? default; - var join = pen?.LineJoin ?? default; - - if (_cachedFor == strokePath - && _path != null - && cap == _cap - && join == _join - && Math.Abs(_width - strokeWidth) < float.Epsilon - && (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon)) + if (PenHelper.GetHashCode(pen, includeBrush: false) is { } penHash && + penHash == _penHash && + strokePath == _cachedFor) + { // We are up to date return; + } _renderBounds = null; _cachedFor = strokePath; - _width = strokeWidth; - _cap = cap; - _join = join; - _miterLimit = miterLimit; - - if (strokePath == null || Math.Abs(strokeWidth) < float.Epsilon) - { - _path = null; - return; - } - - var paint = SKPaintCache.Shared.Get(); - paint.IsStroke = true; - paint.StrokeWidth = (float)_width; - paint.StrokeCap = cap.ToSKStrokeCap(); - paint.StrokeJoin = join.ToSKStrokeJoin(); - paint.StrokeMiter = (float)miterLimit; - - if (includeDashStyle && DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) - paint.PathEffect = dashEffect; + _penHash = penHash; + _path?.Dispose(); - _path = new SKPath(); - paint.GetFillPath(strokePath, _path); + if (strokePath is not null && pen is not null) + _path = SKPathHelper.CreateStrokedPath(strokePath, pen); + else + _path = null; - SKPaintCache.Shared.ReturnReset(paint); } public void Dispose() @@ -223,7 +195,6 @@ namespace Avalonia.Skia _path?.Dispose(); _path = null; } - } } } diff --git a/src/Skia/Avalonia.Skia/Helpers/PenHelper.cs b/src/Skia/Avalonia.Skia/Helpers/PenHelper.cs new file mode 100644 index 0000000000..2f7b6d771d --- /dev/null +++ b/src/Skia/Avalonia.Skia/Helpers/PenHelper.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Skia.Helpers; + +internal static class PenHelper +{ + /// + /// Gets a hash code for a pen, optionally including the brush. + /// + /// The pen. + /// Whether to include the brush in the hash code. + /// The hash code. + public static int GetHashCode(IPen? pen, bool includeBrush) + { + if (pen is null) + return 0; + + var hash = new HashCode(); + hash.Add(pen.LineCap); + hash.Add(pen.LineJoin); + hash.Add(pen.MiterLimit); + hash.Add(pen.Thickness); + + if (pen.DashStyle is { } dashStyle) + { + hash.Add(dashStyle.Offset); + + for (var i = 0; i < dashStyle.Dashes?.Count; i++) + hash.Add(dashStyle.Dashes[i]); + } + + if (includeBrush) + hash.Add(pen.Brush); + + return hash.ToHashCode(); + } +} diff --git a/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs index 3bbb80e305..228d68a4d8 100644 --- a/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs +++ b/src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs @@ -1,9 +1,17 @@ -using SkiaSharp; +using System; +using Avalonia.Media; +using Avalonia.Utilities; +using SkiaSharp; namespace Avalonia.Skia.Helpers; internal static class SKPathHelper { + /// + /// Creates a new path that is a closed version of the source path. + /// + /// The source path. + /// A closed path. public static SKPath CreateClosedPath(SKPath path) { using var iter = path.CreateIterator(true); @@ -29,4 +37,31 @@ internal static class SKPathHelper return rv; } + + /// + /// Creates a path that is the result of a pen being applied to the stroke of the given path. + /// + /// The path to stroke. + /// The pen to use to stroke the path. + /// The resulting path, or null if the pen has 0 thickness. + public static SKPath? CreateStrokedPath(SKPath path, IPen pen) + { + if (MathUtilities.IsZero(pen.Thickness)) + return null; + + var paint = SKPaintCache.Shared.Get(); + paint.IsStroke = true; + paint.StrokeWidth = (float)pen.Thickness; + paint.StrokeCap = pen.LineCap.ToSKStrokeCap(); + paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin(); + paint.StrokeMiter = (float)pen.MiterLimit; + + if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect)) + paint.PathEffect = dashEffect; + + var result = new SKPath(); + paint.GetFillPath(path, result); + SKPaintCache.Shared.ReturnReset(paint); + return result; + } }