Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented Geometry.GetWidenedGeometry. #12724

Merged
merged 6 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions src/Avalonia.Base/Media/Geometry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public abstract class Geometry : AvaloniaObject
AvaloniaProperty.Register<Geometry, Transform?>(nameof(Transform));

private bool _isDirty = true;
private bool _canInvaldate = true;
private IGeometryImpl? _platformImpl;

static Geometry()
Expand All @@ -30,9 +31,14 @@ static Geometry()

internal Geometry()
{

}


private protected Geometry(IGeometryImpl? platformImpl)
{
_platformImpl = platformImpl;
_isDirty = _canInvaldate = false;
}

/// <summary>
/// Raised when the geometry changes.
/// </summary>
Expand Down Expand Up @@ -118,6 +124,17 @@ public bool StrokeContains(IPen pen, Point point)
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>
Expand Down Expand Up @@ -146,6 +163,9 @@ protected static void AffectsGeometry(params AvaloniaProperty[] properties)
/// </summary>
protected void InvalidateGeometry()
{
if (!_canInvaldate)
return;

_isDirty = true;
_platformImpl = null;
Changed?.Invoke(this, EventArgs.Empty);
Expand Down
19 changes: 19 additions & 0 deletions src/Avalonia.Base/Media/ImmutableGeometry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using System;
using Avalonia.Platform;

namespace Avalonia.Media;

internal class ImmutableGeometry : Geometry
grokys marked this conversation as resolved.
Show resolved Hide resolved
{
public ImmutableGeometry(IGeometryImpl? platformImpl)
: base(platformImpl)
{
}

public override Geometry Clone() => new ImmutableGeometry(PlatformImpl);

private protected override IGeometryImpl? CreateDefiningGeometry()
{
return PlatformImpl;
}
}
8 changes: 8 additions & 0 deletions src/Avalonia.Base/Platform/IGeometryImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public interface IGeometryImpl
/// <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>
Expand Down
149 changes: 149 additions & 0 deletions src/Avalonia.Base/Utilities/HashCode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Taken from:
// https://github.com/mono/SkiaSharp/blob/main/binding/Binding.Shared/HashCode.cs
// Partial code copied from:
// https://github.com/dotnet/runtime/blob/6072e4d3a7a2a1493f514cdf4be75a3d56580e84/src/libraries/System.Private.CoreLib/src/System/HashCode.cs

#if NETSTANDARD2_0
#nullable disable

using System.Runtime.CompilerServices;

namespace System;

internal unsafe struct HashCode
{
private static readonly uint s_seed = GenerateGlobalSeed();

private const uint Prime1 = 2654435761U;
private const uint Prime2 = 2246822519U;
private const uint Prime3 = 3266489917U;
private const uint Prime4 = 668265263U;
private const uint Prime5 = 374761393U;

private uint _v1, _v2, _v3, _v4;
private uint _queue1, _queue2, _queue3;
private uint _length;

private static unsafe uint GenerateGlobalSeed()
{
var rnd = new Random();
var result = rnd.Next();
return unchecked((uint)result);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void Initialize(out uint v1, out uint v2, out uint v3, out uint v4)
{
v1 = s_seed + Prime1 + Prime2;
v2 = s_seed + Prime2;
v3 = s_seed;
v4 = s_seed - Prime1;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint Round(uint hash, uint input) =>
RotateLeft(hash + input * Prime2, 13) * Prime1;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint QueueRound(uint hash, uint queuedValue) =>
RotateLeft(hash + queuedValue * Prime3, 17) * Prime4;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixState(uint v1, uint v2, uint v3, uint v4) =>
RotateLeft(v1, 1) + RotateLeft(v2, 7) + RotateLeft(v3, 12) + RotateLeft(v4, 18);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint RotateLeft(uint value, int offset) =>
(value << offset) | (value >> (32 - offset));

private static uint MixEmptyState() =>
s_seed + Prime5;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static uint MixFinal(uint hash)
{
hash ^= hash >> 15;
hash *= Prime2;
hash ^= hash >> 13;
hash *= Prime3;
hash ^= hash >> 16;
return hash;
}

public void Add(void* value) =>
Add(value == null ? 0 : ((IntPtr)value).GetHashCode());

public void Add<T>(T value) =>
Add(value?.GetHashCode() ?? 0);

private void Add(int value)
{
uint val = (uint)value;

// Storing the value of _length locally shaves of quite a few bytes
// in the resulting machine code.
uint previousLength = _length++;
uint position = previousLength % 4;

// Switch can't be inlined.

if (position == 0)
_queue1 = val;
else if (position == 1)
_queue2 = val;
else if (position == 2)
_queue3 = val;
else // position == 3
{
if (previousLength == 3)
Initialize(out _v1, out _v2, out _v3, out _v4);

_v1 = Round(_v1, _queue1);
_v2 = Round(_v2, _queue2);
_v3 = Round(_v3, _queue3);
_v4 = Round(_v4, val);
}
}

public int ToHashCode()
{
// Storing the value of _length locally shaves of quite a few bytes
// in the resulting machine code.
uint length = _length;

// position refers to the *next* queue position in this method, so
// position == 1 means that _queue1 is populated; _queue2 would have
// been populated on the next call to Add.
uint position = length % 4;

// If the length is less than 4, _v1 to _v4 don't contain anything
// yet. xxHash32 treats this differently.

uint hash = length < 4 ? MixEmptyState() : MixState(_v1, _v2, _v3, _v4);

// _length is incremented once per Add(Int32) and is therefore 4
// times too small (xxHash length is in bytes, not ints).

hash += length * 4;

// Mix what remains in the queue

// Switch can't be inlined right now, so use as few branches as
// possible by manually excluding impossible scenarios (position > 1
// is always false if position is not > 0).
if (position > 0)
{
hash = QueueRound(hash, _queue1);
if (position > 1)
{
hash = QueueRound(hash, _queue2);
if (position > 2)
hash = QueueRound(hash, _queue3);
}
}

hash = MixFinal(hash);
return (int)hash;
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ public Rect GetRenderBounds(IPen? pen)
return Bounds.Inflate(pen.Thickness / 2);
}

public IGeometryImpl GetWidenedGeometry(IPen pen) => this;

public bool StrokeContains(IPen? pen, Point point)
{
return false;
Expand Down
22 changes: 4 additions & 18 deletions src/Skia/Avalonia.Skia/DrawingContextImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering.Utilities;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
using SkiaSharp;
using ISceneBrush = Avalonia.Media.ISceneBrush;
Expand Down Expand Up @@ -1252,25 +1253,10 @@ internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize)

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;
Expand Down
64 changes: 28 additions & 36 deletions src/Skia/Avalonia.Skia/GeometryImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities;
using SkiaSharp;

namespace Avalonia.Skia
Expand Down Expand Up @@ -75,6 +77,19 @@ public Rect GetRenderBounds(IPen? pen)
return _pathCache.RenderBounds;
}

public IGeometryImpl GetWidenedGeometry(IPen pen)
{
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.
var closed = SKPathHelper.CreateClosedPath(path);
return new StreamGeometryImpl(closed, closed);
}

return new StreamGeometryImpl(new SKPath(), null);
}

/// <inheritdoc />
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
Expand Down Expand Up @@ -143,66 +158,43 @@ protected void InvalidateCaches()
_pathCache = default;
}

private struct PathCache
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)
{
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;
}
_penHash = penHash;
_path?.Dispose();

var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true;
paint.StrokeWidth = (float)_width;
paint.StrokeCap = cap.ToSKStrokeCap();
paint.StrokeJoin = join.ToSKStrokeJoin();
paint.StrokeMiter = (float)miterLimit;
_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()
{
_path?.Dispose();
_path = null;
}

}
}
}
Loading