Browse Source

Implement CombinedGeometry.

pull/6576/head
Steven Kirk 5 years ago
parent
commit
4dbf646a73
  1. 1
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  2. 170
      src/Avalonia.Visuals/Media/CombinedGeometry.cs
  3. 2
      src/Avalonia.Visuals/Media/GeometryGroup.cs
  4. 9
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  5. 35
      src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
  6. 5
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  7. 1
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  8. 36
      src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs
  9. 5
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  10. 56
      tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs
  11. 5
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  12. 5
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  13. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png
  14. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png
  15. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png
  16. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png
  17. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png
  18. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png
  19. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png
  20. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png

1
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

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

170
src/Avalonia.Visuals/Media/CombinedGeometry.cs

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Media
{
public enum GeometryCombineMode
{
/// <summary>
/// The two regions are combined by taking the union of both. The resulting geometry is
/// geometry A + geometry B.
/// </summary>
Union,
/// <summary>
/// The two regions are combined by taking their intersection. The new area consists of the
/// overlapping region between the two geometries.
/// </summary>
Intersect,
/// <summary>
/// The two regions are combined by taking the area that exists in the first region but not
/// the second and the area that exists in the second region but not the first. The new
/// region consists of (A-B) + (B-A), where A and B are geometries.
/// </summary>
Xor,
/// <summary>
/// The second region is excluded from the first. Given two geometries, A and B, the area of
/// geometry B is removed from the area of geometry A, producing a region that is A-B.
/// </summary>
Exclude,
}
/// <summary>
/// Represents a 2-D geometric shape defined by the combination of two Geometry objects.
/// </summary>
public class CombinedGeometry : Geometry
{
/// <summary>
/// Defines the <see cref="Geometry1"/> property.
/// </summary>
public static readonly StyledProperty<Geometry?> Geometry1Property =
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry1));
/// <summary>
/// Defines the <see cref="Geometry2"/> property.
/// </summary>
public static readonly StyledProperty<Geometry?> Geometry2Property =
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry2));
/// <summary>
/// Defines the <see cref="GeometryCombineMode"/> property.
/// </summary>
public static readonly StyledProperty<GeometryCombineMode> GeometryCombineModeProperty =
AvaloniaProperty.Register<CombinedGeometry, GeometryCombineMode>(nameof(GeometryCombineMode));
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class.
/// </summary>
public CombinedGeometry()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
/// specified <see cref="Geometry"/> objects.
/// </summary>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
public CombinedGeometry(Geometry geometry1, Geometry geometry2)
{
Geometry1 = geometry1;
Geometry2 = geometry2;
}
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
/// specified <see cref="Geometry"/> objects and <see cref="GeometryCombineMode"/>.
/// </summary>
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2)
{
Geometry1 = geometry1;
Geometry2 = geometry2;
GeometryCombineMode = combineMode;
}
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
/// specified <see cref="Geometry"/> objects, <see cref="GeometryCombineMode"/> and
/// <see cref="Transform"/>.
/// </summary>
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
/// <param name="transform">The transform applied to the geometry.</param>
public CombinedGeometry(
GeometryCombineMode combineMode,
Geometry? geometry1,
Geometry? geometry2,
Transform? transform)
{
Geometry1 = geometry1;
Geometry2 = geometry2;
GeometryCombineMode = combineMode;
Transform = transform;
}
/// <summary>
/// Gets or sets the first <see cref="Geometry"/> object of this
/// <see cref="CombinedGeometry"/> object.
/// </summary>
public Geometry? Geometry1
{
get => GetValue(Geometry1Property);
set => SetValue(Geometry1Property, value);
}
/// <summary>
/// Gets or sets the second <see cref="Geometry"/> object of this
/// <see cref="CombinedGeometry"/> object.
/// </summary>
public Geometry? Geometry2
{
get => GetValue(Geometry2Property);
set => SetValue(Geometry2Property, value);
}
/// <summary>
/// Gets or sets the method by which the two geometries (specified by the
/// <see cref="Geometry1"/> and <see cref="Geometry2"/> properties) are combined. The
/// default value is <see cref="GeometryCombineMode.Union"/>.
/// </summary>
public GeometryCombineMode GeometryCombineMode
{
get => GetValue(GeometryCombineModeProperty);
set => SetValue(GeometryCombineModeProperty, value);
}
public override Geometry Clone()
{
return new CombinedGeometry(GeometryCombineMode, Geometry1, Geometry2, Transform);
}
protected override IGeometryImpl? CreateDefiningGeometry()
{
var g1 = Geometry1;
var g2 = Geometry2;
if (g1 is object && g2 is object)
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2);
}
else if (GeometryCombineMode == GeometryCombineMode.Intersect)
return null;
else if (g1 is object)
return g1.PlatformImpl;
else if (g2 is object)
return g2.PlatformImpl;
else
return null;
}
}
}

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

@ -50,7 +50,7 @@ namespace Avalonia.Media
public override Geometry Clone()
{
var result = new GeometryGroup { FillRule = FillRule };
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform };
if (_children?.Count > 0)
result.Children = new GeometryCollection(_children);
return result;

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

@ -67,6 +67,15 @@ namespace Avalonia.Platform
/// <returns>A combined geometry.</returns>
IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children);
/// <summary>
/// Creates a geometry group implementation.
/// </summary>
/// <param name="combineMode">The combine mode</param>
/// <param name="g1">The first geometry.</param>
/// <param name="g2">The second geometry.</param>
/// <returns>A combined geometry.</returns>
IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
/// <summary>
/// Creates a renderer.
/// </summary>

35
src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs

@ -0,0 +1,35 @@
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 CombinedGeometryImpl : GeometryImpl
{
public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath;
var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath;
var op = combineMode switch
{
GeometryCombineMode.Intersect => SKPathOp.Intersect,
GeometryCombineMode.Xor => SKPathOp.Xor,
GeometryCombineMode.Exclude => SKPathOp.Difference,
_ => SKPathOp.Union,
};
var path = path1.Op(path2, op);
EffectivePath = path;
Bounds = path.Bounds.ToAvaloniaRect();
}
public override Rect Bounds { get; }
public override SKPath EffectivePath { get; }
}
}

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

@ -67,6 +67,11 @@ namespace Avalonia.Skia
return new GeometryGroupImpl(fillRule, children);
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
return new CombinedGeometryImpl(combineMode, g1, g2);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{

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

@ -176,6 +176,7 @@ namespace Avalonia.Direct2D1
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);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)

36
src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs

@ -0,0 +1,36 @@
using SharpDX.Direct2D1;
using AM = Avalonia.Media;
namespace Avalonia.Direct2D1.Media
{
/// <summary>
/// A Direct2D implementation of a <see cref="Avalonia.Media.CombinedGeometry"/>.
/// </summary>
internal class CombinedGeometryImpl : GeometryImpl
{
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public CombinedGeometryImpl(
AM.GeometryCombineMode combineMode,
AM.Geometry geometry1,
AM.Geometry geometry2)
: base(CreateGeometry(combineMode, geometry1, geometry2))
{
}
private static Geometry CreateGeometry(
AM.GeometryCombineMode combineMode,
AM.Geometry geometry1,
AM.Geometry geometry2)
{
var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry;
var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry;
var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory);
using var sink = dest.Open();
g1.Combine(g2, (CombineMode)combineMode, sink);
sink.Close();
return dest;
}
}
}

5
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -41,6 +41,11 @@ namespace Avalonia.Benchmarks
throw new NotImplementedException();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
throw new NotImplementedException();
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
throw new NotImplementedException();

56
tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs

@ -0,0 +1,56 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class CombinedGeometryTests : TestBase
{
public CombinedGeometryTests()
: base(@"Media\CombinedGeometry")
{
}
[Theory]
[InlineData(Avalonia.Media.GeometryCombineMode.Union)]
[InlineData(Avalonia.Media.GeometryCombineMode.Intersect)]
[InlineData(Avalonia.Media.GeometryCombineMode.Xor)]
[InlineData(Avalonia.Media.GeometryCombineMode.Exclude)]
public async Task GeometryCombineMode(GeometryCombineMode mode)
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new CombinedGeometry
{
GeometryCombineMode = mode,
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)),
Geometry2 = new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
var testName = $"GeometryCombineMode_{mode}";
await RenderToFile(target, testName);
CompareImages(testName);
}
}
}

5
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -57,6 +57,11 @@ namespace Avalonia.UnitTests
return Mock.Of<IGeometryImpl>();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
return Mock.Of<IGeometryImpl>();
}
public IWriteableBitmapImpl CreateWriteableBitmap(
PixelSize size,
Vector dpi,

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

@ -42,6 +42,11 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(Stream stream)
{
throw new NotImplementedException();

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Loading…
Cancel
Save