@ -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 }; |
||||
|
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,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,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,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(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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: 2.3 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.6 KiB |