Browse Source

Implement GeometryGroup.

pull/6576/head
Steven Kirk 4 years ago
parent
commit
89cb076778
  1. 1
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  2. 37
      src/Avalonia.Visuals/Media/GeometryCollection.cs
  3. 80
      src/Avalonia.Visuals/Media/GeometryGroup.cs
  4. 8
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  5. 36
      src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
  6. 5
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  7. 1
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  8. 33
      src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs
  9. 5
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  10. 154
      tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs
  11. 5
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  12. 26
      tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs
  13. 5
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  14. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd.expected.png
  15. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png
  16. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_NonZero.expected.png
  17. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png
  18. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd.expected.png
  19. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png
  20. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero.expected.png
  21. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png

1
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -47,6 +47,7 @@ namespace Avalonia.Headless
}
public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => throw new NotImplementedException();
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget();

37
src/Avalonia.Visuals/Media/GeometryCollection.cs

@ -0,0 +1,37 @@
using System.Collections;
using System.Collections.Generic;
using Avalonia.Animation;
#nullable enable
namespace Avalonia.Media
{
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry>
{
private List<Geometry> _inner;
public GeometryCollection() => _inner = new List<Geometry>();
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection);
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity);
public Geometry this[int index]
{
get => _inner[index];
set => _inner[index] = value;
}
public int Count => _inner.Count;
public bool IsReadOnly => false;
public void Add(Geometry item) => _inner.Add(item);
public void Clear() => _inner.Clear();
public bool Contains(Geometry item) => _inner.Contains(item);
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator();
public int IndexOf(Geometry item) => _inner.IndexOf(item);
public void Insert(int index, Geometry item) => _inner.Insert(index, item);
public bool Remove(Geometry item) => _inner.Remove(item);
public void RemoveAt(int index) => _inner.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

80
src/Avalonia.Visuals/Media/GeometryGroup.cs

@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Media
{
/// <summary>
/// Represents a composite geometry, composed of other <see cref="Geometry"/> objects.
/// </summary>
public class GeometryGroup : Geometry
{
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty =
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> (
nameof(Children),
o => o.Children,
(o, v) => o.Children = v);
public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule));
private GeometryCollection? _children;
private bool _childrenSet;
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
[Content]
public GeometryCollection? Children
{
get => _children ??= (!_childrenSet ? new GeometryCollection() : null);
set
{
SetAndRaise(ChildrenProperty, ref _children, value);
_childrenSet = true;
}
}
/// <summary>
/// Gets or sets how the intersecting areas of the objects contained in this
/// <see cref="GeometryGroup"/> are combined. The default is <see cref="FillRule.EvenOdd"/>.
/// </summary>
public FillRule FillRule
{
get => GetValue(FillRuleProperty);
set => SetValue(FillRuleProperty, value);
}
public override Geometry Clone()
{
var result = new GeometryGroup { FillRule = FillRule };
if (_children?.Count > 0)
result.Children = new GeometryCollection(_children);
return result;
}
protected override IGeometryImpl? CreateDefiningGeometry()
{
if (_children?.Count > 0)
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return factory.CreateGeometryGroup(FillRule, _children);
}
return null;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty)
{
InvalidateGeometry();
}
}
}
}

8
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -59,6 +59,14 @@ namespace Avalonia.Platform
/// <returns>An <see cref="IStreamGeometryImpl"/>.</returns>
IStreamGeometryImpl CreateStreamGeometry();
/// <summary>
/// Creates a geometry group implementation.
/// </summary>
/// <param name="fillRule">The fill rule.</param>
/// <param name="children">The geometries to group.</param>
/// <returns>A combined geometry.</returns>
IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children);
/// <summary>
/// Creates a renderer.
/// </summary>

36
src/Skia/Avalonia.Skia/GeometryGroupImpl.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
using Avalonia.Media;
using SkiaSharp;
#nullable enable
namespace Avalonia.Skia
{
/// <summary>
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
/// </summary>
internal class GeometryGroupImpl : GeometryImpl
{
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> children)
{
var path = new SKPath
{
FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd,
};
var count = children.Count;
for (var i = 0; i < count; ++i)
{
if (children[i]?.PlatformImpl is GeometryImpl child)
path.AddPath(child.EffectivePath);
}
EffectivePath = path;
Bounds = path.Bounds.ToAvaloniaRect();
}
public override Rect Bounds { get; }
public override SKPath EffectivePath { get; }
}
}

5
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -62,6 +62,11 @@ namespace Avalonia.Skia
return new StreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
return new GeometryGroupImpl(fillRule, children);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{

1
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -175,6 +175,7 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);
public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect);
public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)

33
src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs

@ -0,0 +1,33 @@
using System.Collections.Generic;
using SharpDX.Direct2D1;
using AM = Avalonia.Media;
namespace Avalonia.Direct2D1.Media
{
/// <summary>
/// A Direct2D implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
/// </summary>
internal class GeometryGroupImpl : GeometryImpl
{
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> geometry)
: base(CreateGeometry(fillRule, geometry))
{
}
private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> children)
{
var count = children.Count;
var c = new Geometry[count];
for (var i = 0; i < count; ++i)
{
c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry;
}
return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c);
}
}
}

5
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -36,6 +36,11 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
throw new NotImplementedException();
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
throw new NotImplementedException();

154
tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs

@ -0,0 +1,154 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class GeometryGroupTests : TestBase
{
public GeometryGroupTests()
: base(@"Media\GeometryGroup")
{
}
[Fact]
public async Task FillRule_EvenOdd()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new GeometryGroup
{
FillRule = FillRule.EvenOdd,
Children =
{
new RectangleGeometry(new Rect(25, 25, 100, 100)),
new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
},
}
},
Fill = Brushes.Blue,
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task FillRule_NonZero()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new GeometryGroup
{
FillRule = FillRule.NonZero,
Children =
{
new RectangleGeometry(new Rect(25, 25, 100, 100)),
new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
},
}
},
Fill = Brushes.Blue,
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task FillRule_EvenOdd_Stroke()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new GeometryGroup
{
FillRule = FillRule.EvenOdd,
Children =
{
new RectangleGeometry(new Rect(25, 25, 100, 100)),
new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
},
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
await RenderToFile(target);
CompareImages();
}
[Fact]
public async Task FillRule_NonZero_Stroke()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new GeometryGroup
{
FillRule = FillRule.NonZero,
Children =
{
new RectangleGeometry(new Rect(25, 25, 100, 100)),
new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
},
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
await RenderToFile(target);
CompareImages();
}
}
}

5
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -52,6 +52,11 @@ namespace Avalonia.UnitTests
return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
return Mock.Of<IGeometryImpl>();
}
public IWriteableBitmapImpl CreateWriteableBitmap(
PixelSize size,
Vector dpi,

26
tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs

@ -0,0 +1,26 @@
using Avalonia.Media;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class GeometryGroupTests
{
[Fact]
public void Children_Should_Have_Initial_Collection()
{
var target = new GeometryGroup();
Assert.NotNull(target.Children);
}
[Fact]
public void Children_Can_Be_Set_To_Null()
{
var target = new GeometryGroup();
target.Children = null;
Assert.Null(target.Children);
}
}
}

5
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -37,6 +37,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return new MockStreamGeometry();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(Stream stream)
{
throw new NotImplementedException();

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_NonZero.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_EvenOdd_Stroke.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_NonZero_Stroke.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Loading…
Cancel
Save