Browse Source

Implemented Geometry.GetWidenedGeometry.

pull/12724/head
Steven Kirk 3 years ago
parent
commit
45e5d25ccf
  1. 24
      src/Avalonia.Base/Media/Geometry.cs
  2. 19
      src/Avalonia.Base/Media/ImmutableGeometry.cs
  3. 8
      src/Avalonia.Base/Platform/IGeometryImpl.cs
  4. 2
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  5. 22
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  6. 21
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  7. 28
      src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs
  8. 32
      src/Skia/Avalonia.Skia/Helpers/SKPathHelper.cs
  9. 17
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  10. 50
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  11. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line.expected.png
  12. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png
  13. BIN
      tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line.expected.png
  14. BIN
      tests/TestFiles/Skia/Shapes/Path/GetWidenedPathGeometry_Line_Dash.expected.png

24
src/Avalonia.Base/Media/Geometry.cs

@ -21,6 +21,7 @@ namespace Avalonia.Media
AvaloniaProperty.Register<Geometry, Transform?>(nameof(Transform)); AvaloniaProperty.Register<Geometry, Transform?>(nameof(Transform));
private bool _isDirty = true; private bool _isDirty = true;
private bool _canInvaldate = true;
private IGeometryImpl? _platformImpl; private IGeometryImpl? _platformImpl;
static Geometry() static Geometry()
@ -30,9 +31,14 @@ namespace Avalonia.Media
internal Geometry() internal Geometry()
{ {
} }
private protected Geometry(IGeometryImpl? platformImpl)
{
_platformImpl = platformImpl;
_isDirty = _canInvaldate = false;
}
/// <summary> /// <summary>
/// Raised when the geometry changes. /// Raised when the geometry changes.
/// </summary> /// </summary>
@ -118,6 +124,17 @@ namespace Avalonia.Media
return PlatformImpl?.StrokeContains(pen, point) == true; 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> /// <summary>
/// Marks a property as affecting the geometry's <see cref="PlatformImpl"/>. /// Marks a property as affecting the geometry's <see cref="PlatformImpl"/>.
/// </summary> /// </summary>
@ -146,6 +163,9 @@ namespace Avalonia.Media
/// </summary> /// </summary>
protected void InvalidateGeometry() protected void InvalidateGeometry()
{ {
if (!_canInvaldate)
return;
_isDirty = true; _isDirty = true;
_platformImpl = null; _platformImpl = null;
Changed?.Invoke(this, EventArgs.Empty); Changed?.Invoke(this, EventArgs.Empty);

19
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
src/Avalonia.Base/Platform/IGeometryImpl.cs

@ -28,6 +28,14 @@ namespace Avalonia.Platform
/// <returns>The bounding rectangle.</returns> /// <returns>The bounding rectangle.</returns>
Rect GetRenderBounds(IPen? pen); 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> /// <summary>
/// Indicates whether the geometry's fill contains the specified point. /// Indicates whether the geometry's fill contains the specified point.
/// </summary> /// </summary>

2
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

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

22
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -6,6 +6,7 @@ using System.Threading;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Rendering.Utilities; using Avalonia.Rendering.Utilities;
using Avalonia.Skia.Helpers;
using Avalonia.Utilities; using Avalonia.Utilities;
using SkiaSharp; using SkiaSharp;
using ISceneBrush = Avalonia.Media.ISceneBrush; using ISceneBrush = Avalonia.Media.ISceneBrush;
@ -1252,25 +1253,10 @@ namespace Avalonia.Skia
paint.StrokeMiter = (float) pen.MiterLimit; 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; paint.PathEffect = dashEffect;
rv.AddDisposable(dashEffect);
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);
} }
return rv; return rv;

21
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -2,6 +2,7 @@ using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Skia.Helpers;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia namespace Avalonia.Skia
@ -75,6 +76,22 @@ namespace Avalonia.Skia
return _pathCache.RenderBounds; 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 /> /// <inheritdoc />
public ITransformedGeometryImpl WithTransform(Matrix transform) public ITransformedGeometryImpl WithTransform(Matrix transform)
{ {
@ -191,6 +208,10 @@ namespace Avalonia.Skia
paint.StrokeCap = cap.ToSKStrokeCap(); paint.StrokeCap = cap.ToSKStrokeCap();
paint.StrokeJoin = join.ToSKStrokeJoin(); paint.StrokeJoin = join.ToSKStrokeJoin();
paint.StrokeMiter = (float)miterLimit; paint.StrokeMiter = (float)miterLimit;
if (DrawingContextHelper.TryCreateDashEffect(pen, out var dashEffect))
paint.PathEffect = dashEffect;
_path = new SKPath(); _path = new SKPath();
paint.GetFillPath(strokePath, _path); paint.GetFillPath(strokePath, _path);

28
src/Skia/Avalonia.Skia/Helpers/DrawingContextHelper.cs

@ -1,5 +1,6 @@
using Avalonia.Platform; using System.Diagnostics.CodeAnalysis;
using Avalonia.Rendering; using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp; using SkiaSharp;
namespace Avalonia.Skia.Helpers namespace Avalonia.Skia.Helpers
@ -26,5 +27,28 @@ namespace Avalonia.Skia.Helpers
return new DrawingContextImpl(createInfo); 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
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
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/> /// <inheritdoc/>
public bool FillContains(Point point) public bool FillContains(Point point)
{ {

50
tests/Avalonia.RenderTests/Shapes/PathTests.cs

@ -434,5 +434,55 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
await RenderToFile(target); await RenderToFile(target);
CompareImages(); 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Loading…
Cancel
Save