Browse Source

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
pull/12724/head
Steven Kirk 3 years ago
parent
commit
4e011d4fed
  1. 59
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  2. 38
      src/Skia/Avalonia.Skia/Helpers/PenHelper.cs
  3. 37
      src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs

59
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;
}
}
}
}

38
src/Skia/Avalonia.Skia/Helpers/PenHelper.cs

@ -0,0 +1,38 @@
using System;
using Avalonia.Media;
namespace Avalonia.Skia.Helpers;
internal static class PenHelper
{
/// <summary>
/// Gets a hash code for a pen, optionally including the brush.
/// </summary>
/// <param name="pen">The pen.</param>
/// <param name="includeBrush">Whether to include the brush in the hash code.</param>
/// <returns>The hash code.</returns>
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();
}
}

37
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
{
/// <summary>
/// Creates a new path that is a closed version of the source path.
/// </summary>
/// <param name="path">The source path.</param>
/// <returns>A closed path.</returns>
public static SKPath CreateClosedPath(SKPath path)
{
using var iter = path.CreateIterator(true);
@ -29,4 +37,31 @@ internal static class SKPathHelper
return rv;
}
/// <summary>
/// Creates a path that is the result of a pen being applied to the stroke of the given path.
/// </summary>
/// <param name="path">The path to stroke.</param>
/// <param name="pen">The pen to use to stroke the path.</param>
/// <returns>The resulting path, or null if the pen has 0 thickness.</returns>
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;
}
}

Loading…
Cancel
Save