@ -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 |