Browse Source

Add isStroked overload for IGeometryContext (#15430)

* add isStroked overload for IGeometryContext

* move IGeometryContextEx

* fix Fill Path line

* remove added whitespaces

* close a figure to non-stroke lines with a line

* add geometry tests

* add wpf test files

* Added a test for tracking path closure with missing strokes for various line caps/joins

* add IsStroke overload for other segments

* update tests

* Skip line join test for closed geometry with holes for now

---------

Co-authored-by: Nikita Tsukanov <keks9n@gmail.com>
release/11.1.0-rc1
Emmanuel Hansen 2 years ago
committed by Max Katz
parent
commit
9395904b93
  1. 2
      src/Avalonia.Base/Media/ArcSegment.cs
  2. 2
      src/Avalonia.Base/Media/BezierSegment .cs
  3. 2
      src/Avalonia.Base/Media/LineSegment.cs
  4. 9
      src/Avalonia.Base/Media/PathSegment.cs
  5. 2
      src/Avalonia.Base/Media/QuadraticBezierSegment .cs
  6. 43
      src/Avalonia.Base/Media/StreamGeometryContext.cs
  7. 46
      src/Avalonia.Base/Platform/IGeometryContext2.cs
  8. 128
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  9. 346
      tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs
  10. 159
      tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs
  11. 378
      tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs
  12. 60
      tests/Avalonia.RenderTests/CrossUI/CrossUI.cs
  13. 2
      tests/Avalonia.Skia.RenderTests/CrossTestBase.cs
  14. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Bevel.wpf.png
  15. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Miter.wpf.png
  16. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Round.wpf.png
  17. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Bevel.wpf.png
  18. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Miter.wpf.png
  19. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Round.wpf.png
  20. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Geometry_With_Strokeless_Lines.wpf.png
  21. BIN
      tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Stream_Geometry.wpf.png

2
src/Avalonia.Base/Media/ArcSegment.cs

@ -97,7 +97,7 @@ namespace Avalonia.Media
internal override void ApplyTo(StreamGeometryContext ctx)
{
ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection);
ctx.ArcTo(Point, Size, RotationAngle, IsLargeArc, SweepDirection, IsStroked);
}
public override string ToString()

2
src/Avalonia.Base/Media/BezierSegment .cs

@ -58,7 +58,7 @@ namespace Avalonia.Media
internal override void ApplyTo(StreamGeometryContext ctx)
{
ctx.CubicBezierTo(Point1, Point2, Point3);
ctx.CubicBezierTo(Point1, Point2, Point3, IsStroked);
}
public override string ToString()

2
src/Avalonia.Base/Media/LineSegment.cs

@ -24,7 +24,7 @@ namespace Avalonia.Media
internal override void ApplyTo(StreamGeometryContext ctx)
{
ctx.LineTo(Point);
ctx.LineTo(Point, IsStroked);
}
public override string ToString()

9
src/Avalonia.Base/Media/PathSegment.cs

@ -3,5 +3,14 @@ namespace Avalonia.Media
public abstract class PathSegment : AvaloniaObject
{
internal abstract void ApplyTo(StreamGeometryContext ctx);
public static readonly StyledProperty<bool> IsStrokedProperty =
AvaloniaProperty.Register<PathSegment, bool>(nameof(IsStroked), true);
public bool IsStroked
{
get => GetValue(IsStrokedProperty);
set => SetValue(IsStrokedProperty, value);
}
}
}

2
src/Avalonia.Base/Media/QuadraticBezierSegment .cs

@ -42,7 +42,7 @@ namespace Avalonia.Media
internal override void ApplyTo(StreamGeometryContext ctx)
{
ctx.QuadraticBezierTo(Point1, Point2);
ctx.QuadraticBezierTo(Point1, Point2, IsStroked);
}
public override string ToString()

43
src/Avalonia.Base/Media/StreamGeometryContext.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media
/// of <see cref="StreamGeometryContext"/> is obtained by calling
/// <see cref="StreamGeometry.Open"/>.
/// </remarks>
public class StreamGeometryContext : IGeometryContext
public class StreamGeometryContext : IGeometryContext, IGeometryContext2
{
private readonly IStreamGeometryContextImpl _impl;
@ -102,5 +102,46 @@ namespace Avalonia.Media
{
_impl.Dispose();
}
/// <inheritdoc/>
public void LineTo(Point point, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.LineTo(point, isStroked);
else
_impl.LineTo(point);
_currentPoint = point;
}
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection, isStroked);
else
_impl.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection);
_currentPoint = point;
}
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.CubicBezierTo(controlPoint1, controlPoint2, endPoint, isStroked);
else
_impl.CubicBezierTo(controlPoint1, controlPoint2, endPoint);
_currentPoint = endPoint;
}
public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked)
{
if (_impl is IGeometryContext2 context2)
context2.QuadraticBezierTo(controlPoint, endPoint, isStroked);
else
_impl.QuadraticBezierTo(controlPoint, endPoint);
_currentPoint = endPoint;
}
}
}

46
src/Avalonia.Base/Platform/IGeometryContext2.cs

@ -0,0 +1,46 @@
using Avalonia.Media;
namespace Avalonia.Platform
{
// TODO12 combine with IGeometryContext
public interface IGeometryContext2 : IGeometryContext
{
/// <summary>
/// Draws a line to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void LineTo(Point point, bool isStroked);
/// <summary>
/// Draws an arc to the specified point.
/// </summary>
/// <param name="point">The destination point.</param>
/// <param name="size">The radii of an oval whose perimeter is used to draw the angle.</param>
/// <param name="rotationAngle">The rotation angle (in radians) of the oval that specifies the curve.</param>
/// <param name="isLargeArc">true to draw the arc greater than 180 degrees; otherwise, false.</param>
/// <param name="sweepDirection">
/// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction.
/// </param>
/// <param name="isStroked">Whether the segment is stroked</param>
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked);
/// <summary>
/// Draws a Bezier curve to the specified point.
/// </summary>
/// <param name="controlPoint1">The first control point used to specify the shape of the curve.</param>
/// <param name="controlPoint2">The second control point used to specify the shape of the curve.</param>
/// <param name="endPoint">The destination point for the end of the curve.</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked);
/// <summary>
/// Draws a quadratic Bezier curve to the specified point
/// </summary>
/// <param name="controlPoint ">Control point</param>
/// <param name="endPoint">DestinationPoint</param>
/// <param name="isStroked">Whether the segment is stroked</param>
void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked);
}
}

128
src/Skia/Avalonia.Skia/StreamGeometryImpl.cs

@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
@ -77,12 +78,14 @@ namespace Avalonia.Skia
/// <summary>
/// A Skia implementation of a <see cref="IStreamGeometryContextImpl"/>.
/// </summary>
private class StreamContext : IStreamGeometryContextImpl
private class StreamContext : IStreamGeometryContextImpl, IGeometryContext2
{
private readonly StreamGeometryImpl _geometryImpl;
private SKPath Stroke => _geometryImpl._strokePath;
private SKPath Fill => _geometryImpl._fillPath ??= new();
private bool _isFilled;
private Point _startPoint;
private bool _isFigureBroken;
private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke);
/// <summary>
@ -93,7 +96,7 @@ namespace Avalonia.Skia
{
_geometryImpl = geometryImpl;
}
/// <inheritdoc />
/// <remarks>Will update bounds of passed geometry.</remarks>
public void Dispose()
@ -117,7 +120,7 @@ namespace Avalonia.Skia
sweep,
(float)point.X,
(float)point.Y);
if(Duplicate)
if (Duplicate)
Fill.ArcTo(
(float)size.Width,
(float)size.Height,
@ -136,10 +139,12 @@ namespace Avalonia.Skia
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
}
_isFilled = isFilled;
_startPoint = startPoint;
_isFigureBroken = false;
Stroke.MoveTo((float)startPoint.X, (float)startPoint.Y);
if(Duplicate)
if (Duplicate)
Fill.MoveTo((float)startPoint.X, (float)startPoint.Y);
}
@ -147,7 +152,7 @@ namespace Avalonia.Skia
public void CubicBezierTo(Point point1, Point point2, Point point3)
{
Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
if(Duplicate)
if (Duplicate)
Fill.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
}
@ -155,7 +160,7 @@ namespace Avalonia.Skia
public void QuadraticBezierTo(Point point1, Point point2)
{
Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
if(Duplicate)
if (Duplicate)
Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
}
@ -163,7 +168,7 @@ namespace Avalonia.Skia
public void LineTo(Point point)
{
Stroke.LineTo((float)point.X, (float)point.Y);
if(Duplicate)
if (Duplicate)
Fill.LineTo((float)point.X, (float)point.Y);
}
@ -172,7 +177,13 @@ namespace Avalonia.Skia
{
if (isClosed)
{
Stroke.Close();
if (_isFigureBroken)
{
LineTo(_startPoint);
_isFigureBroken = false;
}
else
Stroke.Close();
if (Duplicate)
Fill.Close();
}
@ -183,6 +194,105 @@ namespace Avalonia.Skia
{
Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
}
/// <inheritdoc />
public void LineTo(Point point, bool isStroked)
{
if (isStroked)
{
Stroke.LineTo((float)point.X, (float)point.Y);
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
_isFigureBroken = true;
Stroke.MoveTo((float)point.X, (float)point.Y);
}
if (Duplicate)
Fill.LineTo((float)point.X, (float)point.Y);
}
/// <inheritdoc />
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked)
{
var arc = isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small;
var sweep = sweepDirection == SweepDirection.Clockwise
? SKPathDirection.Clockwise
: SKPathDirection.CounterClockwise;
if (isStroked)
{
Stroke.ArcTo(
(float)size.Width,
(float)size.Height,
(float)rotationAngle,
arc,
sweep,
(float)point.X,
(float)point.Y);
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
_isFigureBroken = true;
Stroke.MoveTo((float)point.X, (float)point.Y);
}
if (Duplicate)
Fill.ArcTo(
(float)size.Width,
(float)size.Height,
(float)rotationAngle,
arc,
sweep,
(float)point.X,
(float)point.Y);
}
/// <inheritdoc />
public void CubicBezierTo(Point point1, Point point2, Point point3, bool isStroked)
{
if (isStroked)
{
Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
_isFigureBroken = true;
Stroke.MoveTo((float)point3.X, (float)point3.Y);
}
if (Duplicate)
Fill.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
}
/// <inheritdoc />
public void QuadraticBezierTo(Point point1, Point point2, bool isStroked)
{
if (isStroked)
{
Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
_isFigureBroken = true;
Stroke.MoveTo((float)point2.X, (float)point2.Y);
}
if (Duplicate)
Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
}
}
}
}

346
tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs

@ -31,158 +31,272 @@ using WSize = System.Windows.Size;
using WRect = System.Windows.Rect;
using WColor = System.Windows.Media.Color;
using WMatrix = System.Windows.Media.Matrix;
namespace Avalonia.RenderTests.WpfCompare;
using Avalonia.RenderTests.WpfCompare;
using PenLineCap = Avalonia.Media.PenLineCap;
using WPenLineCap = System.Windows.Media.PenLineCap;
using PenLineJoin = Avalonia.Media.PenLineJoin;
using WPenLineJoin = System.Windows.Media.PenLineJoin;
internal static class WpfConvertExtensions
namespace CrossUI
{
public static WPoint ToWpf(this Point pt) => new(pt.X, pt.Y);
public static WSize ToWpf(this Size size) => new(size.Width, size.Height);
public static WRect ToWpf(this Rect rect) => new(rect.Left, rect.Top, rect.Width, rect.Height);
public static WColor ToWpf(this Color color) => WColor.FromArgb(color.A, color.R, color.G, color.B);
public static WMatrix ToWpf(this Matrix m) => new WMatrix(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32);
}
internal class WpfCrossControl : Panel
{
private readonly CrossControl _src;
private readonly Dictionary<CrossControl, WpfCrossControl> _children;
public WpfCrossControl(CrossControl src)
public partial class CrossGlobals
{
_src = src;
_children = src.Children.ToDictionary(x => x, x => new WpfCrossControl(x));
Width = src.Bounds.Width;
Height = src.Bounds.Height;
RenderTransform = new MatrixTransform(src.RenderTransform.ToWpf());
foreach (var ch in src.Children)
public static ICrossStreamGeometryContextImplProvider GetContextImplProvider()
{
var c = _children[ch];
this.Children.Add(c);
return new WpfCrossStreamGeometryContextImplProvider();
}
}
protected override WSize MeasureOverride(WSize availableSize)
{
foreach (var ch in _children)
ch.Value.Measure(ch.Key.Bounds.Size.ToWpf());
return _src.Bounds.Size.ToWpf();
}
protected override WSize ArrangeOverride(WSize finalSize)
public class WpfCrossStreamGeometryContextImplProvider : ICrossStreamGeometryContextImplProvider
{
foreach (var ch in _children)
ch.Value.Arrange(ch.Key.Bounds.ToWpf());
return base.ArrangeOverride(finalSize);
public ICrossStreamGeometryContextImpl Create()
{
return new WpfCrossStreamGeometryContextImpl();
}
}
protected override void OnRender(DrawingContext context)
public class WpfCrossStreamGeometryContextImpl : ICrossStreamGeometryContextImpl
{
_src.Render(new WpfCrossDrawingContext(context));
private StreamGeometry _streamGeometry;
private StreamGeometryContext _context;
public WpfCrossStreamGeometryContextImpl()
{
_streamGeometry = new StreamGeometry();
_context = _streamGeometry.Open();
}
public void ArcTo(Avalonia.Point point, Avalonia.Size size, double rotationAngle, bool isLargeArc, Avalonia.Media.SweepDirection sweepDirection, bool isStroked)
{
_context.ArcTo(point.ToWpf(), size.ToWpf(), rotationAngle, isLargeArc, (SweepDirection)sweepDirection, isStroked, true);
}
public void BeginFigure(Avalonia.Point point, bool isFilled, bool isClosed)
{
_context.BeginFigure(point.ToWpf(), isFilled, isClosed);
}
public void CubicBezierTo(Avalonia.Point controlPoint1, Avalonia.Point controlPoint2, Avalonia.Point endPoint, bool isStroked)
{
_context.BezierTo(controlPoint1.ToWpf(), controlPoint2.ToWpf(), endPoint.ToWpf(), isStroked, true);
}
public void Dispose()
{
_context.Close();
}
public void EndFigure()
{
Dispose();
}
public object GetGeometry()
{
return _streamGeometry;
}
public void LineTo(Avalonia.Point point, bool isStroked)
{
_context.LineTo(point.ToWpf(), isStroked, true);
}
public void QuadraticBezierTo(Avalonia.Point controlPoint, Avalonia.Point endPoint, bool isStroked)
{
_context.QuadraticBezierTo(controlPoint.ToWpf(), endPoint.ToWpf(), isStroked, true);
}
}
}
internal class WpfCrossDrawingContext : ICrossDrawingContext
namespace Avalonia.RenderTests.WpfCompare
{
private readonly DrawingContext _ctx;
public WpfCrossDrawingContext(DrawingContext ctx)
internal static class WpfConvertExtensions
{
_ctx = ctx;
public static WPoint ToWpf(this Point pt) => new(pt.X, pt.Y);
public static WSize ToWpf(this Size size) => new(size.Width, size.Height);
public static WRect ToWpf(this Rect rect) => new(rect.Left, rect.Top, rect.Width, rect.Height);
public static WColor ToWpf(this Color color) => WColor.FromArgb(color.A, color.R, color.G, color.B);
public static WMatrix ToWpf(this Matrix m) => new WMatrix(m.M11, m.M12, m.M21, m.M22, m.M31, m.M32);
}
private static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value.ToWpf());
private static Geometry ConvertGeometry(CrossGeometry g)
internal class WpfCrossControl : Panel
{
if (g is CrossRectangleGeometry rg)
return new RectangleGeometry(rg.Rect.ToWpf());
else if (g is CrossSvgGeometry svg)
return Geometry.Parse(svg.Path);
else if (g is CrossEllipseGeometry ellipse)
return new EllipseGeometry(ellipse.Rect.ToWpf());
throw new NotSupportedException();
}
private readonly CrossControl _src;
private readonly Dictionary<CrossControl, WpfCrossControl> _children;
private static Drawing ConvertDrawing(CrossDrawing src)
{
if (src is CrossDrawingGroup g)
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
if (src is CrossGeometryDrawing geo)
return new GeometryDrawing()
public WpfCrossControl(CrossControl src)
{
_src = src;
_children = src.Children.ToDictionary(x => x, x => new WpfCrossControl(x));
Width = src.Bounds.Width;
Height = src.Bounds.Height;
RenderTransform = new MatrixTransform(src.RenderTransform.ToWpf());
foreach (var ch in src.Children)
{
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
};
throw new NotSupportedException();
var c = _children[ch];
this.Children.Add(c);
}
}
protected override WSize MeasureOverride(WSize availableSize)
{
foreach (var ch in _children)
ch.Value.Measure(ch.Key.Bounds.Size.ToWpf());
return _src.Bounds.Size.ToWpf();
}
protected override WSize ArrangeOverride(WSize finalSize)
{
foreach (var ch in _children)
ch.Value.Arrange(ch.Key.Bounds.ToWpf());
return base.ArrangeOverride(finalSize);
}
protected override void OnRender(DrawingContext context)
{
_src.Render(new WpfCrossDrawingContext(context));
}
}
private static Brush? ConvertBrush(CrossBrush? brush)
internal class WpfCrossDrawingContext : ICrossDrawingContext
{
if (brush == null)
return null;
static Brush Sync(Brush dst, CrossBrush src)
private readonly DrawingContext _ctx;
public WpfCrossDrawingContext(DrawingContext ctx)
{
dst.Opacity = src.Opacity;
dst.Transform = ConvertTransform(src.Transform);
dst.RelativeTransform = ConvertTransform(src.RelativeTransform);
return dst;
_ctx = ctx;
}
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
private static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value.ToWpf());
private static Geometry ConvertGeometry(CrossGeometry g)
{
dst.Stretch = (Stretch)src.Stretch;
dst.AlignmentX = (AlignmentX)src.AlignmentX;
dst.AlignmentY = (AlignmentY)src.AlignmentY;
dst.TileMode = (TileMode)src.TileMode;
dst.Viewbox = src.Viewbox.ToWpf();
dst.ViewboxUnits = (BrushMappingMode)src.ViewboxUnits;
dst.Viewport = src.Viewport.ToWpf();
dst.ViewportUnits = (BrushMappingMode)src.ViewportUnits;
return Sync(dst, src);
if (g is CrossRectangleGeometry rg)
return new RectangleGeometry(rg.Rect.ToWpf());
else if (g is CrossSvgGeometry svg)
return Geometry.Parse(svg.Path);
else if (g is CrossEllipseGeometry ellipse)
return new EllipseGeometry(ellipse.Rect.ToWpf());
else if (g is CrossStreamGeometry streamGeometry)
return (StreamGeometry)streamGeometry.GetContext().GetGeometry();
else if (g is CrossPathGeometry pathGeometry)
return new PathGeometry()
{
Figures = new PathFigureCollection(pathGeometry.Figures.Select(f => new PathFigure(
f.Start.ToWpf(), f.Segments.Select<CrossPathSegment, PathSegment>(s =>
s switch
{
CrossPathSegment.Line line => new LineSegment(line.To.ToWpf(), s.IsStroked),
CrossPathSegment.Arc arc => new ArcSegment(arc.Point.ToWpf(), arc.Size.ToWpf(), arc.RotationAngle, arc.IsLargeArc, (SweepDirection)arc.SweepDirection, s.IsStroked),
CrossPathSegment.CubicBezier cubicBezier => new BezierSegment(cubicBezier.Point1.ToWpf(), cubicBezier.Point2.ToWpf(), cubicBezier.Point3.ToWpf(), cubicBezier.IsStroked),
CrossPathSegment.QuadraticBezier quadraticBezier => new QuadraticBezierSegment(quadraticBezier.Point1.ToWpf(), quadraticBezier.Point2.ToWpf(), quadraticBezier.IsStroked),
_ => throw new NotImplementedException(),
}), f.Closed)))
};
throw new NotSupportedException();
}
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
private static Drawing ConvertDrawing(CrossDrawing src)
{
dst.MappingMode = (BrushMappingMode)src.MappingMode;
dst.SpreadMethod = (GradientSpreadMethod)src.SpreadMethod;
dst.GradientStops =
new GradientStopCollection(src.GradientStops.Select(s => new GradientStop(s.Color.ToWpf(), s.Offset)));
return Sync(dst, src);
if (src is CrossDrawingGroup g)
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
if (src is CrossGeometryDrawing geo)
return new GeometryDrawing()
{
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
};
throw new NotSupportedException();
}
if (brush is CrossSolidColorBrush br)
return Sync(new SolidColorBrush(br.Color.ToWpf()), brush);
if (brush is CrossDrawingBrush db)
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
if (brush is CrossRadialGradientBrush radial)
return SyncGradient(new RadialGradientBrush()
private static Brush? ConvertBrush(CrossBrush? brush)
{
if (brush == null)
return null;
static Brush Sync(Brush dst, CrossBrush src)
{
RadiusX = radial.RadiusX,
RadiusY = radial.RadiusY,
Center = radial.Center.ToWpf(),
GradientOrigin = radial.GradientOrigin.ToWpf()
}, radial);
throw new NotSupportedException();
}
dst.Opacity = src.Opacity;
dst.Transform = ConvertTransform(src.Transform);
dst.RelativeTransform = ConvertTransform(src.RelativeTransform);
return dst;
}
private static Pen? ConvertPen(CrossPen? pen)
{
if (pen == null)
return null;
return new Pen(ConvertBrush(pen.Brush), pen.Thickness);
}
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
{
dst.Stretch = (Stretch)src.Stretch;
dst.AlignmentX = (AlignmentX)src.AlignmentX;
dst.AlignmentY = (AlignmentY)src.AlignmentY;
dst.TileMode = (TileMode)src.TileMode;
dst.Viewbox = src.Viewbox.ToWpf();
dst.ViewboxUnits = (BrushMappingMode)src.ViewboxUnits;
dst.Viewport = src.Viewport.ToWpf();
dst.ViewportUnits = (BrushMappingMode)src.ViewportUnits;
return Sync(dst, src);
}
private static ImageSource ConvertImage(CrossImage image)
{
if (image is CrossBitmapImage bi)
return new BitmapImage(new Uri(bi.Path, UriKind.Absolute));
if (image is CrossDrawingImage di)
return new DrawingImage(ConvertDrawing(di.Drawing));
throw new NotSupportedException();
}
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
{
dst.MappingMode = (BrushMappingMode)src.MappingMode;
dst.SpreadMethod = (GradientSpreadMethod)src.SpreadMethod;
dst.GradientStops =
new GradientStopCollection(src.GradientStops.Select(s => new GradientStop(s.Color.ToWpf(), s.Offset)));
return Sync(dst, src);
}
if (brush is CrossSolidColorBrush br)
return Sync(new SolidColorBrush(br.Color.ToWpf()), brush);
if (brush is CrossDrawingBrush db)
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
if (brush is CrossRadialGradientBrush radial)
return SyncGradient(new RadialGradientBrush()
{
RadiusX = radial.RadiusX,
RadiusY = radial.RadiusY,
Center = radial.Center.ToWpf(),
GradientOrigin = radial.GradientOrigin.ToWpf()
}, radial);
throw new NotSupportedException();
}
private static Pen? ConvertPen(CrossPen? pen)
{
if (pen == null)
return null;
var cap = pen.LineCap switch
{
PenLineCap.Flat => WPenLineCap.Flat,
PenLineCap.Round => WPenLineCap.Round,
PenLineCap.Square => WPenLineCap.Square
};
var join = pen.LineJoin switch
{
PenLineJoin.Bevel => WPenLineJoin.Bevel,
PenLineJoin.Miter => WPenLineJoin.Miter,
PenLineJoin.Round => WPenLineJoin.Round
};
return new Pen(ConvertBrush(pen.Brush), pen.Thickness)
{
StartLineCap = cap,
EndLineCap = cap,
DashCap = cap,
LineJoin = join,
};
}
private static ImageSource ConvertImage(CrossImage image)
{
if (image is CrossBitmapImage bi)
return new BitmapImage(new Uri(bi.Path, UriKind.Absolute));
if (image is CrossDrawingImage di)
return new DrawingImage(ConvertDrawing(di.Drawing));
throw new NotSupportedException();
}
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc.ToWpf());
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geo) =>
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geo));
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc.ToWpf());
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geo) =>
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geo));
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc.ToWpf());
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc.ToWpf());
}
}

159
tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs

@ -0,0 +1,159 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Media;
using CrossUI;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests;
#elif AVALONIA_D2D
namespace Avalonia.Direct2D1.RenderTests;
#else
namespace Avalonia.RenderTests.WpfCompare;
#endif
public class CrossGeometryTests : CrossTestBase
{
public CrossGeometryTests() : base("Media/Geometry")
{
}
[CrossFact]
public void Should_Render_Stream_Geometry()
{
var geometry = new CrossStreamGeometry();
var context = geometry.GetContext();
context.BeginFigure(new Point(150, 15), true, true);
context.LineTo(new Point(258, 77), true);
context.LineTo(new Point(258, 202), true);
context.LineTo(new Point(150, 265), true);
context.LineTo(new Point(42, 202), true);
context.LineTo(new Point(42, 77), true);
context.EndFigure();
var brush = new CrossDrawingBrush()
{
TileMode = TileMode.None,
Drawing = new CrossDrawingGroup()
{
Children = new List<CrossDrawing>()
{
new CrossGeometryDrawing(new CrossRectangleGeometry(new(0, 0, 300, 280)))
{
Brush = new CrossSolidColorBrush(Colors.White)
},
new CrossGeometryDrawing(geometry)
{
Pen = new CrossPen()
{
Brush = new CrossSolidColorBrush(Colors.Black),
Thickness = 2
}
}
}
}
};
RenderAndCompare(new CrossControl()
{
Width = 300,
Height = 280,
Background = brush
});
}
[CrossFact]
public void Should_Render_Geometry_With_Strokeless_Lines()
{
var geometry = new CrossStreamGeometry();
var context = geometry.GetContext();
context.BeginFigure(new Point(150, 15), true, true);
context.LineTo(new Point(258, 77), true);
context.LineTo(new Point(258, 202), false);
context.LineTo(new Point(150, 265), true);
context.LineTo(new Point(42, 202), true);
context.LineTo(new Point(42, 77), false);
context.EndFigure();
var brush = new CrossDrawingBrush()
{
TileMode = TileMode.None,
Drawing = new CrossDrawingGroup()
{
Children = new List<CrossDrawing>()
{
new CrossGeometryDrawing(new CrossRectangleGeometry(new(0, 0, 300, 280)))
{
Brush = new CrossSolidColorBrush(Colors.White)
},
new CrossGeometryDrawing(geometry)
{
Pen = new CrossPen()
{
Brush = new CrossSolidColorBrush(Colors.Black),
Thickness = 2
}
}
}
}
};
RenderAndCompare(new CrossControl()
{
Width = 300,
Height = 280,
Background = brush
});
}
// Skip the test for now
#if !AVALONIA_SKIA
[CrossTheory,
InlineData(PenLineCap.Flat, PenLineJoin.Round),
InlineData(PenLineCap.Flat, PenLineJoin.Bevel),
InlineData(PenLineCap.Flat, PenLineJoin.Miter),
InlineData(PenLineCap.Round, PenLineJoin.Round),
InlineData(PenLineCap.Round, PenLineJoin.Bevel),
InlineData(PenLineCap.Round, PenLineJoin.Miter),
]
#endif
public void Should_Properly_CloseFigure(PenLineCap lineCap, PenLineJoin lineJoin)
{
var geometry = new CrossPathGeometry();
var center = new Point(150, 150);
var r = 100d;
var pointCount = 5;
var points = Enumerable.Range(0, pointCount).Select(a => a * Math.PI / pointCount * 2).Select(a =>
new Point(center.X + Math.Sin(a) * r, center.Y + Math.Cos(a) * r)).ToArray();
var figure = new CrossPathFigure() { Start = points[0], Closed = true };
geometry.Figures.Add(figure);
var lineNum = 0;
for (var c = 2; lineNum < pointCount - 1; c = (c + 2) % pointCount, lineNum++)
{
figure.Segments.Add(new CrossPathSegment.Line(points[c], (lineNum) % 3 < 2));
}
var control = new CrossFuncControl(ctx =>
{
ctx.DrawRectangle(new CrossSolidColorBrush(Colors.White), null, new(0, 0, 300, 300));
ctx.DrawGeometry(null,
new CrossPen()
{
Brush = new CrossSolidColorBrush(Colors.Black),
Thickness = 20,
LineJoin = lineJoin,
LineCap = lineCap
}, geometry);
}) { Width = 300, Height = 300 };
RenderAndCompare(control,
$"{nameof(Should_Properly_CloseFigure)}_{lineCap}_{lineJoin}");
}
}

378
tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs

@ -8,165 +8,293 @@ using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
using CrossUI;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests.CrossUI;
#else
namespace Avalonia.Direct2D1.RenderTests.CrossUI;
#endif
class AvaloniaCrossControl : Control
namespace CrossUI
{
private readonly CrossControl _src;
private readonly Dictionary<CrossControl, AvaloniaCrossControl> _children;
using Avalonia;
public AvaloniaCrossControl(CrossControl src)
public partial class CrossGlobals
{
_src = src;
_children = src.Children.ToDictionary(x => x, x => new AvaloniaCrossControl(x));
Width = src.Bounds.Width;
Height = src.Bounds.Height;
RenderTransform = new MatrixTransform(src.RenderTransform);
RenderTransformOrigin = new RelativePoint(default, RelativeUnit.Relative);
foreach (var ch in src.Children)
{
var c = _children[ch];
VisualChildren.Add(c);
LogicalChildren.Add(c);
public static ICrossStreamGeometryContextImplProvider GetContextImplProvider()
{
return new AvaloniaCrossStreamGeometryContextImplProvider();
}
}
protected override Size MeasureOverride(Size availableSize)
public class AvaloniaCrossStreamGeometryContextImplProvider : ICrossStreamGeometryContextImplProvider
{
foreach (var ch in _children)
ch.Value.Measure(ch.Key.Bounds.Size);
return _src.Bounds.Size;
public ICrossStreamGeometryContextImpl Create()
{
return new AvaloniaCrossStreamGeometryContextImpl();
}
}
protected override Size ArrangeOverride(Size finalSize)
public class AvaloniaCrossStreamGeometryContextImpl : ICrossStreamGeometryContextImpl
{
foreach (var ch in _children)
ch.Value.Arrange(ch.Key.Bounds);
return finalSize;
}
private StreamGeometry _streamGeometry;
private StreamGeometryContext _context;
private bool _isClosed;
public override void Render(DrawingContext context)
{
_src.Render(new AvaloniaCrossDrawingContext(context));
}
}
public AvaloniaCrossStreamGeometryContextImpl()
{
_streamGeometry = new StreamGeometry();
_context = _streamGeometry.Open();
}
class AvaloniaCrossDrawingContext : ICrossDrawingContext
{
private readonly DrawingContext _ctx;
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked)
{
_context.ArcTo(point, size, rotationAngle, isLargeArc, sweepDirection, isStroked);
}
public AvaloniaCrossDrawingContext(DrawingContext ctx)
{
_ctx = ctx;
}
public void BeginFigure(Point point, bool isFilled, bool isClosed)
{
_isClosed = isClosed;
_context.BeginFigure(point, isFilled);
}
static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value);
public void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked)
{
_context.CubicBezierTo(controlPoint1, controlPoint2, endPoint, isStroked);
}
static RelativeRect ConvertRect(Rect rc, BrushMappingMode mode)
=> new RelativeRect(rc,
mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
public void Dispose()
{
_context.Dispose();
}
static RelativePoint ConvertPoint(Point pt, BrushMappingMode mode)
=> new(pt, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
public void EndFigure()
{
_context.EndFigure(_isClosed);
Dispose();
}
static RelativeScalar ConvertScalar(double scalar, BrushMappingMode mode)
=> new(scalar, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
public object GetGeometry()
{
return _streamGeometry;
}
static Geometry ConvertGeometry(CrossGeometry g)
{
if (g is CrossRectangleGeometry rg)
return new RectangleGeometry(rg.Rect);
else if (g is CrossSvgGeometry svg)
return PathGeometry.Parse(svg.Path);
else if (g is CrossEllipseGeometry ellipse)
return new EllipseGeometry(ellipse.Rect);
throw new NotSupportedException();
public void LineTo(Point point, bool isStroked)
{
_context.LineTo(point, isStroked);
}
public void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked)
{
_context.QuadraticBezierTo(controlPoint, endPoint, isStroked);
}
}
static Drawing ConvertDrawing(CrossDrawing src)
}
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests.CrossUI
{
#else
namespace Avalonia.Direct2D1.RenderTests.CrossUI
{
#endif
class AvaloniaCrossControl : Control
{
if (src is CrossDrawingGroup g)
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
if (src is CrossGeometryDrawing geo)
return new GeometryDrawing()
private readonly CrossControl _src;
private readonly Dictionary<CrossControl, AvaloniaCrossControl> _children;
public AvaloniaCrossControl(CrossControl src)
{
_src = src;
_children = src.Children.ToDictionary(x => x, x => new AvaloniaCrossControl(x));
Width = src.Bounds.Width;
Height = src.Bounds.Height;
RenderTransform = new MatrixTransform(src.RenderTransform);
RenderTransformOrigin = new RelativePoint(default, RelativeUnit.Relative);
foreach (var ch in src.Children)
{
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
};
throw new NotSupportedException();
var c = _children[ch];
VisualChildren.Add(c);
LogicalChildren.Add(c);
}
}
protected override Size MeasureOverride(Size availableSize)
{
foreach (var ch in _children)
ch.Value.Measure(ch.Key.Bounds.Size);
return _src.Bounds.Size;
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (var ch in _children)
ch.Value.Arrange(ch.Key.Bounds);
return finalSize;
}
public override void Render(DrawingContext context)
{
_src.Render(new AvaloniaCrossDrawingContext(context));
}
}
static IBrush? ConvertBrush(CrossBrush? brush)
class AvaloniaCrossDrawingContext : ICrossDrawingContext
{
if (brush == null)
return null;
static Brush Sync(Brush dst, CrossBrush src)
private readonly DrawingContext _ctx;
public AvaloniaCrossDrawingContext(DrawingContext ctx)
{
dst.Opacity = src.Opacity;
dst.Transform = ConvertTransform(src.Transform);
dst.TransformOrigin = new RelativePoint(default, RelativeUnit.Absolute);
if (src.RelativeTransform != null)
throw new PlatformNotSupportedException();
return dst;
_ctx = ctx;
}
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
static Transform? ConvertTransform(Matrix? m) => m == null ? null : new MatrixTransform(m.Value);
static RelativeRect ConvertRect(Rect rc, BrushMappingMode mode)
=> new RelativeRect(rc,
mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
static RelativePoint ConvertPoint(Point pt, BrushMappingMode mode)
=> new(pt, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
static RelativeScalar ConvertScalar(double scalar, BrushMappingMode mode)
=> new(scalar, mode == BrushMappingMode.RelativeToBoundingBox ? RelativeUnit.Relative : RelativeUnit.Absolute);
static Geometry ConvertGeometry(CrossGeometry g)
{
dst.Stretch = src.Stretch;
dst.AlignmentX = src.AlignmentX;
dst.AlignmentY = src.AlignmentY;
dst.TileMode = src.TileMode;
dst.SourceRect = ConvertRect(src.Viewbox, src.ViewboxUnits);
dst.DestinationRect = ConvertRect(src.Viewport, src.ViewportUnits);
return Sync(dst, src);
if (g is CrossRectangleGeometry rg)
return new RectangleGeometry(rg.Rect);
else if (g is CrossSvgGeometry svg)
return PathGeometry.Parse(svg.Path);
else if (g is CrossEllipseGeometry ellipse)
return new EllipseGeometry(ellipse.Rect);
else if(g is CrossStreamGeometry streamGeometry)
return (StreamGeometry)streamGeometry.GetContext().GetGeometry();
else if (g is CrossPathGeometry path)
return new PathGeometry()
{
Figures = RetAddRange(new PathFigures(), path.Figures.Select(f =>
new PathFigure()
{
StartPoint = f.Start,
IsClosed = f.Closed,
Segments = RetAddRange<PathSegments, PathSegment>(new PathSegments(), f.Segments.Select<CrossPathSegment, PathSegment>(s =>
s switch
{
CrossPathSegment.Line l => new LineSegment()
{
Point = l.To, IsStroked = l.IsStroked
},
CrossPathSegment.Arc a => new ArcSegment()
{
Point = a.Point,
RotationAngle = a.RotationAngle,
Size = a.Size,
IsLargeArc = a.IsLargeArc,
SweepDirection = a.SweepDirection,
IsStroked = a.IsStroked
},
CrossPathSegment.CubicBezier c => new BezierSegment()
{
Point1 = c.Point1,
Point2 = c.Point2,
Point3 = c.Point3,
IsStroked = c.IsStroked
},
CrossPathSegment.QuadraticBezier q => new QuadraticBezierSegment()
{
Point1 = q.Point1,
Point2 = q.Point2,
IsStroked = q.IsStroked
}
}))
}))
};
throw new NotSupportedException();
}
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
static TList RetAddRange<TList, T>(TList l, IEnumerable<T> en) where TList : IList<T>
{
dst.GradientStops = new GradientStops();
dst.GradientStops.AddRange(src.GradientStops);
dst.SpreadMethod = src.SpreadMethod;
return Sync(dst, src);
foreach(var e in en)
l.Add(e);
return l;
}
if (brush is CrossSolidColorBrush br)
return Sync(new SolidColorBrush(br.Color), brush);
if (brush is CrossDrawingBrush db)
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
if (brush is CrossRadialGradientBrush radial)
return SyncGradient(
new RadialGradientBrush()
static Drawing ConvertDrawing(CrossDrawing src)
{
if (src is CrossDrawingGroup g)
return new DrawingGroup() { Children = new DrawingCollection(g.Children.Select(ConvertDrawing)) };
if (src is CrossGeometryDrawing geo)
return new GeometryDrawing()
{
Center = ConvertPoint(radial.Center, radial.MappingMode),
GradientOrigin = ConvertPoint(radial.GradientOrigin, radial.MappingMode),
RadiusX = ConvertScalar(radial.RadiusX, radial.MappingMode),
RadiusY = ConvertScalar(radial.RadiusY, radial.MappingMode)
}, radial);
throw new NotSupportedException();
}
Geometry = ConvertGeometry(geo.Geometry), Brush = ConvertBrush(geo.Brush), Pen = ConvertPen(geo.Pen)
};
throw new NotSupportedException();
}
static IBrush? ConvertBrush(CrossBrush? brush)
{
if (brush == null)
return null;
static Brush Sync(Brush dst, CrossBrush src)
{
dst.Opacity = src.Opacity;
dst.Transform = ConvertTransform(src.Transform);
dst.TransformOrigin = new RelativePoint(default, RelativeUnit.Absolute);
if (src.RelativeTransform != null)
throw new PlatformNotSupportedException();
return dst;
}
static IPen? ConvertPen(CrossPen? pen)
{
if (pen == null)
return null;
return new Pen(ConvertBrush(pen.Brush), pen.Thickness);
}
static Brush SyncTile(TileBrush dst, CrossTileBrush src)
{
dst.Stretch = src.Stretch;
dst.AlignmentX = src.AlignmentX;
dst.AlignmentY = src.AlignmentY;
dst.TileMode = src.TileMode;
dst.SourceRect = ConvertRect(src.Viewbox, src.ViewboxUnits);
dst.DestinationRect = ConvertRect(src.Viewport, src.ViewportUnits);
return Sync(dst, src);
}
static IImage ConvertImage(CrossImage image)
{
if (image is CrossBitmapImage bi)
return new Bitmap(bi.Path);
if (image is CrossDrawingImage di)
return new DrawingImage(ConvertDrawing(di.Drawing));
throw new NotSupportedException();
}
static Brush SyncGradient(GradientBrush dst, CrossGradientBrush src)
{
dst.GradientStops = new GradientStops();
dst.GradientStops.AddRange(src.GradientStops);
dst.SpreadMethod = src.SpreadMethod;
return Sync(dst, src);
}
if (brush is CrossSolidColorBrush br)
return Sync(new SolidColorBrush(br.Color), brush);
if (brush is CrossDrawingBrush db)
return SyncTile(new DrawingBrush(ConvertDrawing(db.Drawing)), db);
if (brush is CrossRadialGradientBrush radial)
return SyncGradient(
new RadialGradientBrush()
{
Center = ConvertPoint(radial.Center, radial.MappingMode),
GradientOrigin = ConvertPoint(radial.GradientOrigin, radial.MappingMode),
RadiusX = ConvertScalar(radial.RadiusX, radial.MappingMode),
RadiusY = ConvertScalar(radial.RadiusY, radial.MappingMode)
}, radial);
throw new NotSupportedException();
}
static IPen? ConvertPen(CrossPen? pen)
{
if (pen == null)
return null;
return new Pen(ConvertBrush(pen.Brush), pen.Thickness) { LineCap = pen.LineCap, LineJoin = pen.LineJoin };
}
static IImage ConvertImage(CrossImage image)
{
if (image is CrossBitmapImage bi)
return new Bitmap(bi.Path);
if (image is CrossDrawingImage di)
return new DrawingImage(ConvertDrawing(di.Drawing));
throw new NotSupportedException();
}
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc);
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geometry) =>
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geometry));
public void DrawRectangle(CrossBrush? brush, CrossPen? pen, Rect rc) => _ctx.DrawRectangle(ConvertBrush(brush), ConvertPen(pen), rc);
public void DrawGeometry(CrossBrush? brush, CrossPen? pen, CrossGeometry geometry) =>
_ctx.DrawGeometry(ConvertBrush(brush), ConvertPen(pen), ConvertGeometry(geometry));
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc);
public void DrawImage(CrossImage image, Rect rc) => _ctx.DrawImage(ConvertImage(image), rc);
}
}

60
tests/Avalonia.RenderTests/CrossUI/CrossUI.cs

@ -8,6 +8,11 @@ using Avalonia;
namespace CrossUI;
public partial class CrossGlobals
{
}
public class CrossBrush
{
public double Opacity = 1;
@ -108,6 +113,23 @@ public class CrossEllipseGeometry : CrossGeometry
public Rect Rect { get; set; }
}
public class CrossStreamGeometry : CrossGeometry
{
private ICrossStreamGeometryContextImpl? _contextImpl;
public CrossStreamGeometry()
{
}
public ICrossStreamGeometryContextImpl GetContext()
{
_contextImpl ??= CrossGlobals.GetContextImplProvider().Create();
return _contextImpl;
}
}
public class CrossRectangleGeometry : CrossGeometry
{
public Rect Rect;
@ -118,6 +140,26 @@ public class CrossRectangleGeometry : CrossGeometry
}
}
public class CrossPathGeometry : CrossGeometry
{
public List<CrossPathFigure> Figures { get; set; } = new();
}
public class CrossPathFigure
{
public Point Start { get; set; }
public List<CrossPathSegment> Segments { get; set; } = new();
public bool Closed { get; set; }
}
public abstract record class CrossPathSegment(bool IsStroked)
{
public record Line(Point To, bool IsStroked) : CrossPathSegment(IsStroked);
public record Arc(Point Point, Size Size, double RotationAngle, bool IsLargeArc, SweepDirection SweepDirection, bool IsStroked) : CrossPathSegment(IsStroked);
public record CubicBezier(Point Point1, Point Point2, Point Point3, bool IsStroked) : CrossPathSegment(IsStroked);
public record QuadraticBezier(Point Point1, Point Point2, bool IsStroked) : CrossPathSegment(IsStroked);
}
public class CrossDrawingBrush : CrossTileBrush
{
public CrossDrawing Drawing;
@ -127,6 +169,24 @@ public class CrossPen
{
public CrossBrush Brush;
public double Thickness = 1;
public PenLineJoin LineJoin { get; set; } = PenLineJoin.Miter;
public PenLineCap LineCap { get; set; } = PenLineCap.Flat;
}
public interface ICrossStreamGeometryContextImpl : IDisposable
{
object GetGeometry();
void BeginFigure(Point point, bool isFilled, bool isClosed);
void EndFigure();
void LineTo(Point point, bool isStroked);
void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked);
void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked);
void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked);
}
public interface ICrossStreamGeometryContextImplProvider
{
ICrossStreamGeometryContextImpl Create();
}
public interface ICrossDrawingContext

2
tests/Avalonia.Skia.RenderTests/CrossTestBase.cs

@ -19,7 +19,7 @@ class CrossFactAttribute : FactAttribute
}
class CrossThreoryAttribute : TheoryAttribute
class CrossTheoryAttribute : TheoryAttribute
{
}

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Bevel.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Miter.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Round.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Bevel.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Miter.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Round.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Geometry_With_Strokeless_Lines.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Stream_Geometry.wpf.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Loading…
Cancel
Save