Browse Source

Refactored geometry.

There were a number of problems with `Geometry` and its subclasses and platform implementations. Fix these, for more details see the PR that this commit is a part of.
pull/1349/head
Steven Kirk 8 years ago
parent
commit
d2e1eba876
  1. 33
      src/Avalonia.Visuals/Media/EllipseGeometry.cs
  2. 126
      src/Avalonia.Visuals/Media/Geometry.cs
  3. 3
      src/Avalonia.Visuals/Media/GeometryDrawing.cs
  4. 66
      src/Avalonia.Visuals/Media/LineGeometry.cs
  5. 68
      src/Avalonia.Visuals/Media/PathGeometry.cs
  6. 73
      src/Avalonia.Visuals/Media/PolylineGeometry.cs
  7. 24
      src/Avalonia.Visuals/Media/RectangleGeometry.cs
  8. 20
      src/Avalonia.Visuals/Media/StreamGeometry.cs
  9. 18
      src/Avalonia.Visuals/Platform/IGeometryImpl.cs
  10. 24
      src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs
  11. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
  12. 2
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  13. 19
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  14. 58
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  15. 63
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  16. 2
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  17. 14
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  18. 10
      src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs
  19. 15
      src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs
  20. 3
      tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs
  21. 12
      tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs
  22. 126
      tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs
  23. 2
      tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs
  24. 19
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

33
src/Avalonia.Visuals/Media/EllipseGeometry.cs

@ -17,15 +17,9 @@ namespace Avalonia.Media
public static readonly StyledProperty<Rect> RectProperty =
AvaloniaProperty.Register<EllipseGeometry, Rect>(nameof(Rect));
public Rect Rect
{
get => GetValue(RectProperty);
set => SetValue(RectProperty, value);
}
static EllipseGeometry()
{
RectProperty.Changed.AddClassHandler<EllipseGeometry>(x => x.RectChanged);
AffectsGeometry(RectProperty);
}
/// <summary>
@ -33,8 +27,6 @@ namespace Avalonia.Media
/// </summary>
public EllipseGeometry()
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
}
/// <summary>
@ -46,17 +38,30 @@ namespace Avalonia.Media
Rect = rect;
}
/// <summary>
/// Gets or sets a rect that defines the bounds of the ellipse.
/// </summary>
public Rect Rect
{
get => GetValue(RectProperty);
set => SetValue(RectProperty, value);
}
/// <inheritdoc/>
public override Geometry Clone()
{
return new EllipseGeometry(Rect);
}
private void RectChanged(AvaloniaPropertyChangedEventArgs e)
/// <inheritdoc/>
protected override IGeometryImpl CreateDefiningGeometry()
{
var rect = (Rect)e.NewValue;
using (var ctx = ((IStreamGeometryImpl)PlatformImpl).Open())
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
using (var ctx = geometry.Open())
{
var rect = Rect;
double controlPointRatio = (Math.Sqrt(2) - 1) * 4 / 3;
var center = rect.Center;
var radius = new Vector(rect.Width / 2, rect.Height / 2);
@ -80,6 +85,10 @@ namespace Avalonia.Media
ctx.CubicBezierTo(new Point(x0, y1), new Point(x1, y0), new Point(x2, y0));
ctx.EndFigure(true);
}
return geometry;
}
private void RectChanged(AvaloniaPropertyChangedEventArgs e) => InvalidateGeometry();
}
}

126
src/Avalonia.Visuals/Media/Geometry.cs

@ -17,26 +17,47 @@ namespace Avalonia.Media
public static readonly StyledProperty<Transform> TransformProperty =
AvaloniaProperty.Register<Geometry, Transform>(nameof(Transform));
/// <summary>
/// Initializes static members of the <see cref="Geometry"/> class.
/// </summary>
private bool _isDirty = true;
private IGeometryImpl _platformImpl;
static Geometry()
{
TransformProperty.Changed.AddClassHandler<Geometry>(x => x.TransformChanged);
}
/// <summary>
/// Raised when the geometry changes.
/// </summary>
public event EventHandler Changed;
/// <summary>
/// Gets the geometry's bounding rectangle.
/// </summary>
public Rect Bounds => PlatformImpl.Bounds;
public Rect Bounds => PlatformImpl?.Bounds ?? Rect.Empty;
/// <summary>
/// Gets the platform-specific implementation of the geometry.
/// </summary>
public virtual IGeometryImpl PlatformImpl
public IGeometryImpl PlatformImpl
{
get;
protected set;
get
{
if (_isDirty)
{
var geometry = CreateDefiningGeometry();
var transform = Transform;
if (geometry != null && transform != null && transform.Value != Matrix.Identity)
{
geometry = geometry.WithTransform(transform.Value);
}
_platformImpl = geometry;
_isDirty = false;
}
return _platformImpl;
}
}
/// <summary>
@ -55,14 +76,11 @@ namespace Avalonia.Media
public abstract Geometry Clone();
/// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness.
/// Gets the geometry's bounding rectangle with the specified pen.
/// </summary>
/// <param name="strokeThickness">The stroke thickness.</param>
/// <param name="pen">The stroke thickness.</param>
/// <returns>The bounding rectangle.</returns>
public Rect GetRenderBounds(double strokeThickness)
{
return PlatformImpl.GetRenderBounds(strokeThickness);
}
public Rect GetRenderBounds(Pen pen) => PlatformImpl?.GetRenderBounds(pen) ?? Rect.Empty;
/// <summary>
/// Indicates whether the geometry's fill contains the specified point.
@ -71,7 +89,7 @@ namespace Avalonia.Media
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
public bool FillContains(Point point)
{
return PlatformImpl.FillContains(point);
return PlatformImpl?.FillContains(point) ?? false;
}
/// <summary>
@ -82,13 +100,87 @@ namespace Avalonia.Media
/// <returns><c>true</c> if the geometry contains the point; otherwise, <c>false</c>.</returns>
public bool StrokeContains(Pen pen, Point point)
{
return PlatformImpl.StrokeContains(pen, point);
return PlatformImpl?.StrokeContains(pen, point) ?? false;
}
/// <summary>
/// Marks a property as affecting the geometry's <see cref="PlatformImpl"/>.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateGeometry"/> to be called on the element.
/// </remarks>
protected static void AffectsGeometry(params AvaloniaProperty[] properties)
{
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsGeometryInvalidate);
}
}
/// <summary>
/// Creates the platform implementation of the geometry, without the transform applied.
/// </summary>
/// <returns></returns>
protected abstract IGeometryImpl CreateDefiningGeometry();
/// <summary>
/// Invalidates the platform implementation of the geometry.
/// </summary>
protected void InvalidateGeometry()
{
_isDirty = true;
_platformImpl?.Dispose();
_platformImpl = null;
Changed?.Invoke(this, EventArgs.Empty);
}
private void TransformChanged(AvaloniaPropertyChangedEventArgs e)
{
var transform = (Transform)e.NewValue;
PlatformImpl = PlatformImpl.WithTransform(transform.Value);
var oldValue = (Transform)e.OldValue;
var newValue = (Transform)e.NewValue;
if (oldValue != null)
{
oldValue.Changed -= TransformChanged;
}
if (newValue != null)
{
newValue.Changed += TransformChanged;
}
TransformChanged(newValue, EventArgs.Empty);
}
private void TransformChanged(object sender, EventArgs e)
{
var transform = ((Transform)sender)?.Value;
if (_platformImpl is ITransformedGeometryImpl t)
{
if (transform == null || transform == Matrix.Identity)
{
_platformImpl = t.SourceGeometry;
}
else if (transform != t.Transform)
{
_platformImpl = t.SourceGeometry.WithTransform(transform.Value);
}
}
else if (_platformImpl != null && transform != null && transform != Matrix.Identity)
{
_platformImpl = PlatformImpl.WithTransform(transform.Value);
}
Changed?.Invoke(this, EventArgs.Empty);
}
private static void AffectsGeometryInvalidate(AvaloniaPropertyChangedEventArgs e)
{
var control = e.Sender as Geometry;
control?.InvalidateGeometry();
}
}
}

3
src/Avalonia.Visuals/Media/GeometryDrawing.cs

@ -37,7 +37,8 @@
public override Rect GetBounds()
{
// adding the Pen's stroke thickness here could yield wrong results due to transforms
return Geometry?.GetRenderBounds(0) ?? new Rect();
var pen = new Pen(Brushes.Black, 0);
return Geometry?.GetRenderBounds(pen) ?? new Rect();
}
}
}

66
src/Avalonia.Visuals/Media/LineGeometry.cs

@ -16,29 +16,15 @@ namespace Avalonia.Media
public static readonly StyledProperty<Point> StartPointProperty =
AvaloniaProperty.Register<LineGeometry, Point>(nameof(StartPoint));
public Point StartPoint
{
get => GetValue(StartPointProperty);
set => SetValue(StartPointProperty, value);
}
/// <summary>
/// Defines the <see cref="EndPoint"/> property.
/// </summary>
public static readonly StyledProperty<Point> EndPointProperty =
AvaloniaProperty.Register<LineGeometry, Point>(nameof(EndPoint));
private bool _isDirty = true;
public Point EndPoint
{
get => GetValue(EndPointProperty);
set => SetValue(EndPointProperty, value);
}
static LineGeometry()
{
StartPointProperty.Changed.AddClassHandler<LineGeometry>(x => x.PointsChanged);
EndPointProperty.Changed.AddClassHandler<LineGeometry>(x => x.PointsChanged);
AffectsGeometry(StartPointProperty, EndPointProperty);
}
/// <summary>
@ -46,8 +32,6 @@ namespace Avalonia.Media
/// </summary>
public LineGeometry()
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
}
/// <summary>
@ -61,38 +45,44 @@ namespace Avalonia.Media
EndPoint = endPoint;
}
public override IGeometryImpl PlatformImpl
/// <summary>
/// Gets or sets the start point of the line.
/// </summary>
public Point StartPoint
{
get
{
PrepareIfNeeded();
return base.PlatformImpl;
}
protected set => base.PlatformImpl = value;
get => GetValue(StartPointProperty);
set => SetValue(StartPointProperty, value);
}
public void PrepareIfNeeded()
/// <summary>
/// Gets or sets the end point of the line.
/// </summary>
public Point EndPoint
{
if (_isDirty)
{
_isDirty = false;
using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
{
context.BeginFigure(StartPoint, false);
context.LineTo(EndPoint);
context.EndFigure(false);
}
}
get => GetValue(EndPointProperty);
set => SetValue(EndPointProperty, value);
}
/// <inheritdoc/>
public override Geometry Clone()
{
PrepareIfNeeded();
return new LineGeometry(StartPoint, EndPoint);
}
private void PointsChanged(AvaloniaPropertyChangedEventArgs e) => _isDirty = true;
/// <inheritdoc/>
protected override IGeometryImpl CreateDefiningGeometry()
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
using (var context = geometry.Open())
{
context.BeginFigure(StartPoint, false);
context.LineTo(EndPoint);
context.EndFigure(false);
}
return geometry;
}
}
}

68
src/Avalonia.Visuals/Media/PathGeometry.cs

@ -1,10 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Collections;
using Avalonia.Metadata;
using Avalonia.Platform;
using System;
namespace Avalonia.Media
{
@ -22,12 +22,14 @@ namespace Avalonia.Media
public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<PathGeometry, FillRule>(nameof(FillRule));
private PathFigures _figures;
private IDisposable _figuresObserver = null;
private IDisposable _figuresPropertiesObserver = null;
static PathGeometry()
{
FiguresProperty.Changed.Subscribe(onNext: v =>
{
(v.Sender as PathGeometry)?.OnFiguresChanged(v.OldValue as PathFigures, v.NewValue as PathFigures);
});
FiguresProperty.Changed.AddClassHandler<PathGeometry>((s, e) =>
s.OnFiguresChanged(e.NewValue as PathFigures));
}
/// <summary>
@ -63,61 +65,33 @@ namespace Avalonia.Media
set { SetValue(FillRuleProperty, value); }
}
public override IGeometryImpl PlatformImpl
protected override IGeometryImpl CreateDefiningGeometry()
{
get
{
PrepareIfNeeded();
return base.PlatformImpl;
}
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
protected set
using (var ctx = new StreamGeometryContext(geometry.Open()))
{
base.PlatformImpl = value;
}
}
public override Geometry Clone()
{
PrepareIfNeeded();
return base.Clone();
}
public void PrepareIfNeeded()
{
if (_isDirty)
{
_isDirty = false;
using (var ctx = Open())
ctx.SetFillRule(FillRule);
foreach (var f in Figures)
{
ctx.SetFillRule(FillRule);
foreach (var f in Figures)
{
f.ApplyTo(ctx);
}
f.ApplyTo(ctx);
}
}
}
internal void NotifyChanged()
{
_isDirty = true;
return geometry;
}
private PathFigures _figures;
private IDisposable _figuresObserver = null;
private IDisposable _figuresPropertiesObserver = null;
private bool _isDirty = true;
private void OnFiguresChanged(PathFigures oldValue, PathFigures newValue)
private void OnFiguresChanged(PathFigures figures)
{
_figuresObserver?.Dispose();
_figuresPropertiesObserver?.Dispose();
_figuresObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged());
_figuresPropertiesObserver = newValue?.TrackItemPropertyChanged(t => NotifyChanged());
_figuresObserver = figures?.ForEachItem(
_ => InvalidateGeometry(),
_ => InvalidateGeometry(),
() => InvalidateGeometry());
_figuresPropertiesObserver = figures?.TrackItemPropertyChanged(_ => InvalidateGeometry());
}
}
}

73
src/Avalonia.Visuals/Media/PolylineGeometry.cs

@ -27,14 +27,12 @@ namespace Avalonia.Media
AvaloniaProperty.Register<PolylineGeometry, bool>(nameof(IsFilled));
private Points _points;
private bool _isDirty = true;
private IDisposable _pointsObserver;
static PolylineGeometry()
{
PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) =>
s.OnPointsChanged(e.OldValue as Points, e.NewValue as Points));
IsFilledProperty.Changed.AddClassHandler<PolylineGeometry>((s, _) => s.NotifyChanged());
AffectsGeometry(IsFilledProperty);
PointsProperty.Changed.AddClassHandler<PolylineGeometry>((s, e) => s.OnPointsChanged(e.NewValue as Points));
}
/// <summary>
@ -42,9 +40,6 @@ namespace Avalonia.Media
/// </summary>
public PolylineGeometry()
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
Points = new Points();
}
@ -57,29 +52,6 @@ namespace Avalonia.Media
IsFilled = isFilled;
}
public void PrepareIfNeeded()
{
if (_isDirty)
{
_isDirty = false;
using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
{
var points = Points;
var isFilled = IsFilled;
if (points.Count > 0)
{
context.BeginFigure(points[0], isFilled);
for (int i = 1; i < points.Count; i++)
{
context.LineTo(points[i]);
}
context.EndFigure(isFilled);
}
}
}
}
/// <summary>
/// Gets or sets the figures.
/// </summary>
@ -99,33 +71,42 @@ namespace Avalonia.Media
set => SetValue(IsFilledProperty, value);
}
public override IGeometryImpl PlatformImpl
{
get
{
PrepareIfNeeded();
return base.PlatformImpl;
}
protected set => base.PlatformImpl = value;
}
/// <inheritdoc/>
public override Geometry Clone()
{
PrepareIfNeeded();
return new PolylineGeometry(Points, IsFilled);
}
private void OnPointsChanged(Points oldValue, Points newValue)
protected override IGeometryImpl CreateDefiningGeometry()
{
_pointsObserver?.Dispose();
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
using (var context = geometry.Open())
{
var points = Points;
var isFilled = IsFilled;
if (points.Count > 0)
{
context.BeginFigure(points[0], isFilled);
for (int i = 1; i < points.Count; i++)
{
context.LineTo(points[i]);
}
context.EndFigure(isFilled);
}
}
_pointsObserver = newValue?.ForEachItem(f => NotifyChanged(), f => NotifyChanged(), () => NotifyChanged());
return geometry;
}
internal void NotifyChanged()
private void OnPointsChanged(Points newValue)
{
_isDirty = true;
_pointsObserver?.Dispose();
_pointsObserver = newValue?.ForEachItem(
_ => InvalidateGeometry(),
_ => InvalidateGeometry(),
InvalidateGeometry);
}
}
}

24
src/Avalonia.Visuals/Media/RectangleGeometry.cs

@ -16,6 +16,8 @@ namespace Avalonia.Media
public static readonly StyledProperty<Rect> RectProperty =
AvaloniaProperty.Register<RectangleGeometry, Rect>(nameof(Rect));
bool _isDirty = true;
public Rect Rect
{
get => GetValue(RectProperty);
@ -24,7 +26,7 @@ namespace Avalonia.Media
static RectangleGeometry()
{
RectProperty.Changed.AddClassHandler<RectangleGeometry>(x => x.RectChanged);
AffectsGeometry(RectProperty);
}
/// <summary>
@ -32,36 +34,36 @@ namespace Avalonia.Media
/// </summary>
public RectangleGeometry()
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
}
/// <summary>
/// Initializes a new instance of the <see cref="RectangleGeometry"/> class.
/// </summary>
/// <param name="rect">The rectangle bounds.</param>
public RectangleGeometry(Rect rect) : this()
public RectangleGeometry(Rect rect)
{
Rect = rect;
}
/// <inheritdoc/>
public override Geometry Clone()
{
return new RectangleGeometry(Rect);
}
public override Geometry Clone() => new RectangleGeometry(Rect);
private void RectChanged(AvaloniaPropertyChangedEventArgs e)
protected override IGeometryImpl CreateDefiningGeometry()
{
var rect = (Rect)e.NewValue;
using (var context = ((IStreamGeometryImpl)PlatformImpl).Open())
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
var geometry = factory.CreateStreamGeometry();
using (var context = geometry.Open())
{
var rect = Rect;
context.BeginFigure(rect.TopLeft, true);
context.LineTo(rect.TopRight);
context.LineTo(rect.BottomRight);
context.LineTo(rect.BottomLeft);
context.EndFigure(true);
}
return geometry;
}
}
}

20
src/Avalonia.Visuals/Media/StreamGeometry.cs

@ -10,22 +10,22 @@ namespace Avalonia.Media
/// </summary>
public class StreamGeometry : Geometry
{
IStreamGeometryImpl _impl;
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometry"/> class.
/// </summary>
public StreamGeometry()
{
IPlatformRenderInterface factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
PlatformImpl = factory.CreateStreamGeometry();
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometry"/> class.
/// </summary>
/// <param name="impl">The platform-specific implementation.</param>
private StreamGeometry(IGeometryImpl impl)
private StreamGeometry(IStreamGeometryImpl impl)
{
PlatformImpl = impl;
_impl = impl;
}
/// <summary>
@ -61,5 +61,17 @@ namespace Avalonia.Media
{
return new StreamGeometryContext(((IStreamGeometryImpl)PlatformImpl).Open());
}
/// <inheritdoc/>
protected override IGeometryImpl CreateDefiningGeometry()
{
if (_impl == null)
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
_impl = factory.CreateStreamGeometry();
}
return _impl;
}
}
}

18
src/Avalonia.Visuals/Platform/IGeometryImpl.cs

@ -1,14 +1,15 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Media;
namespace Avalonia.Platform
{
/// <summary>
/// Defines the platform-specific interface for <see cref="Avalonia.Media.Geometry"/>.
/// Defines the platform-specific interface for a <see cref="Geometry"/>.
/// </summary>
public interface IGeometryImpl
public interface IGeometryImpl : IDisposable
{
/// <summary>
/// Gets the geometry's bounding rectangle.
@ -16,16 +17,11 @@ namespace Avalonia.Platform
Rect Bounds { get; }
/// <summary>
/// Gets the transform to applied to the geometry.
/// Gets the geometry's bounding rectangle with the specified pen.
/// </summary>
Matrix Transform { get; }
/// <summary>
/// Gets the geometry's bounding rectangle with the specified stroke thickness.
/// </summary>
/// <param name="strokeThickness">The stroke thickness.</param>
/// <param name="pen">The pen to use. May be null.</param>
/// <returns>The bounding rectangle.</returns>
Rect GetRenderBounds(double strokeThickness);
Rect GetRenderBounds(Pen pen);
/// <summary>
/// Indicates whether the geometry's fill contains the specified point.
@ -54,6 +50,6 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="transform">The transform.</param>
/// <returns>The cloned geometry.</returns>
IGeometryImpl WithTransform(Matrix transform);
ITransformedGeometryImpl WithTransform(Matrix transform);
}
}

24
src/Avalonia.Visuals/Platform/ITransformedGeometryImpl.cs

@ -0,0 +1,24 @@
using System;
namespace Avalonia.Platform
{
/// <summary>
/// Represents a geometry with a transform applied.
/// </summary>
/// <remarks>
/// An <see cref="ITransformedGeometryImpl"/> transforms a geometry without transforming its
/// stroke thickness.
/// </remarks>
public interface ITransformedGeometryImpl : IGeometryImpl
{
/// <summary>
/// Gets the source geometry that the <see cref="Transform"/> is applied to.
/// </summary>
IGeometryImpl SourceGeometry { get; }
/// <summary>
/// Gets the applied transform.
/// </summary>
Matrix Transform { get; }
}
}

2
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
Pen pen,
IGeometryImpl geometry,
IDictionary<IVisual, Scene> childScenes = null)
: base(geometry.GetRenderBounds(pen?.Thickness ?? 0), transform, null)
: base(geometry.GetRenderBounds(pen), transform, null)
{
Transform = transform;
Brush = brush?.ToImmutable();

2
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -68,7 +68,7 @@ namespace Avalonia.Skia
public void DrawGeometry(IBrush brush, Pen pen, IGeometryImpl geometry)
{
var impl = (StreamGeometryImpl)geometry;
var impl = (GeometryImpl)geometry;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))

19
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -0,0 +1,19 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
abstract class GeometryImpl : IGeometryImpl
{
public abstract Rect Bounds { get; }
public abstract SKPath EffectivePath { get; }
public abstract void Dispose();
public abstract bool FillContains(Point point);
public abstract Rect GetRenderBounds(Pen pen);
public abstract IGeometryImpl Intersect(IGeometryImpl geometry);
public abstract bool StrokeContains(Pen pen, Point point);
public abstract ITransformedGeometryImpl WithTransform(Matrix transform);
}
}

58
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@ -1,46 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.RenderHelpers;
using SkiaSharp;
namespace Avalonia.Skia
{
class StreamGeometryImpl : IStreamGeometryImpl
class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{
Rect _bounds;
SKPath _path;
private Matrix _transform = Matrix.Identity;
public override SKPath EffectivePath => _path;
public SKPath EffectivePath => _path;
public Rect GetRenderBounds(double strokeThickness)
public override Rect GetRenderBounds(Pen pen)
{
// TODO: Calculate properly.
return Bounds.TransformToAABB(Transform).Inflate(strokeThickness);
return GetRenderBounds(pen?.Thickness ?? 0);
}
public Rect Bounds { get; private set; }
public Matrix Transform
{
get { return _transform; }
}
public override Rect Bounds => _bounds;
public IStreamGeometryImpl Clone()
{
return new StreamGeometryImpl
{
_path = _path?.Clone(),
_transform = Transform,
Bounds = Bounds
_bounds = Bounds
};
}
public override void Dispose() => _path.Dispose();
public IStreamGeometryContextImpl Open()
{
_path = new SKPath();
@ -49,41 +38,34 @@ namespace Avalonia.Skia
return new StreamContext(this);
}
public bool FillContains(Point point)
public override bool FillContains(Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
// return EffectivePath.Contains(point.X, point.Y);
return GetRenderBounds(0).Contains(point);
}
public bool StrokeContains(Pen pen, Point point)
public override bool StrokeContains(Pen pen, Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
// return EffectivePath.Contains(point.X, point.Y);
return GetRenderBounds(0).Contains(point);
}
public IGeometryImpl Intersect(IGeometryImpl geometry)
public override IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
public IGeometryImpl WithTransform(Matrix transform)
public override ITransformedGeometryImpl WithTransform(Matrix transform)
{
var result = (StreamGeometryImpl)Clone();
if (result.Transform != Matrix.Identity)
{
result._path.Transform(result.Transform.Invert().ToSKMatrix());
}
if (transform != Matrix.Identity)
{
result._path.Transform(transform.ToSKMatrix());
}
return new TransformedGeometryImpl(this, transform);
}
result._transform = transform;
return result;
private Rect GetRenderBounds(double strokeThickness)
{
// TODO: Calculate properly.
return Bounds.Inflate(strokeThickness);
}
class StreamContext : IStreamGeometryContextImpl
@ -102,7 +84,7 @@ namespace Avalonia.Skia
{
SKRect rc;
_path.GetBounds(out rc);
_geometryImpl.Bounds = rc.ToAvaloniaRect();
_geometryImpl._bounds = rc.ToAvaloniaRect();
}
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)

63
src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs

@ -0,0 +1,63 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia
{
class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{
public TransformedGeometryImpl(GeometryImpl source, Matrix transform)
{
SourceGeometry = source;
Transform = transform;
EffectivePath = source.EffectivePath.Clone();
EffectivePath.Transform(transform.ToSKMatrix());
}
public override SKPath EffectivePath { get; }
public IGeometryImpl SourceGeometry { get; }
public Matrix Transform { get; }
public override Rect Bounds => SourceGeometry.Bounds.TransformToAABB(Transform);
public override void Dispose()
{
}
public override bool FillContains(Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
return GetRenderBounds(0).Contains(point);
}
public override Rect GetRenderBounds(Pen pen)
{
return GetRenderBounds(pen.Thickness);
}
public override IGeometryImpl Intersect(IGeometryImpl geometry)
{
throw new NotImplementedException();
}
public override bool StrokeContains(Pen pen, Point point)
{
// TODO: Not supported by SkiaSharp yet, so use expanded Rect
return GetRenderBounds(0).Contains(point);
}
public override ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new TransformedGeometryImpl(this, transform);
}
public Rect GetRenderBounds(double strokeThickness)
{
// TODO: Calculate properly.
return Bounds.Inflate(strokeThickness);
}
}
}

2
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -188,7 +188,7 @@ namespace Avalonia.Direct2D1.Media
if (pen != null)
{
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen.Thickness).Size))
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_renderTarget))
{
if (d2dBrush.PlatformBrush != null)

14
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -23,13 +23,13 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc/>
public Geometry Geometry { get; }
/// <inheritdoc/>
public virtual Matrix Transform => Matrix.Identity;
public void Dispose() => Geometry.Dispose();
/// <inheritdoc/>
public Rect GetRenderBounds(double strokeThickness)
public Rect GetRenderBounds(Avalonia.Media.Pen pen)
{
return Geometry.GetWidenedBounds((float)strokeThickness).ToAvalonia();
var factory = AvaloniaLocator.Current.GetService<Factory>();
return Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
}
/// <inheritdoc/>
@ -56,15 +56,15 @@ namespace Avalonia.Direct2D1.Media
return Geometry.StrokeContainsPoint(point.ToSharpDX(), (float)pen.Thickness);
}
/// <inheritdoc/>
public IGeometryImpl WithTransform(Matrix transform)
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
var factory = AvaloniaLocator.Current.GetService<Factory>();
return new TransformedGeometryImpl(
new TransformedGeometry(
factory,
GetSourceGeometry(),
transform.ToDirect2D()));
transform.ToDirect2D()),
this);
}
protected virtual Geometry GetSourceGeometry() => Geometry;

10
src/Windows/Avalonia.Direct2D1/Media/TransformedGeometryImpl.cs

@ -1,23 +1,27 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Platform;
using SharpDX.Direct2D1;
namespace Avalonia.Direct2D1.Media
{
public class TransformedGeometryImpl : GeometryImpl
public class TransformedGeometryImpl : GeometryImpl, ITransformedGeometryImpl
{
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="geometry">An existing Direct2D <see cref="TransformedGeometry"/>.</param>
public TransformedGeometryImpl(TransformedGeometry geometry)
public TransformedGeometryImpl(TransformedGeometry geometry, GeometryImpl source)
: base(geometry)
{
SourceGeometry = source;
}
public IGeometryImpl SourceGeometry { get; }
/// <inheritdoc/>
public override Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
public Matrix Transform => ((TransformedGeometry)Geometry).Transform.ToAvalonia();
protected override Geometry GetSourceGeometry() => ((TransformedGeometry)Geometry).SourceGeometry;
}

15
src/Windows/Avalonia.Direct2D1/PrimitiveExtensions.cs

@ -105,7 +105,18 @@ namespace Avalonia.Direct2D1
/// <param name="pen">The pen to convert.</param>
/// <param name="target">The render target.</param>
/// <returns>The Direct2D brush.</returns>
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget target)
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, SharpDX.Direct2D1.RenderTarget renderTarget)
{
return pen.ToDirect2DStrokeStyle(renderTarget.Factory);
}
/// <summary>
/// Converts a pen to a Direct2D stroke style.
/// </summary>
/// <param name="pen">The pen to convert.</param>
/// <param name="target">The render target.</param>
/// <returns>The Direct2D brush.</returns>
public static StrokeStyle ToDirect2DStrokeStyle(this Avalonia.Media.Pen pen, Factory factory)
{
var properties = new StrokeStyleProperties
{
@ -123,7 +134,7 @@ namespace Avalonia.Direct2D1
properties.DashOffset = (float)pen.DashStyle.Offset;
dashes = pen.DashStyle?.Dashes.Select(x => (float)x).ToArray();
}
return new StrokeStyle(target.Factory, properties, dashes);
return new StrokeStyle(factory, properties, dashes);
}
/// <summary>

3
tests/Avalonia.Direct2D1.UnitTests/Media/GeometryTests.cs

@ -31,8 +31,9 @@ namespace Avalonia.Direct2D1.UnitTests.Media
Direct2D1Platform.Initialize();
var target = StreamGeometry.Parse("M 0 2 L 4 6 L 0 10 Z");
var pen = new Pen(Brushes.Black, 2);
Assert.Equal(new Rect(-1, -0.414, 6.414, 12.828), target.GetRenderBounds(2), Compare);
Assert.Equal(new Rect(-1, -0.414, 6.414, 12.828), target.GetRenderBounds(pen), Compare);
}
}
}

12
tests/Avalonia.UnitTests/MockStreamGeometryImpl.cs

@ -5,7 +5,7 @@ using Avalonia.Platform;
namespace Avalonia.UnitTests
{
public class MockStreamGeometryImpl : IStreamGeometryImpl
public class MockStreamGeometryImpl : IStreamGeometryImpl, ITransformedGeometryImpl
{
private MockStreamGeometryContext _context;
@ -27,6 +27,8 @@ namespace Avalonia.UnitTests
_context = context;
}
public IGeometryImpl SourceGeometry { get; }
public Rect Bounds => _context.CalculateBounds();
public Matrix Transform { get; }
@ -36,6 +38,10 @@ namespace Avalonia.UnitTests
return this;
}
public void Dispose()
{
}
public bool FillContains(Point point)
{
return _context.FillContains(point);
@ -46,7 +52,7 @@ namespace Avalonia.UnitTests
return false;
}
public Rect GetRenderBounds(double strokeThickness) => Bounds;
public Rect GetRenderBounds(Pen pen) => Bounds;
public IGeometryImpl Intersect(IGeometryImpl geometry)
{
@ -58,7 +64,7 @@ namespace Avalonia.UnitTests
return _context;
}
public IGeometryImpl WithTransform(Matrix transform)
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
return new MockStreamGeometryImpl(transform, _context);
}

126
tests/Avalonia.Visuals.UnitTests/Media/GeometryTests.cs

@ -0,0 +1,126 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
using Moq;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class GeometryTests
{
[Fact]
public void Changing_AffectsGeometry_Property_Causes_PlatformImpl_To_Be_Updated()
{
var target = new TestGeometry();
var platformImpl = target.PlatformImpl;
target.Foo = true;
Assert.NotSame(platformImpl, target.PlatformImpl);
}
[Fact]
public void Changing_AffectsGeometry_Property_Causes_Changed_To_Be_Raised()
{
var target = new TestGeometry();
var raised = false;
target.Changed += (s, e) => raised = true;
target.Foo = true;
Assert.True(raised);
}
[Fact]
public void Old_PlatformImpl_Is_Disposed_When_Updated()
{
var target = new TestGeometry();
var platformImpl = target.PlatformImpl;
target.Foo = true;
Mock.Get(platformImpl).Verify(x => x.Dispose());
}
[Fact]
public void Setting_Transform_Causes_Changed_To_Be_Raised()
{
var target = new TestGeometry();
var raised = false;
target.Changed += (s, e) => raised = true;
target.Transform = new RotateTransform(45);
Assert.True(raised);
}
[Fact]
public void Changing_Transform_Causes_Changed_To_Be_Raised()
{
var transform = new RotateTransform(45);
var target = new TestGeometry { Transform = transform };
var raised = false;
target.Changed += (s, e) => raised = true;
transform.Angle = 90;
Assert.True(raised);
}
[Fact]
public void Removing_Transform_Causes_Changed_To_Be_Raised()
{
var transform = new RotateTransform(45);
var target = new TestGeometry { Transform = transform };
var raised = false;
target.Changed += (s, e) => raised = true;
target.Transform = null;
Assert.True(raised);
}
[Fact]
public void Transform_Produces_Transformed_PlatformImpl()
{
var target = new TestGeometry();
var rotate = new RotateTransform(45);
Assert.False(target.PlatformImpl is ITransformedGeometryImpl);
target.Transform = rotate;
Assert.True(target.PlatformImpl is ITransformedGeometryImpl);
rotate.Angle = 0;
Assert.False(target.PlatformImpl is ITransformedGeometryImpl);
}
private class TestGeometry : Geometry
{
public static readonly AvaloniaProperty<bool> FooProperty =
AvaloniaProperty.Register<TestGeometry, bool>(nameof(Foo));
static TestGeometry()
{
AffectsGeometry(FooProperty);
}
public bool Foo
{
get => GetValue(FooProperty);
set => SetValue(FooProperty, value);
}
public override Geometry Clone()
{
throw new NotImplementedException();
}
protected override IGeometryImpl CreateDefiningGeometry()
{
return Mock.Of<IGeometryImpl>(
x => x.WithTransform(It.IsAny<Matrix>()) ==
Mock.Of<ITransformedGeometryImpl>(y =>
y.SourceGeometry == x));
}
}
}
}

2
tests/Avalonia.Visuals.UnitTests/Media/RectangleGeometryTests.cs

@ -27,7 +27,7 @@ namespace Avalonia.Visuals.UnitTests.Media
private TestServices GetServices()
{
var context = Mock.Of<IStreamGeometryContextImpl>();
var transformedGeometry = new Mock<IGeometryImpl>();
var transformedGeometry = new Mock<ITransformedGeometryImpl>();
var streamGeometry = Mock.Of<IStreamGeometryImpl>(x =>
x.Open() == context &&
x.WithTransform(It.IsAny<Matrix>()) == transformedGeometry.Object);

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

@ -65,22 +65,13 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
}
}
public Matrix Transform
public IStreamGeometryImpl Clone()
{
get
{
throw new NotImplementedException();
}
set
{
throw new NotImplementedException();
}
return this;
}
public IStreamGeometryImpl Clone()
public void Dispose()
{
return this;
}
public bool FillContains(Point point)
@ -88,7 +79,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return _impl.FillContains(point);
}
public Rect GetRenderBounds(double strokeThickness)
public Rect GetRenderBounds(Pen pen)
{
throw new NotImplementedException();
}
@ -108,7 +99,7 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
throw new NotImplementedException();
}
public IGeometryImpl WithTransform(Matrix transform)
public ITransformedGeometryImpl WithTransform(Matrix transform)
{
throw new NotImplementedException();
}

Loading…
Cancel
Save