Skip to content

Commit

Permalink
Fix some issues with strokeless geometry segments (#16019)
Browse files Browse the repository at this point in the history
* Added failing cross test.

For polyline segment with `IsStroked = false`.

* Fixed some issues with non-stroked segments

* Fix CombinedGeometryImpl with empty paths.

If an empty (but non-null) stroke was passed to `CombinedGeometryImpl` then its empty bounds would be used and the fill bounds ignored. Added a test and fixed that.

---------

Co-authored-by: Nikita Tsukanov <[email protected]>
  • Loading branch information
grokys and kekekeks committed Jun 13, 2024
1 parent 118778b commit eb4f575
Show file tree
Hide file tree
Showing 10 changed files with 97 additions and 30 deletions.
2 changes: 1 addition & 1 deletion src/Avalonia.Base/Media/PolyLineSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ internal override void ApplyTo(StreamGeometryContext ctx)
{
for (int i = 0; i < points.Count; i++)
{
ctx.LineTo(points[i]);
ctx.LineTo(points[i], IsStroked);
}
}
}
Expand Down
8 changes: 7 additions & 1 deletion src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ public CombinedGeometryImpl(SKPath? stroke, SKPath? fill)
{
StrokePath = stroke;
FillPath = fill;
Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default;

var bounds = stroke?.TightBounds ?? default;

if (fill != null)
bounds.Union(fill.TightBounds);

Bounds = bounds.ToAvaloniaRect();
}

public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
Expand Down
5 changes: 4 additions & 1 deletion src/Skia/Avalonia.Skia/GeometryImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ public Rect GetRenderBounds(IPen? pen)
lock (_lock)
{
_pathCache.UpdateIfNeeded(StrokePath, pen);
return _pathCache.RenderBounds;
var bounds = _pathCache.RenderBounds;
if (StrokePath != FillPath && FillPath != null)
bounds = bounds.Union(FillPath.Bounds.ToAvaloniaRect());
return bounds;
}
}

Expand Down
49 changes: 23 additions & 26 deletions src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,22 @@ private class StreamContext : IStreamGeometryContextImpl, IGeometryContext2
private bool _isFigureBroken;
private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke);

private void EnsureSeparateFillPath()
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
}

private void BreakFigure()
{
if (!_isFigureBroken)
{
_isFigureBroken = true;
EnsureSeparateFillPath();
}

}

/// <summary>
/// Initializes a new instance of the <see cref="StreamContext"/> class.
/// <param name="geometryImpl">Geometry to operate on.</param>
Expand Down Expand Up @@ -134,11 +150,8 @@ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc,
/// <inheritdoc />
public void BeginFigure(Point startPoint, bool isFilled)
{
if (!isFilled)
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();
}
if (!isFilled)
EnsureSeparateFillPath();

_isFilled = isFilled;
_startPoint = startPoint;
Expand Down Expand Up @@ -179,7 +192,7 @@ public void EndFigure(bool isClosed)
{
if (_isFigureBroken)
{
LineTo(_startPoint);
Stroke.LineTo(_startPoint.ToSKPoint());
_isFigureBroken = false;
}
else
Expand All @@ -204,11 +217,7 @@ public void LineTo(Point point, bool isStroked)
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();

_isFigureBroken = true;

BreakFigure();
Stroke.MoveTo((float)point.X, (float)point.Y);
}
if (Duplicate)
Expand Down Expand Up @@ -236,11 +245,7 @@ public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc,
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();

_isFigureBroken = true;

BreakFigure();
Stroke.MoveTo((float)point.X, (float)point.Y);
}
if (Duplicate)
Expand All @@ -263,11 +268,7 @@ public void CubicBezierTo(Point point1, Point point2, Point point3, bool isStrok
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();

_isFigureBroken = true;

BreakFigure();
Stroke.MoveTo((float)point3.X, (float)point3.Y);
}
if (Duplicate)
Expand All @@ -283,11 +284,7 @@ public void QuadraticBezierTo(Point point1, Point point2, bool isStroked)
}
else
{
if (Stroke == Fill)
_geometryImpl._fillPath = Stroke.Clone();

_isFigureBroken = true;

BreakFigure();
Stroke.MoveTo((float)point2.X, (float)point2.Y);
}
if (Duplicate)
Expand Down
1 change: 1 addition & 0 deletions tests/Avalonia.RenderTests.WpfCompare/CrossUI.Wpf.cs
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ s switch
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),
CrossPathSegment.PolyLine polyLine => new PolyLineSegment(polyLine.Points.Select(p => p.ToWpf()).ToList(), polyLine.IsStroked),
_ => throw new NotImplementedException(),
}), f.Closed)))
};
Expand Down
33 changes: 32 additions & 1 deletion tests/Avalonia.RenderTests/CrossTests/CrossGeometryTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Drawing.Drawing2D;
using System.Linq;
using Avalonia.Media;
using CrossUI;
Expand Down Expand Up @@ -109,7 +110,37 @@ public void Should_Render_Geometry_With_Strokeless_Lines()
Background = brush
});
}



[CrossFact]
public void Should_Render_PolyLineSegment_With_Strokeless_Lines()
{
var brush = new CrossSolidColorBrush(Colors.Blue);
var pen = new CrossPen()
{
Brush = new CrossSolidColorBrush(Colors.Red),
Thickness = 8
};
var figure = new CrossPathFigure()
{
Closed = true,
Segments =
{
new CrossPathSegment.PolyLine([new(0, 0), new(100, 0), new(100, 100), new(0, 100), new(0, 0)], false)
}
};
var geometry = new CrossPathGeometry { Figures = { figure } };

var control = new CrossFuncControl(ctx => ctx.DrawGeometry(brush, pen, geometry))
{
Width = 100,
Height = 100,
};

RenderAndCompare(control,
$"{nameof(Should_Render_PolyLineSegment_With_Strokeless_Lines)}");
}

// Skip the test for now
#if !AVALONIA_SKIA
[CrossTheory,
Expand Down
5 changes: 5 additions & 0 deletions tests/Avalonia.RenderTests/CrossUI/CrossUI.Avalonia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,11 @@ s switch
Point2 = q.Point2,
IsStroked = q.IsStroked
},
CrossPathSegment.PolyLine p => new PolyLineSegment()
{
Points = p.Points.ToList(),
IsStroked = p.IsStroked
},
_ => throw new InvalidOperationException()
}))
}))
Expand Down
1 change: 1 addition & 0 deletions tests/Avalonia.RenderTests/CrossUI/CrossUI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ 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 record PolyLine(IEnumerable<Point> Points, bool IsStroked) : CrossPathSegment(IsStroked);
}

public class CrossDrawingBrush : CrossTileBrush
Expand Down
23 changes: 23 additions & 0 deletions tests/Avalonia.Skia.UnitTests/CombinedGeometryImplTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using SkiaSharp;
using Xunit;

namespace Avalonia.Skia.UnitTests;

public class CombinedGeometryImplTests
{
[Fact]
public void Combining_Fill_With_Empty_Stroke_Returns_Fill_Bounds()
{
var fill = new SKPath();
fill.LineTo(100, 0);
fill.LineTo(100, 100);
fill.LineTo(0, 100);
fill.Close();

var stroke = new SKPath();

var result = new CombinedGeometryImpl(stroke, fill);

Assert.Equal(new Rect(0, 0, 100, 100), result.Bounds);
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit eb4f575

Please sign in to comment.