From 6e35fd2bbeb6a1831e82dde1c9b7c39bd577f769 Mon Sep 17 00:00:00 2001 From: workgroupengineering Date: Thu, 15 Aug 2024 06:27:09 +0200 Subject: [PATCH] feat(Geometries): PolyBezierSegment (#16664) * feat(Geometries): PolyBezierSegment * fix: warning * add PolyBezierSegment to CrossUI Test * test: AddPolyBezierSegment CrossUI test --- src/Avalonia.Base/Media/PolyBezierSegment.cs | 90 ++++++++++++++ src/Avalonia.Base/Points.cs | 22 ++-- .../CrossUI.Wpf.cs | 1 + .../CrossTests/CrossGeometryTests.cs | 30 +++++ .../CrossUI/CrossUI.Avalonia.cs | 115 ++++++++++-------- tests/Avalonia.RenderTests/CrossUI/CrossUI.cs | 1 + ...ezierSegment_With_Strokeless_Lines.wpf.png | Bin 0 -> 4265 bytes 7 files changed, 195 insertions(+), 64 deletions(-) create mode 100644 src/Avalonia.Base/Media/PolyBezierSegment.cs create mode 100644 tests/TestFiles/CrossTests/Media/Geometry/Should_Render_PolyBezierSegment_With_Strokeless_Lines.wpf.png diff --git a/src/Avalonia.Base/Media/PolyBezierSegment.cs b/src/Avalonia.Base/Media/PolyBezierSegment.cs new file mode 100644 index 0000000000..e77efe4b6d --- /dev/null +++ b/src/Avalonia.Base/Media/PolyBezierSegment.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +/// +/// PolyBezierSegment +/// +public sealed class PolyBezierSegment : PathSegment +{ + /// + /// Points DirectProperty definition + /// + public static readonly DirectProperty PointsProperty = + AvaloniaProperty.RegisterDirect(nameof(Points), + o => o.Points, + (o, v) => o.Points = v); + + private Points? _points = []; + + public PolyBezierSegment() + { + + } + + public PolyBezierSegment(IEnumerable points, bool isStroked) + { + if (points is null) + { + throw new ArgumentNullException(nameof(points)); + } + + Points = new Points(points); + IsStroked = isStroked; + } + + /// + /// Gets or sets the Point collection that defines this object. + /// + /// + /// The points. + /// + [Metadata.Content] + public Points? Points + { + get => _points; + set => SetAndRaise(PointsProperty, ref _points, value); + } + + internal override void ApplyTo(StreamGeometryContext ctx) + { + var isStroken = this.IsStroked; + if (_points is { Count: > 0 } points) + { + var i = 0; + for (; i < points.Count; i += 3) + { + ctx.CubicBezierTo(points[i], + points[i + 1], + points[i + 2], + isStroken); + } + var delta = i - points.Count; + if (delta != 0) + { + Logging.Logger.TryGet(Logging.LogEventLevel.Warning, + Logging.LogArea.Visual) + ?.Log(nameof(PolyBezierSegment), + $"{nameof(PolyBezierSegment)} has ivalid number of points. Last {Math.Abs(delta)} points will be ignored."); + } + } + } + + public override string ToString() + { + var builder = StringBuilderCache.Acquire(); + if (_points is { Count: > 0 } points) + { + builder.Append('C').Append(' '); + foreach (var point in _points) + { + builder.Append(FormattableString.Invariant($"{point}")); + builder.Append(' '); + } + builder.Length = builder.Length - 1; + } + return StringBuilderCache.GetStringAndRelease(builder); + } +} diff --git a/src/Avalonia.Base/Points.cs b/src/Avalonia.Base/Points.cs index 2f88ecd80f..2e79f8e856 100644 --- a/src/Avalonia.Base/Points.cs +++ b/src/Avalonia.Base/Points.cs @@ -1,18 +1,20 @@ using System.Collections.Generic; using Avalonia.Collections; -namespace Avalonia +namespace Avalonia; + +/// +/// Represents a collection of values that can be individually accessed by index. +/// +public sealed class Points : AvaloniaList { - public sealed class Points : AvaloniaList + public Points() { - public Points() - { - - } + + } - public Points(IEnumerable points) : base(points) - { - - } + public Points(IEnumerable points) : base(points) + { + } } diff --git a/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs b/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs index 32e29a5193..9f183729fe 100644 --- a/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs +++ b/tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs @@ -191,6 +191,7 @@ namespace Avalonia.RenderTests.WpfCompare 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), CrossPathSegment.PolyLine polyLine => new PolyLineSegment(polyLine.Points.Select(p => p.ToWpf()).ToList(), polyLine.IsStroked), + CrossPathSegment.PolyBezierSegment pb => new PolyBezierSegment(pb.Points.Select(p => p.ToWpf()), pb.IsStroked), _ => throw new NotImplementedException(), }), f.Closed))) }; diff --git a/tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs b/tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs index 0761e09a13..ac4a701f94 100644 --- a/tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs +++ b/tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs @@ -141,6 +141,36 @@ public class CrossGeometryTests : CrossTestBase $"{nameof(Should_Render_PolyLineSegment_With_Strokeless_Lines)}"); } + [CrossFact] + public void Should_Render_PolyBezierSegment_With_Strokeless_Lines() + { + var brush = new CrossSolidColorBrush(Colors.Blue); + var pen = new CrossPen() + { + Brush = new CrossSolidColorBrush(Colors.Red), + Thickness = 8 + }; + var figure = new CrossPathFigure() + { + Start = new Point(10, 100), + Closed = false, + Segments = + { + new CrossPathSegment.PolyBezierSegment([new(0, 0), new(200, 0), new(300, 100), new(300, 0), new(500, 0), new(600,100)], false) + } + }; + var geometry = new CrossPathGeometry { Figures = { figure } }; + + var control = new CrossFuncControl(ctx => ctx.DrawGeometry(brush, pen, geometry)) + { + Width = 700, + Height = 400, + }; + + RenderAndCompare(control, + $"{nameof(Should_Render_PolyBezierSegment_With_Strokeless_Lines)}"); + } + // Skip the test for now #if !AVALONIA_SKIA [CrossTheory, diff --git a/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs b/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs index 96574db757..42929a3690 100644 --- a/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs +++ b/tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs @@ -156,61 +156,68 @@ namespace Avalonia.Direct2D1.RenderTests.CrossUI 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); - 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 - }, - CrossPathSegment.PolyLine p => new PolyLineSegment() + switch (g) + { + case CrossRectangleGeometry rg: + return new RectangleGeometry(rg.Rect); + case CrossSvgGeometry svg: + return PathGeometry.Parse(svg.Path); + case CrossEllipseGeometry ellipse: + return new EllipseGeometry(ellipse.Rect); + case CrossStreamGeometry streamGeometry: + return (StreamGeometry)streamGeometry.GetContext().GetGeometry(); + case 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 { - Points = p.Points.ToList(), - IsStroked = p.IsStroked - }, - _ => throw new InvalidOperationException() - })) - })) - }; - throw new NotSupportedException(); + 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 + }, + CrossPathSegment.PolyLine p => new PolyLineSegment() + { + Points = p.Points.ToList(), + IsStroked = p.IsStroked + }, + CrossPathSegment.PolyBezierSegment p => new PolyBezierSegment(p.Points,p.IsStroked), + _ => throw new InvalidOperationException() + })) + })) + }; + default: + throw new NotSupportedException(); + } } static TList RetAddRange(TList l, IEnumerable en) where TList : IList diff --git a/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs b/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs index 587e8d0f83..028ace0eff 100644 --- a/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs +++ b/tests/Avalonia.RenderTests/CrossUI/CrossUI.cs @@ -159,6 +159,7 @@ public abstract record class CrossPathSegment(bool 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 record PolyLine(IEnumerable Points, bool IsStroked) : CrossPathSegment(IsStroked); + public record PolyBezierSegment(IEnumerable Points, bool IsStroked) : CrossPathSegment(IsStroked); } public class CrossDrawingBrush : CrossTileBrush diff --git a/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_PolyBezierSegment_With_Strokeless_Lines.wpf.png b/tests/TestFiles/CrossTests/Media/Geometry/Should_Render_PolyBezierSegment_With_Strokeless_Lines.wpf.png new file mode 100644 index 0000000000000000000000000000000000000000..097ae18331378b057ad5fde3593f5fce39777a43 GIT binary patch literal 4265 zcmeHK`#Tft8{bsCURg{S7HMHQlhfwVK`~(_O64>;#bO#`4xxyYQ!^x|8od{r+d{&j0|m8De~|0|Hw#1a&nO z006cBGl5Xdu8?s6h7xvxQaE=cvS`#{zUnn|GT)J%cLe=gZjZf3jfMU0+v~hlkQ-;c8}9aSuX>!f z7w@PMbWmNg_{n{z^%R$~E*@GlD$Z>OLrL{lldAOx>n)le(_Wo`U0&^M;N#c#=m4do zGw%6Mdi~l%!ugL&C@1I4mM7pHakzzmKakG)GkyCJ3fZVc9GE{^PFH3nsST9mK-xom zXRjSYk3giq&%+$+`c0%ogFf~31k+4kD^zUxnR{RAEH@hmExsBphz1>{+`JZ#@Djh$PzkM1ZdBxQGr{018iI>8)BWr%bw^5j&f<)Pr} z;U4(782>EgRPXY>ND$1i*uWhkQk)7VPOH63ze*!Gd0$d(YkoMLU3Pg82D2lT@qVsv z!E-wsinEL%lnFXn+=ly-|I*y3Q&6-0Fw6H=rk;A&oc@*XFD|LJHm6Vfc2pptLW?aZ zFgt+zz)hO-_okK-Eq4se(DqktvNwRMEbkkC)6z0rX76%NvgGwO?XM=0elpd8pY`v^ z8u>hwV!v&gNADwhvmy81e#3D2zx89+qnsCOk#5S~!@ipqeWG=#=t7CyP4(#b;s@kS zu`3B$cBIg=);u>TGz~vM_9}51741(t9__4{G4U9Qbj}rjIA*}JqZAlp&mUkvpZ__{ zbc{Q;{FQ3tCU)7!7R-JN>`-x>mYF7xNSmZ1MYWU7_{Uf!ewXM7h8CWI1kq9;hSWaA zv1X*Fa_~pBM|`75I_-ySj_yWc12@M_PQiADE9+3`uD1W2P6=a2w5%GM0KZ6(5z$9Dv;4c`!?a}I+@JA|k zp^SD^Sbn9~`wQ9wKW|Xf?1u004r34=J_EL{4%+*+v!|0R)Y;$Aj4unK9-~K4R7MGlsmr(jRxqL-kWu&y8CaYDW7r zPxUH4wKU}2Hen~{@fY@+JT;6@Y|w6tjfi=Xv--@NIA&>}nsvhtG@nCF^+L`ef+W zU#FRIq4(ssr66fVDokP(OJwfD7AX_>auWKrcj;Yk0o#gLu(m5%+#jgZ5FN3#`3=>^l2FPbOW<$tCiq7tAJMAK_+DCT z_@Y3$)Tuf>1~wctS&4@Bcz8C#8*`0kIe(dSud=4@pFGQXLFyr8;X5i*S#bpT=-I$i#Y zG?Q+U?_Gt)SI-h7o>)ST%^Z+%QhUR}B)h4@WgqzaF4F=s-VcnZ>(zh}tyP^B1z#4d z3O&Y83n*ya4UJU7P7_>8Cla;_%T=0eHIk`j3a37_uO7hqc4+MQ_le@sLIH|59pay_ z8Xcd0FaZkwbul`;IK8;ow+mafY$X%X;cuUzQ|Rj5W@@hE5i7OVae^L-B`#x

q|= zS5Zgk`8_nRgEe}F9=8oGA`k{2AfCwDzY-fPv|~>E-YLKHy(g)ZfdJXbAYMFKS=gCo zfJ|y`A>Mz8#*|PLiwk zNFgk($lvTlg4u4fU1KI=B&8f0vRFk=w%WH+VI zMAi%(QQR_rb+M70HF0b+h(Hv%@ckp9x=HFFntuERhuQ9gtGy811&Jr# zn|Zt2rE9%`7D5&ER7LjEqn{N;c!zk8my^<-vOgFOefdZ7_d3OYLac@& zeVkb@t3sYq9i2K5Rc)rIGn5JK-cPdS*pu8v+;k)lPK+h{)~ahjK4Q`ZyP%%f&T_I* zf8sL+)?d(Zlcli^2+El{u>_Z7Uyg3F>l{6Z1pf8f=MWwNQDNWj3lJPJ<^uPL&$WPrw?pjU40MKkVS=ixpJcB z`n)Bp>&~s*Ure~`&H`*@4@m;pGd%US}o`ikq=QZA{XiYuKb(8E+ww7Pm zo+1Fdre6s6$~P#pVwjRU{Y&QigZ5>a_6jsW339^=?5oU)q%x<3|roXZFbkVeNqHywn!xFT?@Hdw-h&< z0r?1kT1To~?>KU6qrQBOT4Xj|5+~0#=^qw4&fTV(EpVhK%*|5bly{wyb*|vza2=+5T37v zYd|7gzpV-3Uaci~?#jNm*Yzhe`|>s>hW@ycTiBz#y%I*rkOjW2Louli3rE^< z83EWd0s$2nuAf(Q^Ud9vOQ*XQ(p|oFGey_)1wBzET7_{7e8dO6XGaDFrG;*uo?@Sj zMyJ!kmI%5zU~#NxR{FN!O^*6Y`wk=xg@_9W4Z6&ZU~tX`(MB@j!FOk literal 0 HcmV?d00001