diff --git a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
index 170cc9d420..1a4f467f40 100644
--- a/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
@@ -8,11 +8,25 @@ namespace Avalonia.Skia
///
internal class CombinedGeometryImpl : GeometryImpl
{
- public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+ public CombinedGeometryImpl(SKPath? stroke, SKPath? fill)
{
- var path1 = (g1.PlatformImpl as GeometryImpl)?.EffectivePath;
- var path2 = (g2.PlatformImpl as GeometryImpl)?.EffectivePath;
+ StrokePath = stroke;
+ FillPath = fill;
+ Bounds = (stroke ?? fill)?.TightBounds.ToAvaloniaRect() ?? default;
+ }
+
+ public static CombinedGeometryImpl ForceCreate(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
+ {
+ if (g1.PlatformImpl is GeometryImpl i1
+ && g2.PlatformImpl is GeometryImpl i2
+ && TryCreate(combineMode, i1, i2) is { } result)
+ return result;
+
+ return new(null, null);
+ }
+ public static CombinedGeometryImpl? TryCreate(GeometryCombineMode combineMode, GeometryImpl g1, GeometryImpl g2)
+ {
var op = combineMode switch
{
GeometryCombineMode.Intersect => SKPathOp.Intersect,
@@ -21,13 +35,28 @@ namespace Avalonia.Skia
_ => SKPathOp.Union
};
- var path = path1?.Op(path2, op);
+ var stroke =
+ g1.StrokePath != null && g2.StrokePath != null
+ ? g1.StrokePath.Op(g2.StrokePath, op)
+ : null;
+
+ SKPath? fill = null;
+ if (g1.FillPath != null && g2.FillPath != null)
+ {
+ // Reuse stroke if fill paths are the same
+ if (ReferenceEquals(g1.FillPath, g1.StrokePath) && ReferenceEquals(g2.FillPath, g2.StrokePath))
+ fill = stroke;
+ else
+ fill = g1.FillPath.Op(g2.FillPath, op);
+ }
- EffectivePath = path;
- Bounds = path?.Bounds.ToAvaloniaRect() ?? default;
+ if (stroke == null && fill == null)
+ return null;
+ return new CombinedGeometryImpl(stroke, fill);
}
public override Rect Bounds { get; }
- public override SKPath? EffectivePath { get; }
+ public override SKPath? StrokePath { get; }
+ public override SKPath? FillPath { get; }
}
}
diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
index db7b068543..af0231579c 100644
--- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
+++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs
@@ -230,20 +230,21 @@ namespace Avalonia.Skia
var impl = (GeometryImpl) geometry;
var size = geometry.Bounds.Size;
- if (brush is not null)
+ if (brush is not null && impl.FillPath != null)
{
using (var fill = CreatePaint(_fillPaint, brush, size))
{
- Canvas.DrawPath(impl.EffectivePath, fill.Paint);
+ Canvas.DrawPath(impl.FillPath, fill.Paint);
}
}
if (pen is not null
+ && impl.StrokePath != null
&& TryCreatePaint(_strokePaint, pen, size.Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
- Canvas.DrawPath(impl.EffectivePath, stroke.Paint);
+ Canvas.DrawPath(impl.StrokePath, stroke.Paint);
}
}
}
@@ -639,7 +640,7 @@ namespace Avalonia.Skia
{
CheckLease();
Canvas.Save();
- Canvas.ClipPath(((GeometryImpl)clip).EffectivePath, SKClipOperation.Intersect, true);
+ Canvas.ClipPath(((GeometryImpl)clip).FillPath, SKClipOperation.Intersect, true);
}
///
diff --git a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs
index ac05691c67..64976d4eea 100644
--- a/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs
@@ -8,14 +8,15 @@ namespace Avalonia.Skia
internal class EllipseGeometryImpl : GeometryImpl
{
public override Rect Bounds { get; }
- public override SKPath EffectivePath { get; }
+ public override SKPath StrokePath { get; }
+ public override SKPath FillPath => StrokePath;
public EllipseGeometryImpl(Rect rect)
{
var path = new SKPath();
path.AddOval(rect.ToSKRect());
- EffectivePath = path;
+ StrokePath = path;
Bounds = rect;
}
}
diff --git a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
index 2828f9a9c1..01be42bad0 100644
--- a/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
@@ -11,26 +11,51 @@ namespace Avalonia.Skia
{
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList children)
{
- var path = new SKPath
+ var fillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd;
+ var count = children.Count;
+
+ var stroke = new SKPath
{
- FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd,
+ FillType = fillType
};
-
- var count = children.Count;
+ bool requiresFillPass = false;
for (var i = 0; i < count; ++i)
{
- if (children[i].PlatformImpl is GeometryImpl { EffectivePath: { } effectivePath })
+ if (children[i].PlatformImpl is GeometryImpl geo)
{
- path.AddPath(effectivePath);
+ if (geo.StrokePath != null)
+ stroke.AddPath(geo.StrokePath);
+ if (!ReferenceEquals(geo.StrokePath, geo.FillPath))
+ requiresFillPass = true;
}
}
+
+ StrokePath = stroke;
+
+ if (requiresFillPass)
+ {
+ var fill = new SKPath
+ {
+ FillType = fillType
+ };
+
+ for (var i = 0; i < count; ++i)
+ {
+ if (children[i].PlatformImpl is GeometryImpl { FillPath: { } fillPath })
+ fill.AddPath(fillPath);
+ }
+
+ FillPath = fill;
+ }
+ else
+ FillPath = stroke;
- EffectivePath = path;
- Bounds = path.Bounds.ToAvaloniaRect();
+ Bounds = stroke.TightBounds.ToAvaloniaRect();
}
public override Rect Bounds { get; }
- public override SKPath EffectivePath { get; }
+ public override SKPath StrokePath { get; }
+ public override SKPath FillPath { get; }
}
}
diff --git a/src/Skia/Avalonia.Skia/GeometryImpl.cs b/src/Skia/Avalonia.Skia/GeometryImpl.cs
index 34270c2078..aee84d1346 100644
--- a/src/Skia/Avalonia.Skia/GeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/GeometryImpl.cs
@@ -14,7 +14,7 @@ namespace Avalonia.Skia
private PathCache _pathCache;
private SKPathMeasure? _cachedPathMeasure;
- private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(EffectivePath!);
+ private SKPathMeasure CachedPathMeasure => _cachedPathMeasure ??= new SKPathMeasure(StrokePath!);
///
public abstract Rect Bounds { get; }
@@ -24,19 +24,20 @@ namespace Avalonia.Skia
{
get
{
- if (EffectivePath is null)
+ if (StrokePath is null)
return 0;
return CachedPathMeasure.Length;
}
}
- public abstract SKPath? EffectivePath { get; }
+ public abstract SKPath? StrokePath { get; }
+ public abstract SKPath? FillPath { get; }
///
public bool FillContains(Point point)
{
- return PathContainsCore(EffectivePath, point);
+ return PathContainsCore(FillPath, point);
}
///
@@ -74,7 +75,7 @@ namespace Avalonia.Skia
var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true;
paint.StrokeWidth = strokeWidth;
- paint.GetFillPath(EffectivePath, strokePath);
+ paint.GetFillPath(StrokePath, strokePath);
SKPaintCache.Shared.ReturnReset(paint);
@@ -96,14 +97,10 @@ namespace Avalonia.Skia
///
public IGeometryImpl? Intersect(IGeometryImpl geometry)
{
- if (EffectivePath is { } path
- && (geometry as GeometryImpl)?.EffectivePath is { } otherPath
- && path.Op(otherPath, SKPathOp.Intersect) is { } result)
- {
- return new StreamGeometryImpl(result);
- }
-
- return null;
+ var other = geometry as GeometryImpl;
+ if (other == null)
+ return null;
+ return CombinedGeometryImpl.TryCreate(GeometryCombineMode.Intersect, this, other);
}
///
@@ -128,7 +125,7 @@ namespace Avalonia.Skia
///
public bool TryGetPointAtDistance(double distance, out Point point)
{
- if (EffectivePath is null)
+ if (StrokePath is null)
{
point = new Point();
return false;
@@ -142,7 +139,7 @@ namespace Avalonia.Skia
///
public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent)
{
- if (EffectivePath is null)
+ if (StrokePath is null)
{
point = new Point();
tangent = new Point();
@@ -158,7 +155,7 @@ namespace Avalonia.Skia
public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure,
[NotNullWhen(true)] out IGeometryImpl? segmentGeometry)
{
- if (EffectivePath is null)
+ if (StrokePath is null)
{
segmentGeometry = null;
return false;
@@ -172,7 +169,7 @@ namespace Avalonia.Skia
if (res)
{
- segmentGeometry = new StreamGeometryImpl(_skPathSegment);
+ segmentGeometry = new StreamGeometryImpl(_skPathSegment, null);
}
return res;
diff --git a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs
index b102a4c119..2ac0a2e18c 100644
--- a/src/Skia/Avalonia.Skia/LineGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/LineGeometryImpl.cs
@@ -9,7 +9,8 @@ namespace Avalonia.Skia
internal class LineGeometryImpl : GeometryImpl
{
public override Rect Bounds { get; }
- public override SKPath EffectivePath { get; }
+ public override SKPath StrokePath { get; }
+ public override SKPath? FillPath => null;
public LineGeometryImpl(Point p1, Point p2)
{
@@ -17,7 +18,7 @@ namespace Avalonia.Skia
path.MoveTo(p1.ToSKPoint());
path.LineTo(p2.ToSKPoint());
- EffectivePath = path;
+ StrokePath = path;
Bounds = new Rect(
new Point(Math.Min(p1.X, p2.X), Math.Min(p1.Y, p2.Y)),
new Point(Math.Max(p1.X, p2.X), Math.Max(p1.Y, p2.Y)));
diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
index ab1c6b8816..9c4b326f14 100644
--- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
+++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
@@ -65,7 +65,7 @@ namespace Avalonia.Skia
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
- return new CombinedGeometryImpl(combineMode, g1, g2);
+ return CombinedGeometryImpl.ForceCreate(combineMode, g1, g2);
}
public IGeometryImpl BuildGlyphRunGeometry(GlyphRun glyphRun)
@@ -104,7 +104,7 @@ namespace Avalonia.Skia
SKFontCache.Shared.Return(skFont);
- return new StreamGeometryImpl(path);
+ return new StreamGeometryImpl(path, path);
}
///
diff --git a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs
index 93d453e8f0..2d127e07c9 100644
--- a/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs
@@ -8,14 +8,15 @@ namespace Avalonia.Skia
internal class RectangleGeometryImpl : GeometryImpl
{
public override Rect Bounds { get; }
- public override SKPath EffectivePath { get; }
+ public override SKPath StrokePath { get; }
+ public override SKPath? FillPath => StrokePath;
public RectangleGeometryImpl(Rect rect)
{
var path = new SKPath();
path.AddRect(rect.ToSKRect());
- EffectivePath = path;
+ StrokePath = path;
Bounds = rect;
}
}
diff --git a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
index 0c3289767e..eb081c4f50 100644
--- a/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Avalonia.Media;
using Avalonia.Platform;
using SkiaSharp;
@@ -10,36 +11,39 @@ namespace Avalonia.Skia
internal class StreamGeometryImpl : GeometryImpl, IStreamGeometryImpl
{
private Rect _bounds;
- private readonly SKPath _effectivePath;
+ private readonly SKPath _strokePath;
+ private SKPath? _fillPath;
///
/// Initializes a new instance of the class.
///
- /// An existing Skia .
+ /// An existing Skia for the stroke.
+ /// An existing Skia for the fill, can also be null or the same as the stroke
/// Precomputed path bounds.
- public StreamGeometryImpl(SKPath path, Rect bounds)
+ public StreamGeometryImpl(SKPath stroke, SKPath? fill, Rect? bounds = null)
{
- _effectivePath = path;
- _bounds = bounds;
+ _strokePath = stroke;
+ _fillPath = fill;
+ _bounds = bounds ?? stroke.TightBounds.ToAvaloniaRect();
}
- ///
- /// Initializes a new instance of the class.
- ///
- /// An existing Skia .
- public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect())
+ private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect))
{
+
}
///
/// Initializes a new instance of the class.
///
- public StreamGeometryImpl() : this(CreateEmptyPath(), default)
+ public StreamGeometryImpl() : this(CreateEmptyPath())
{
}
-
+
///
- public override SKPath EffectivePath => _effectivePath;
+ public override SKPath? StrokePath => _strokePath;
+
+ ///
+ public override SKPath? FillPath => _fillPath;
///
public override Rect Bounds => _bounds;
@@ -47,7 +51,9 @@ namespace Avalonia.Skia
///
public IStreamGeometryImpl Clone()
{
- return new StreamGeometryImpl(_effectivePath.Clone(), Bounds);
+ var stroke = _strokePath.Clone();
+ var fill = _fillPath == _strokePath ? stroke : _fillPath.Clone();
+ return new StreamGeometryImpl(stroke, fill, Bounds);
}
///
@@ -74,7 +80,10 @@ namespace Avalonia.Skia
private class StreamContext : IStreamGeometryContextImpl
{
private readonly StreamGeometryImpl _geometryImpl;
- private readonly SKPath _path;
+ private SKPath Stroke => _geometryImpl._strokePath;
+ private SKPath Fill => _geometryImpl._fillPath ??= new();
+ private bool _isFilled;
+ private bool Duplicate => _isFilled && !ReferenceEquals(_geometryImpl._fillPath, Stroke);
///
/// Initializes a new instance of the class.
@@ -83,52 +92,79 @@ namespace Avalonia.Skia
public StreamContext(StreamGeometryImpl geometryImpl)
{
_geometryImpl = geometryImpl;
- _path = _geometryImpl._effectivePath;
}
///
/// Will update bounds of passed geometry.
public void Dispose()
{
- _geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect();
+ _geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect();
_geometryImpl.InvalidateCaches();
}
///
public void ArcTo(Point point, Size size, double rotationAngle, bool isLargeArc, SweepDirection sweepDirection)
{
- _path.ArcTo(
+ var arc = isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small;
+ var sweep = sweepDirection == SweepDirection.Clockwise
+ ? SKPathDirection.Clockwise
+ : SKPathDirection.CounterClockwise;
+ Stroke.ArcTo(
(float)size.Width,
(float)size.Height,
(float)rotationAngle,
- isLargeArc ? SKPathArcSize.Large : SKPathArcSize.Small,
- sweepDirection == SweepDirection.Clockwise ? SKPathDirection.Clockwise : SKPathDirection.CounterClockwise,
+ arc,
+ sweep,
(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 BeginFigure(Point startPoint, bool isFilled)
{
- _path.MoveTo((float)startPoint.X, (float)startPoint.Y);
+ if (!isFilled)
+ {
+ if (Stroke == Fill)
+ _geometryImpl._fillPath = Stroke.Clone();
+ }
+
+ _isFilled = isFilled;
+ Stroke.MoveTo((float)startPoint.X, (float)startPoint.Y);
+ if(Duplicate)
+ Fill.MoveTo((float)startPoint.X, (float)startPoint.Y);
}
///
public void CubicBezierTo(Point point1, Point point2, Point point3)
{
- _path.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (float)point3.X, (float)point3.Y);
+ Stroke.CubicTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y, (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)
{
- _path.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
+ Stroke.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
+ if(Duplicate)
+ Fill.QuadTo((float)point1.X, (float)point1.Y, (float)point2.X, (float)point2.Y);
}
///
public void LineTo(Point point)
{
- _path.LineTo((float)point.X, (float)point.Y);
+ Stroke.LineTo((float)point.X, (float)point.Y);
+ if(Duplicate)
+ Fill.LineTo((float)point.X, (float)point.Y);
}
///
@@ -136,14 +172,16 @@ namespace Avalonia.Skia
{
if (isClosed)
{
- _path.Close();
+ Stroke.Close();
+ if (Duplicate)
+ Fill.Close();
}
}
///
public void SetFillRule(FillRule fillRule)
{
- _path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
+ Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
}
}
}
diff --git a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
index fb3c2e403f..2dee8c318c 100644
--- a/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
+++ b/src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
@@ -17,16 +17,27 @@ namespace Avalonia.Skia
{
SourceGeometry = source;
Transform = transform;
+ var matrix = transform.ToSKMatrix();
- var transformedPath = source.EffectivePath.Clone();
- transformedPath?.Transform(transform.ToSKMatrix());
-
- EffectivePath = transformedPath;
+ var transformedPath = StrokePath = source.StrokePath.Clone();
+ transformedPath?.Transform(matrix);
+
Bounds = transformedPath?.TightBounds.ToAvaloniaRect() ?? default;
+
+ if (ReferenceEquals(source.StrokePath, source.FillPath))
+ FillPath = transformedPath;
+ else if (source.FillPath != null)
+ {
+ FillPath = transformedPath = source.FillPath.Clone();
+ transformedPath.Transform(matrix);
+ }
}
///
- public override SKPath? EffectivePath { get; }
+ public override SKPath? StrokePath { get; }
+
+ ///
+ public override SKPath? FillPath { get; }
///
public IGeometryImpl SourceGeometry { get; }
diff --git a/tests/Avalonia.RenderTests/Shapes/PathTests.cs b/tests/Avalonia.RenderTests/Shapes/PathTests.cs
index bac16cca88..bf375121de 100644
--- a/tests/Avalonia.RenderTests/Shapes/PathTests.cs
+++ b/tests/Avalonia.RenderTests/Shapes/PathTests.cs
@@ -376,5 +376,63 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
await RenderToFile(target);
CompareImages();
}
+
+ [Fact]
+ public async Task BeginFigure_IsFilled_Is_Respected()
+ {
+ var target = new Border
+ {
+ Width = 200,
+ Height = 200,
+ Background = Brushes.White,
+ Child = new Path
+ {
+ Fill = Brushes.Black,
+ Stroke = Brushes.Black,
+ StrokeThickness = 10,
+ Data = new PathGeometry()
+ {
+ Figures = new()
+ {
+ new PathFigure
+ {
+ IsFilled = false, IsClosed = false,
+ StartPoint = new Point(170,170),
+ Segments = new ()
+ {
+ new LineSegment
+ {
+ Point = new Point(60, 170)
+ },
+ new LineSegment
+ {
+ Point = new Point(60, 60)
+ }
+ }
+ },
+ new PathFigure
+ {
+ IsFilled = true, IsClosed = true,
+ StartPoint = new Point(60,20),
+ Segments = new ()
+ {
+ new LineSegment
+ {
+ Point = new Point(20, 60)
+ },
+ new LineSegment
+ {
+ Point = new Point(100, 60)
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ await RenderToFile(target);
+ CompareImages();
+ }
}
}
diff --git a/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png
new file mode 100644
index 0000000000..d67d71c520
Binary files /dev/null and b/tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png differ
diff --git a/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png b/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png
new file mode 100644
index 0000000000..d67d71c520
Binary files /dev/null and b/tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png differ