Browse Source

Implemented BeginFigure(isFilled: false)

pull/10491/head
Nikita Tsukanov 4 years ago
parent
commit
a9ef92bbeb
  1. 43
      src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
  2. 9
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  3. 5
      src/Skia/Avalonia.Skia/EllipseGeometryImpl.cs
  4. 43
      src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
  5. 31
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  6. 5
      src/Skia/Avalonia.Skia/LineGeometryImpl.cs
  7. 4
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  8. 5
      src/Skia/Avalonia.Skia/RectangleGeometryImpl.cs
  9. 90
      src/Skia/Avalonia.Skia/StreamGeometryImpl.cs
  10. 21
      src/Skia/Avalonia.Skia/TransformedGeometryImpl.cs
  11. 58
      tests/Avalonia.RenderTests/Shapes/PathTests.cs
  12. BIN
      tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png
  13. BIN
      tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png

43
src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs

@ -8,11 +8,25 @@ namespace Avalonia.Skia
/// </summary>
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; }
}
}

9
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);
}
/// <inheritdoc />

5
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;
}
}

43
src/Skia/Avalonia.Skia/GeometryGroupImpl.cs

@ -11,26 +11,51 @@ namespace Avalonia.Skia
{
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> 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; }
}
}

31
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!);
/// <inheritdoc />
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; }
/// <inheritdoc />
public bool FillContains(Point point)
{
return PathContainsCore(EffectivePath, point);
return PathContainsCore(FillPath, point);
}
/// <inheritdoc />
@ -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
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -128,7 +125,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
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
/// <inheritdoc />
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;

5
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)));

4
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);
}
/// <inheritdoc />

5
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;
}
}

90
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;
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
/// <param name="stroke">An existing Skia <see cref="SKPath"/> for the stroke.</param>
/// <param name="fill">An existing Skia <see cref="SKPath"/> for the fill, can also be null or the same as the stroke</param>
/// <param name="bounds">Precomputed path bounds.</param>
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();
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
/// <param name="path">An existing Skia <see cref="SKPath"/>.</param>
public StreamGeometryImpl(SKPath path) : this(path, path.TightBounds.ToAvaloniaRect())
private StreamGeometryImpl(SKPath path) : this(path, path, default(Rect))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public StreamGeometryImpl() : this(CreateEmptyPath(), default)
public StreamGeometryImpl() : this(CreateEmptyPath())
{
}
/// <inheritdoc />
public override SKPath EffectivePath => _effectivePath;
public override SKPath? StrokePath => _strokePath;
/// <inheritdoc />
public override SKPath? FillPath => _fillPath;
/// <inheritdoc />
public override Rect Bounds => _bounds;
@ -47,7 +51,9 @@ namespace Avalonia.Skia
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -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);
/// <summary>
/// Initializes a new instance of the <see cref="StreamContext"/> class.
@ -83,52 +92,79 @@ namespace Avalonia.Skia
public StreamContext(StreamGeometryImpl geometryImpl)
{
_geometryImpl = geometryImpl;
_path = _geometryImpl._effectivePath;
}
/// <inheritdoc />
/// <remarks>Will update bounds of passed geometry.</remarks>
public void Dispose()
{
_geometryImpl._bounds = _path.TightBounds.ToAvaloniaRect();
_geometryImpl._bounds = Stroke.TightBounds.ToAvaloniaRect();
_geometryImpl.InvalidateCaches();
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
@ -136,14 +172,16 @@ namespace Avalonia.Skia
{
if (isClosed)
{
_path.Close();
Stroke.Close();
if (Duplicate)
Fill.Close();
}
}
/// <inheritdoc />
public void SetFillRule(FillRule fillRule)
{
_path.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
Fill.FillType = fillRule == FillRule.EvenOdd ? SKPathFillType.EvenOdd : SKPathFillType.Winding;
}
}
}

21
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);
}
}
/// <inheritdoc />
public override SKPath? EffectivePath { get; }
public override SKPath? StrokePath { get; }
/// <inheritdoc />
public override SKPath? FillPath { get; }
/// <inheritdoc />
public IGeometryImpl SourceGeometry { get; }

58
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();
}
}
}

BIN
tests/TestFiles/Direct2D1/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

BIN
tests/TestFiles/Skia/Shapes/Path/BeginFigure_IsFilled_Is_Respected.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

Loading…
Cancel
Save