Add GeometryGroup and CombinedGeometry Conflicts: src/Avalonia.Visuals/ApiCompatBaseline.txtrelease/0.10.8
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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, Transform = Transform }; |
||||
|
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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; } |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,89 @@ |
|||||
|
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 = $"{nameof(GeometryCombineMode)}_{mode}"; |
||||
|
await RenderToFile(target, testName); |
||||
|
CompareImages(testName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Geometry1_Transform() |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new CombinedGeometry |
||||
|
{ |
||||
|
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)) |
||||
|
{ |
||||
|
Transform = new RotateTransform(45, 75, 75) |
||||
|
}, |
||||
|
Geometry2 = new EllipseGeometry |
||||
|
{ |
||||
|
Center = new Point(125, 125), |
||||
|
RadiusX = 50, |
||||
|
RadiusY = 50, |
||||
|
} |
||||
|
}, |
||||
|
Fill = Brushes.Blue, |
||||
|
Stroke = Brushes.Red, |
||||
|
StrokeThickness = 1, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
await RenderToFile(target); |
||||
|
CompareImages(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,95 @@ |
|||||
|
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") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[InlineData(FillRule.EvenOdd)] |
||||
|
[InlineData(FillRule.NonZero)] |
||||
|
public async Task FillRule_Stroke(FillRule fillRule) |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new GeometryGroup |
||||
|
{ |
||||
|
FillRule = fillRule, |
||||
|
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, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
var testName = $"{nameof(FillRule_Stroke)}_{fillRule}"; |
||||
|
await RenderToFile(target, testName); |
||||
|
CompareImages(testName); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Child_Transform() |
||||
|
{ |
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 200, |
||||
|
Height = 200, |
||||
|
Background = Brushes.White, |
||||
|
Child = new Path |
||||
|
{ |
||||
|
Data = new GeometryGroup |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
new RectangleGeometry(new Rect(25, 25, 100, 100)) |
||||
|
{ |
||||
|
Transform = new RotateTransform(45, 75, 75) |
||||
|
}, |
||||
|
new EllipseGeometry |
||||
|
{ |
||||
|
Center = new Point(125, 125), |
||||
|
RadiusX = 50, |
||||
|
RadiusY = 50, |
||||
|
}, |
||||
|
} |
||||
|
}, |
||||
|
Fill = Brushes.Blue, |
||||
|
Stroke = Brushes.Red, |
||||
|
StrokeThickness = 1, |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
await RenderToFile(target); |
||||
|
CompareImages(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 3.6 KiB |