Skip to content

Commit 5539f01

Browse files
author
Dan Walmsley
authored
Merge pull request #12606 from AvaloniaUI/fixes/correct-render-bounds
When calculating geometry bounds take into account parameters that affect geometry bounds
2 parents 1f0d189 + 96da4f0 commit 5539f01

File tree

7 files changed

+141
-152
lines changed

7 files changed

+141
-152
lines changed

src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,5 @@ public override void Invoke(ref RenderDataNodeRenderContext context)
2525
context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!);
2626
}
2727

28-
public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen).CalculateBoundsWithLineCaps(ServerPen) ?? default;
28+
public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen) ?? default;
2929
}

src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Skia/Avalonia.Skia/DrawingContextImpl.cs

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,31 +1247,8 @@ internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize)
12471247
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
12481248
// TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
12491249

1250-
switch (pen.LineCap)
1251-
{
1252-
case PenLineCap.Round:
1253-
paint.StrokeCap = SKStrokeCap.Round;
1254-
break;
1255-
case PenLineCap.Square:
1256-
paint.StrokeCap = SKStrokeCap.Square;
1257-
break;
1258-
default:
1259-
paint.StrokeCap = SKStrokeCap.Butt;
1260-
break;
1261-
}
1262-
1263-
switch (pen.LineJoin)
1264-
{
1265-
case PenLineJoin.Miter:
1266-
paint.StrokeJoin = SKStrokeJoin.Miter;
1267-
break;
1268-
case PenLineJoin.Round:
1269-
paint.StrokeJoin = SKStrokeJoin.Round;
1270-
break;
1271-
default:
1272-
paint.StrokeJoin = SKStrokeJoin.Bevel;
1273-
break;
1274-
}
1250+
paint.StrokeCap = pen.LineCap.ToSKStrokeCap();
1251+
paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
12751252

12761253
paint.StrokeMiter = (float) pen.MiterLimit;
12771254

src/Skia/Avalonia.Skia/GeometryImpl.cs

Lines changed: 57 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -43,46 +43,11 @@ public bool FillContains(Point point)
4343
/// <inheritdoc />
4444
public bool StrokeContains(IPen? pen, Point point)
4545
{
46-
// Skia requires to compute stroke path to check for point containment.
47-
// Due to that we are caching using stroke width.
48-
// Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
46+
_pathCache.UpdateIfNeeded(StrokePath, pen);
4947

50-
var strokeWidth = (float)(pen?.Thickness ?? 0);
51-
52-
if (!_pathCache.HasCacheFor(strokeWidth))
53-
{
54-
UpdatePathCache(strokeWidth);
55-
}
56-
57-
return PathContainsCore(_pathCache.CachedStrokePath, point);
58-
}
59-
60-
/// <summary>
61-
/// Update path cache for given stroke width.
62-
/// </summary>
63-
/// <param name="strokeWidth">Stroke width.</param>
64-
private void UpdatePathCache(float strokeWidth)
65-
{
66-
var strokePath = new SKPath();
67-
68-
// For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
69-
if (Math.Abs(strokeWidth) < float.Epsilon)
70-
{
71-
_pathCache.Cache(strokePath, strokeWidth, Bounds);
72-
}
73-
else
74-
{
75-
var paint = SKPaintCache.Shared.Get();
76-
paint.IsStroke = true;
77-
paint.StrokeWidth = strokeWidth;
78-
paint.GetFillPath(StrokePath, strokePath);
79-
80-
SKPaintCache.Shared.ReturnReset(paint);
81-
82-
_pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
83-
}
48+
return PathContainsCore(_pathCache.ExpandedPath, point);
8449
}
85-
50+
8651
/// <summary>
8752
/// Check Skia path if it contains a point.
8853
/// </summary>
@@ -106,14 +71,8 @@ private static bool PathContainsCore(SKPath? path, Point point)
10671
/// <inheritdoc />
10772
public Rect GetRenderBounds(IPen? pen)
10873
{
109-
var strokeWidth = (float)(pen?.Thickness ?? 0);
110-
111-
if (!_pathCache.HasCacheFor(strokeWidth))
112-
{
113-
UpdatePathCache(strokeWidth);
114-
}
115-
116-
return _pathCache.CachedGeometryRenderBounds;
74+
_pathCache.UpdateIfNeeded(StrokePath, pen);
75+
return _pathCache.RenderBounds;
11776
}
11877

11978
/// <inheritdoc />
@@ -180,66 +139,70 @@ public bool TryGetSegment(double startDistance, double stopDistance, bool startO
180139
/// </summary>
181140
protected void InvalidateCaches()
182141
{
183-
_pathCache.Invalidate();
142+
_pathCache.Dispose();
143+
_pathCache = default;
184144
}
185145

186146
private struct PathCache
187147
{
188-
private float _cachedStrokeWidth;
189-
190-
/// <summary>
191-
/// Tolerance for two stroke widths to be deemed equal
192-
/// </summary>
193-
public const float Tolerance = float.Epsilon;
194-
195-
/// <summary>
196-
/// Cached contour path.
197-
/// </summary>
198-
public SKPath? CachedStrokePath { get; private set; }
199-
200-
/// <summary>
201-
/// Cached geometry render bounds.
202-
/// </summary>
203-
public Rect CachedGeometryRenderBounds { get; private set; }
204-
205-
/// <summary>
206-
/// Is cached valid for given stroke width.
207-
/// </summary>
208-
/// <param name="strokeWidth">Stroke width to check.</param>
209-
/// <returns>True, if CachedStrokePath can be used for given stroke width.</returns>
210-
public bool HasCacheFor(float strokeWidth)
148+
private double _width, _miterLimit;
149+
private PenLineCap _cap;
150+
private PenLineJoin _join;
151+
private SKPath? _path, _cachedFor;
152+
private Rect? _renderBounds;
153+
private static readonly SKPath s_emptyPath = new();
154+
155+
156+
public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect();
157+
public SKPath ExpandedPath => _path ?? s_emptyPath;
158+
159+
public void UpdateIfNeeded(SKPath? strokePath, IPen? pen)
211160
{
212-
return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance;
213-
}
214-
215-
/// <summary>
216-
/// Cache path for given stroke width. Takes ownership of a passed path.
217-
/// </summary>
218-
/// <param name="path">Path to cache.</param>
219-
/// <param name="strokeWidth">Stroke width to cache.</param>
220-
/// <param name="geometryRenderBounds">Render bounds to use.</param>
221-
public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds)
222-
{
223-
if (CachedStrokePath != path)
161+
var strokeWidth = pen?.Thickness ?? 0;
162+
var miterLimit = pen?.MiterLimit ?? 0;
163+
var cap = pen?.LineCap ?? default;
164+
var join = pen?.LineJoin ?? default;
165+
166+
if (_cachedFor == strokePath
167+
&& _path != null
168+
&& cap == _cap
169+
&& join == _join
170+
&& Math.Abs(_width - strokeWidth) < float.Epsilon
171+
&& (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon))
172+
// We are up to date
173+
return;
174+
175+
_renderBounds = null;
176+
_cachedFor = strokePath;
177+
_width = strokeWidth;
178+
_cap = cap;
179+
_join = join;
180+
_miterLimit = miterLimit;
181+
182+
if (strokePath == null || Math.Abs(strokeWidth) < float.Epsilon)
224183
{
225-
CachedStrokePath?.Dispose();
184+
_path = null;
185+
return;
226186
}
227187

228-
CachedStrokePath = path;
229-
CachedGeometryRenderBounds = geometryRenderBounds;
230-
_cachedStrokeWidth = strokeWidth;
188+
var paint = SKPaintCache.Shared.Get();
189+
paint.IsStroke = true;
190+
paint.StrokeWidth = (float)_width;
191+
paint.StrokeCap = cap.ToSKStrokeCap();
192+
paint.StrokeJoin = join.ToSKStrokeJoin();
193+
paint.StrokeMiter = (float)miterLimit;
194+
_path = new SKPath();
195+
paint.GetFillPath(strokePath, _path);
196+
197+
SKPaintCache.Shared.ReturnReset(paint);
231198
}
232199

233-
/// <summary>
234-
/// Invalidate cache state.
235-
/// </summary>
236-
public void Invalidate()
200+
public void Dispose()
237201
{
238-
CachedStrokePath?.Dispose();
239-
CachedStrokePath = null;
240-
CachedGeometryRenderBounds = default;
241-
_cachedStrokeWidth = default;
202+
_path?.Dispose();
203+
_path = null;
242204
}
205+
243206
}
244207
}
245208
}

src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,26 @@ public static SKTextAlign ToSKTextAlign(this TextAlignment a)
196196
}
197197
}
198198

199+
public static SKStrokeCap ToSKStrokeCap(this PenLineCap cap)
200+
{
201+
return cap switch
202+
{
203+
PenLineCap.Round => SKStrokeCap.Round,
204+
PenLineCap.Square => SKStrokeCap.Square,
205+
_ => SKStrokeCap.Butt
206+
};
207+
}
208+
209+
public static SKStrokeJoin ToSKStrokeJoin(this PenLineJoin join)
210+
{
211+
return join switch
212+
{
213+
PenLineJoin.Bevel => SKStrokeJoin.Bevel,
214+
PenLineJoin.Round => SKStrokeJoin.Round,
215+
_ => SKStrokeJoin.Miter
216+
};
217+
}
218+
199219
public static TextAlignment ToAvalonia(this SKTextAlign a)
200220
{
201221
switch (a)

src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
using System;
12
using Avalonia.Logging;
3+
using Avalonia.Media;
24
using Avalonia.Platform;
35
using SharpDX.Direct2D1;
6+
using Geometry = SharpDX.Direct2D1.Geometry;
7+
using PathGeometry = SharpDX.Direct2D1.PathGeometry;
48

59
namespace Avalonia.Direct2D1.Media
610
{
@@ -27,7 +31,20 @@ public GeometryImpl(Geometry geometry)
2731
/// <inheritdoc/>
2832
public Rect GetRenderBounds(Avalonia.Media.IPen pen)
2933
{
30-
return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia();
34+
if (pen == null || Math.Abs(pen.Thickness) < float.Epsilon)
35+
return Geometry.GetBounds().ToAvalonia();
36+
var originalBounds = Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
37+
switch (pen.LineCap)
38+
{
39+
case PenLineCap.Flat:
40+
return originalBounds;
41+
case PenLineCap.Round:
42+
return originalBounds.Inflate(pen.Thickness / 2);
43+
case PenLineCap.Square:
44+
return originalBounds.Inflate(pen.Thickness);
45+
default:
46+
throw new ArgumentOutOfRangeException();
47+
}
3148
}
3249

3350
/// <inheritdoc/>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System;
2+
using Avalonia.Controls.Shapes;
3+
using Avalonia.Layout;
4+
using Avalonia.Media;
5+
using Avalonia.Platform;
6+
using Avalonia.Rendering;
7+
using Avalonia.UnitTests;
8+
using Xunit;
9+
10+
namespace Avalonia.Skia.UnitTests
11+
{
12+
public class RenderBoundsTests
13+
{
14+
[Theory,
15+
InlineData("M10 20 L 20 10 L 30 20", PenLineCap.Round, PenLineJoin.Miter, 2, 10,
16+
8.585786819458008, 8.585786819458008, 22.828428268432617, 12.828428268432617),
17+
InlineData("M10 10 L 20 10", PenLineCap.Round, PenLineJoin.Miter,2, 10,
18+
9,9,12,2),
19+
InlineData("M10 10 L 20 15 L 10 20", PenLineCap.Flat, PenLineJoin.Miter, 2, 20,
20+
9.552786827087402, 9.105572700500488, 12.683281898498535, 11.788853645324707)
21+
22+
]
23+
public void RenderBoundsAreCorrectlyCalculated(string path, PenLineCap cap, PenLineJoin join, double thickness, double miterLimit, double x, double y, double width, double height)
24+
{
25+
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
26+
.With(renderInterface: new PlatformRenderInterface())))
27+
{
28+
var geo = PathGeometry.Parse(path);
29+
var pen = new Pen(Brushes.Black, thickness, null, cap, join, miterLimit);
30+
var bounds = geo.GetRenderBounds(pen);
31+
var tolerance = 0.001;
32+
if (
33+
Math.Abs(bounds.X - x) > tolerance
34+
|| Math.Abs(bounds.Y - y) > tolerance
35+
|| Math.Abs(bounds.Width - width) > tolerance
36+
|| Math.Abs(bounds.Height - height) > tolerance)
37+
Assert.Fail($"Expected {x}:{y}:{width}:{height}, got {bounds}");
38+
39+
Assert.Equal(new Rect(x, y, width, height), bounds);
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)