diff --git a/src/Avalonia.Base/Media/ArcSegment.cs b/src/Avalonia.Base/Media/ArcSegment.cs index ee353b0a893..f5807ab9d7b 100644 --- a/src/Avalonia.Base/Media/ArcSegment.cs +++ b/src/Avalonia.Base/Media/ArcSegment.cs @@ -97,7 +97,7 @@ public SweepDirection SweepDirection 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() diff --git a/src/Avalonia.Base/Media/BezierSegment .cs b/src/Avalonia.Base/Media/BezierSegment .cs index 31efe1ec232..1c09081e3b2 100644 --- a/src/Avalonia.Base/Media/BezierSegment .cs +++ b/src/Avalonia.Base/Media/BezierSegment .cs @@ -58,7 +58,7 @@ public Point Point3 internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.CubicBezierTo(Point1, Point2, Point3); + ctx.CubicBezierTo(Point1, Point2, Point3, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/LineSegment.cs b/src/Avalonia.Base/Media/LineSegment.cs index 68193bb770f..8b0014fcc8e 100644 --- a/src/Avalonia.Base/Media/LineSegment.cs +++ b/src/Avalonia.Base/Media/LineSegment.cs @@ -24,7 +24,7 @@ public Point Point internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.LineTo(Point); + ctx.LineTo(Point, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/PathSegment.cs b/src/Avalonia.Base/Media/PathSegment.cs index 0b517e56f3a..6b5f5f601df 100644 --- a/src/Avalonia.Base/Media/PathSegment.cs +++ b/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 IsStrokedProperty = + AvaloniaProperty.Register(nameof(IsStroked), true); + + public bool IsStroked + { + get => GetValue(IsStrokedProperty); + set => SetValue(IsStrokedProperty, value); + } } } diff --git a/src/Avalonia.Base/Media/QuadraticBezierSegment .cs b/src/Avalonia.Base/Media/QuadraticBezierSegment .cs index 01d22f20437..a338080065b 100644 --- a/src/Avalonia.Base/Media/QuadraticBezierSegment .cs +++ b/src/Avalonia.Base/Media/QuadraticBezierSegment .cs @@ -42,7 +42,7 @@ public Point Point2 internal override void ApplyTo(StreamGeometryContext ctx) { - ctx.QuadraticBezierTo(Point1, Point2); + ctx.QuadraticBezierTo(Point1, Point2, IsStroked); } public override string ToString() diff --git a/src/Avalonia.Base/Media/StreamGeometryContext.cs b/src/Avalonia.Base/Media/StreamGeometryContext.cs index 66fb65a6c29..c8072564b19 100644 --- a/src/Avalonia.Base/Media/StreamGeometryContext.cs +++ b/src/Avalonia.Base/Media/StreamGeometryContext.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media /// of is obtained by calling /// . /// - public class StreamGeometryContext : IGeometryContext + public class StreamGeometryContext : IGeometryContext, IGeometryContext2 { private readonly IStreamGeometryContextImpl _impl; @@ -102,5 +102,46 @@ public void Dispose() { _impl.Dispose(); } + + /// + 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; + } } } diff --git a/src/Avalonia.Base/Platform/IGeometryContext2.cs b/src/Avalonia.Base/Platform/IGeometryContext2.cs new file mode 100644 index 00000000000..4142430e9d7 --- /dev/null +++ b/src/Avalonia.Base/Platform/IGeometryContext2.cs @@ -0,0 +1,46 @@ +using Avalonia.Media; + +namespace Avalonia.Platform +{ + // TODO12 combine with IGeometryContext + public interface IGeometryContext2 : IGeometryContext + { + /// + /// Draws a line to the specified point. + /// + /// The destination point. + /// Whether the segment is stroked + void LineTo(Point point, bool isStroked); + + /// + /// Draws an arc to the specified point. + /// + /// The destination point. + /// The radii of an oval whose perimeter is used to draw the angle. + /// The rotation angle (in radians) of the oval that specifies the curve. + /// true to draw the arc greater than 180 degrees; otherwise, false. + /// + /// A value that indicates whether the arc is drawn in the Clockwise or Counterclockwise direction. + /// + /// Whether the segment is stroked + void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection, bool isStroked); + + /// + /// Draws a Bezier curve to the specified point. + /// + /// The first control point used to specify the shape of the curve. + /// The second control point used to specify the shape of the curve. + /// The destination point for the end of the curve. + /// Whether the segment is stroked + void CubicBezierTo(Point controlPoint1, Point controlPoint2, Point endPoint, bool isStroked); + + /// + /// Draws a quadratic Bezier curve to the specified point + /// + /// Control point + /// DestinationPoint + /// Whether the segment is stroked + void QuadraticBezierTo(Point controlPoint, Point endPoint, bool isStroked); + } + +} diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs index eb081c4f504..e66c77c9a32 100644 --- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs +++ b/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 @@ private static SKPath CreateEmptyPath() /// /// A Skia implementation of a . /// - 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); /// @@ -93,7 +96,7 @@ public StreamContext(StreamGeometryImpl geometryImpl) { _geometryImpl = geometryImpl; } - + /// /// Will update bounds of passed geometry. public void Dispose() @@ -117,7 +120,7 @@ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, sweep, (float)point.X, (float)point.Y); - if(Duplicate) + if (Duplicate) Fill.ArcTo( (float)size.Width, (float)size.Height, @@ -136,10 +139,12 @@ public void BeginFigure(Point startPoint, bool isFilled) 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 @@ public void BeginFigure(Point startPoint, bool isFilled) 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 @@ public void CubicBezierTo(Point point1, Point point2, Point point3) 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 @@ public void QuadraticBezierTo(Point point1, Point point2) 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 @@ public void EndFigure(bool isClosed) { if (isClosed) { - Stroke.Close(); + if (_isFigureBroken) + { + LineTo(_startPoint); + _isFigureBroken = false; + } + else + Stroke.Close(); if (Duplicate) Fill.Close(); } @@ -183,6 +194,105 @@ public void SetFillRule(FillRule fillRule) { Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding; } + + /// + 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); + } + + /// + 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); + } + + /// + 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); + } + + /// + 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); + } } } } diff --git a/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs b/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs index b4506632488..302f7f695c2 100644 --- a/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs +++ b/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs @@ -31,158 +31,272 @@ 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 _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 _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(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()); + } } diff --git a/tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs b/tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs new file mode 100644 index 00000000000..a5492328e6f --- /dev/null +++ b/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() + { + 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() + { + 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}"); + } +} diff --git a/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs b/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs index 29a772b7ae4..8ac89dc220c 100644 --- a/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs +++ b/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs @@ -8,165 +8,293 @@ 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 _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 _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(new PathSegments(), f.Segments.Select(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 l, IEnumerable en) where TList : IList { - 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); + } } diff --git a/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs b/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs index 449f75b0c63..3710123eaf6 100644 --- a/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs +++ b/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs @@ -8,6 +8,11 @@ namespace CrossUI; +public partial class CrossGlobals +{ + +} + public class CrossBrush { public double Opacity = 1; @@ -108,6 +113,23 @@ public CrossEllipseGeometry() 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 CrossRectangleGeometry(Rect rect) } } +public class CrossPathGeometry : CrossGeometry +{ + public List Figures { get; set; } = new(); +} + +public class CrossPathFigure +{ + public Point Start { get; set; } + public List 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 diff --git a/tests/Avalonia.Skia.RenderTests/CrossTestBase.cs b/tests/Avalonia.Skia.RenderTests/CrossTestBase.cs index 215d5b48262..a94736b0c79 100644 --- a/tests/Avalonia.Skia.RenderTests/CrossTestBase.cs +++ b/tests/Avalonia.Skia.RenderTests/CrossTestBase.cs @@ -19,7 +19,7 @@ class CrossFactAttribute : FactAttribute } -class CrossThreoryAttribute : TheoryAttribute +class CrossTheoryAttribute : TheoryAttribute { } diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Bevel.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Bevel.wpf.png new file mode 100644 index 00000000000..e174ccad15f Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Bevel.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Miter.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Miter.wpf.png new file mode 100644 index 00000000000..8cf98ba6336 Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Miter.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Round.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Round.wpf.png new file mode 100644 index 00000000000..f44dd37b36a Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Flat_Round.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Bevel.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Bevel.wpf.png new file mode 100644 index 00000000000..57565c7fe9d Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Bevel.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Miter.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Miter.wpf.png new file mode 100644 index 00000000000..7814ba07c0f Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Miter.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Round.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Round.wpf.png new file mode 100644 index 00000000000..4eeae15572e Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Properly_CloseFigure_Round_Round.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Geometry_With_Strokeless_Lines.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Geometry_With_Strokeless_Lines.wpf.png new file mode 100644 index 00000000000..7d6a0b40151 Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Geometry_With_Strokeless_Lines.wpf.png differ diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Stream_Geometry.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Stream_Geometry.wpf.png new file mode 100644 index 00000000000..2fb85630ab2 Binary files /dev/null and b/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_Stream_Geometry.wpf.png differ