1
0
Эх сурвалжийг харах

Merge pull request #3909 from AvaloniaUI/fixes/calculate-bounds-for-drawline

Calculate LineNode bounds, (Pen caps and Pen Thickness too)
Steven Kirk 5 жил өмнө
parent
commit
0861f663fc

+ 53 - 0
samples/RenderDemo/Controls/LineBoundsDemoControl.cs

@@ -0,0 +1,53 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Media;
+using Avalonia.Rendering.SceneGraph;
+using Avalonia.Threading;
+
+namespace RenderDemo.Controls
+{
+    public class LineBoundsDemoControl : Control
+    {
+        static LineBoundsDemoControl()
+        {
+            AffectsRender<LineBoundsDemoControl>(AngleProperty);
+        }
+
+        public LineBoundsDemoControl()
+        {
+            var timer = new DispatcherTimer();
+            timer.Interval = TimeSpan.FromSeconds(1 / 60);
+            timer.Tick += (sender, e) => Angle += Math.PI / 360;
+            timer.Start();
+        }
+
+        public static readonly StyledProperty<double> AngleProperty =
+            AvaloniaProperty.Register<LineBoundsDemoControl, double>(nameof(Angle));        
+
+        public double Angle
+        {
+            get => GetValue(AngleProperty);
+            set => SetValue(AngleProperty, value);
+        }
+
+        public override void Render(DrawingContext drawingContext)
+        {
+            var lineLength = Math.Sqrt((100 * 100) + (100 * 100));
+
+            var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength);
+            var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength);
+
+
+            var p1 = new Point(200, 200);
+            var p2 = new Point(p1.X + diffX, p1.Y + diffY);
+
+            var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square);
+            var boundPen = new Pen(Brushes.Black);
+
+            drawingContext.DrawLine(pen, p1, p2);
+
+            drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen));
+        }
+    }
+}

+ 3 - 0
samples/RenderDemo/MainWindow.xaml

@@ -44,6 +44,9 @@
       <TabItem Header="GlyphRun">
         <pages:GlyphRunPage/>
       </TabItem>
+      <TabItem Header="LineBounds">
+        <pages:LineBoundsPage />
+      </TabItem>
     </TabControl>
   </DockPanel>
 </Window>

+ 9 - 0
samples/RenderDemo/Pages/LineBoundsPage.xaml

@@ -0,0 +1,9 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             xmlns:controls="clr-namespace:RenderDemo.Controls"
+             x:Class="RenderDemo.Pages.LineBoundsPage">
+  <controls:LineBoundsDemoControl />
+</UserControl>

+ 19 - 0
samples/RenderDemo/Pages/LineBoundsPage.xaml.cs

@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace RenderDemo.Pages
+{
+    public class LineBoundsPage : UserControl
+    {
+        public LineBoundsPage()
+        {
+            this.InitializeComponent();
+        }
+
+        private void InitializeComponent()
+        {
+            AvaloniaXamlLoader.Load(this);
+        }
+    }
+}

+ 3 - 0
samples/RenderDemo/RenderDemo.csproj

@@ -3,6 +3,9 @@
     <OutputType>Exe</OutputType>
     <TargetFramework>netcoreapp3.1</TargetFramework>
   </PropertyGroup>
+  <ItemGroup>
+    <Compile Include="..\..\src\Avalonia.Visuals\Rendering\SceneGraph\LineBoundsHelper.cs" Link="LineBoundsHelper.cs" />
+  </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
     <ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />

+ 11 - 4
src/Avalonia.Visuals/Media/PixelRect.cs

@@ -377,7 +377,7 @@ namespace Avalonia
         /// <returns>The device-independent rect.</returns>
         public static PixelRect FromRect(Rect rect, double scale) => new PixelRect(
             PixelPoint.FromPoint(rect.Position, scale),
-            PixelSize.FromSize(rect.Size, scale));
+            FromPointCeiling(rect.BottomRight, new Vector(scale, scale)));
 
         /// <summary>
         /// Converts a <see cref="Rect"/> to device pixels using the specified scaling factor.
@@ -387,7 +387,7 @@ namespace Avalonia
         /// <returns>The device-independent point.</returns>
         public static PixelRect FromRect(Rect rect, Vector scale) => new PixelRect(
             PixelPoint.FromPoint(rect.Position, scale),
-            PixelSize.FromSize(rect.Size, scale));
+            FromPointCeiling(rect.BottomRight, scale));
 
         /// <summary>
         /// Converts a <see cref="Rect"/> to device pixels using the specified dots per inch (DPI).
@@ -397,7 +397,7 @@ namespace Avalonia
         /// <returns>The device-independent point.</returns>
         public static PixelRect FromRectWithDpi(Rect rect, double dpi) => new PixelRect(
             PixelPoint.FromPointWithDpi(rect.Position, dpi),
-            PixelSize.FromSizeWithDpi(rect.Size, dpi));
+            FromPointCeiling(rect.BottomRight, new Vector(dpi / 96, dpi / 96)));
 
         /// <summary>
         /// Converts a <see cref="Rect"/> to device pixels using the specified dots per inch (DPI).
@@ -407,7 +407,7 @@ namespace Avalonia
         /// <returns>The device-independent point.</returns>
         public static PixelRect FromRectWithDpi(Rect rect, Vector dpi) => new PixelRect(
             PixelPoint.FromPointWithDpi(rect.Position, dpi),
-            PixelSize.FromSizeWithDpi(rect.Size, dpi));
+            FromPointCeiling(rect.BottomRight, dpi / 96));
 
         /// <summary>
         /// Returns the string representation of the rectangle.
@@ -441,5 +441,12 @@ namespace Avalonia
                 );
             }
         }
+
+        private static PixelPoint FromPointCeiling(Point point, Vector scale)
+        {
+            return new PixelPoint(
+                (int)Math.Ceiling(point.X * scale.X),
+                (int)Math.Ceiling(point.Y * scale.Y));
+        }
     }
 }

+ 6 - 5
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -443,11 +443,12 @@ namespace Avalonia.Rendering
         private static Rect SnapToDevicePixels(Rect rect, double scale)
         {
             return new Rect(
-                Math.Floor(rect.X * scale) / scale,
-                Math.Floor(rect.Y * scale) / scale,
-                Math.Ceiling(rect.Width * scale) / scale,
-                Math.Ceiling(rect.Height * scale) / scale);
-                
+                new Point(
+                    Math.Floor(rect.X * scale) / scale,
+                    Math.Floor(rect.Y * scale) / scale),
+                new Point(
+                    Math.Ceiling(rect.Right * scale) / scale,
+                    Math.Ceiling(rect.Bottom * scale) / scale));
         }
 
         private void RenderOverlay(Scene scene, ref IDrawingContextImpl parentContent)

+ 2 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/BrushDrawOperation.cs

@@ -9,8 +9,8 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     internal abstract class BrushDrawOperation : DrawOperation
     {
-        public BrushDrawOperation(Rect bounds, Matrix transform, IPen pen)
-            : base(bounds, transform, pen)
+        public BrushDrawOperation(Rect bounds, Matrix transform)
+            : base(bounds, transform)
         {
         }
 

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs

@@ -9,7 +9,7 @@ namespace Avalonia.Rendering.SceneGraph
         public Matrix Transform { get; }
         public ICustomDrawOperation Custom { get; }
         public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) 
-            : base(custom.Bounds, transform, null)
+            : base(custom.Bounds, transform)
         {
             Transform = transform;
             Custom = custom;

+ 2 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/DrawOperation.cs

@@ -9,9 +9,9 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     internal abstract class DrawOperation : IDrawOperation
     {
-        public DrawOperation(Rect bounds, Matrix transform, IPen pen)
+        public DrawOperation(Rect bounds, Matrix transform)
         {
-            bounds = bounds.Inflate((pen?.Thickness ?? 0) / 2).TransformToAABB(transform);
+            bounds = bounds.TransformToAABB(transform);
             Bounds = new Rect(
                 new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)),
                 new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom)));

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
             IPen pen,
             IGeometryImpl geometry,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(geometry.GetRenderBounds(pen), transform, null)
+            : base(geometry.GetRenderBounds(pen), transform)
         {
             Transform = transform;
             Brush = brush?.ToImmutable();

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/GlyphRunNode.cs

@@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
             GlyphRun glyphRun,
             Point baselineOrigin,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(glyphRun.Bounds, transform, null)
+            : base(glyphRun.Bounds, transform)
         {
             Transform = transform;
             Foreground = foreground?.ToImmutable();

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="destRect">The destination rect.</param>
         /// <param name="bitmapInterpolationMode">The bitmap interpolation mode.</param>
         public ImageNode(Matrix transform, IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
-            : base(destRect, transform, null)
+            : base(destRect, transform)
         {
             Transform = transform;
             Source = source.Clone();

+ 68 - 0
src/Avalonia.Visuals/Rendering/SceneGraph/LineBoundsHelper.cs

@@ -0,0 +1,68 @@
+using System;
+using Avalonia.Media;
+
+namespace Avalonia.Rendering.SceneGraph
+{
+    internal static class LineBoundsHelper
+    {
+        private static double CalculateAngle(Point p1, Point p2)
+        {
+            var xDiff = p2.X - p1.X;
+            var yDiff = p2.Y - p1.Y;
+
+            return Math.Atan2(yDiff, xDiff);
+        }
+
+        internal static double CalculateOppSide(double angle, double hyp)
+        {
+            return Math.Sin(angle) * hyp;
+        }
+
+        internal static double CalculateAdjSide(double angle, double hyp)
+        {
+            return Math.Cos(angle) * hyp;
+        }
+
+        private static (Point p1, Point p2) TranslatePointsAlongTangent(Point p1, Point p2, double angle, double distance)
+        {
+            var xDiff = CalculateOppSide(angle, distance);
+            var yDiff = CalculateAdjSide(angle, distance);
+
+            var c1 = new Point(p1.X + xDiff, p1.Y - yDiff);
+            var c2 = new Point(p1.X - xDiff, p1.Y + yDiff);
+
+            var c3 = new Point(p2.X + xDiff, p2.Y - yDiff);
+            var c4 = new Point(p2.X - xDiff, p2.Y + yDiff);
+
+            var minX = Math.Min(c1.X, Math.Min(c2.X, Math.Min(c3.X, c4.X)));
+            var minY = Math.Min(c1.Y, Math.Min(c2.Y, Math.Min(c3.Y, c4.Y)));
+            var maxX = Math.Max(c1.X, Math.Max(c2.X, Math.Max(c3.X, c4.X)));
+            var maxY = Math.Max(c1.Y, Math.Max(c2.Y, Math.Max(c3.Y, c4.Y)));
+
+            return (new Point(minX, minY), new Point(maxX, maxY));
+        }
+
+        private static Rect CalculateBounds(Point p1, Point p2, double thickness, double angleToCorner)
+        {
+            var pts = TranslatePointsAlongTangent(p1, p2, angleToCorner, thickness / 2);
+
+            return new Rect(pts.p1, pts.p2);
+        }
+
+        public static Rect CalculateBounds(Point p1, Point p2, IPen p)
+        {
+            var radians = CalculateAngle(p1, p2);
+
+            if (p.LineCap != PenLineCap.Flat)
+            {
+                var pts = TranslatePointsAlongTangent(p1, p2, radians - Math.PI / 2, p.Thickness / 2);
+
+                return CalculateBounds(pts.p1, pts.p2, p.Thickness, radians);
+            }
+            else
+            {
+                return CalculateBounds(p1, p2, p.Thickness, radians);
+            }
+        }
+    }
+}

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@@ -25,7 +25,7 @@ namespace Avalonia.Rendering.SceneGraph
             Point p1,
             Point p2,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(new Rect(p1, p2), transform, pen)
+            : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform)
         {
             Transform = transform;
             Pen = pen?.ToImmutable();

+ 2 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/OpacityMaskNode.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// <param name="bounds">The bounds of the mask.</param>
         /// <param name="childScenes">Child scenes for drawing visual brushes.</param>
         public OpacityMaskNode(IBrush mask, Rect bounds, IDictionary<IVisual, Scene> childScenes = null)
-            : base(Rect.Empty, Matrix.Identity, null)
+            : base(Rect.Empty, Matrix.Identity)
         {
             Mask = mask?.ToImmutable();
             MaskBounds = bounds;
@@ -30,7 +30,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// opacity mask pop.
         /// </summary>
         public OpacityMaskNode()
-            : base(Rect.Empty, Matrix.Identity, null)
+            : base(Rect.Empty, Matrix.Identity)
         {
         }
 

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs

@@ -28,7 +28,7 @@ namespace Avalonia.Rendering.SceneGraph
             RoundedRect rect,
             BoxShadows boxShadows,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(boxShadows.TransformBounds(rect.Rect), transform, pen)
+            : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform)
         {
             Transform = transform;
             Brush = brush?.ToImmutable();

+ 1 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/TextNode.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Rendering.SceneGraph
             Point origin,
             IFormattedTextImpl text,
             IDictionary<IVisual, Scene> childScenes = null)
-            : base(text.Bounds.Translate(origin), transform, null)
+            : base(text.Bounds.Translate(origin), transform)
         {
             Transform = transform;
             Foreground = foreground?.ToImmutable();

+ 1 - 1
src/Skia/Avalonia.Skia/GeometryImpl.cs

@@ -95,7 +95,7 @@ namespace Avalonia.Skia
                 UpdatePathCache(strokeWidth);
             }
             
-            return _pathCache.CachedGeometryRenderBounds.Inflate(strokeWidth / 2.0);
+            return _pathCache.CachedGeometryRenderBounds;
         }
         
         /// <inheritdoc />

+ 46 - 0
tests/Avalonia.Visuals.UnitTests/Media/PixelRectTests.cs

@@ -0,0 +1,46 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace Avalonia.Visuals.UnitTests.Media
+{
+    public class PixelRectTests
+    {
+        [Fact]
+        public void FromRect_Snaps_To_Device_Pixels()
+        {
+            var rect = new Rect(189, 189, 26, 164);
+            var result = PixelRect.FromRect(rect, 1.5);
+
+            Assert.Equal(new PixelRect(283, 283, 40, 247), result);
+        }
+
+        [Fact]
+        public void FromRect_Vector_Snaps_To_Device_Pixels()
+        {
+            var rect = new Rect(189, 189, 26, 164);
+            var result = PixelRect.FromRect(rect, new Vector(1.5, 1.5));
+
+            Assert.Equal(new PixelRect(283, 283, 40, 247), result);
+        }
+
+        [Fact]
+        public void FromRectWithDpi_Snaps_To_Device_Pixels()
+        {
+            var rect = new Rect(189, 189, 26, 164);
+            var result = PixelRect.FromRectWithDpi(rect, 144);
+
+            Assert.Equal(new PixelRect(283, 283, 40, 247), result);
+        }
+
+        [Fact]
+        public void FromRectWithDpi_Vector_Snaps_To_Device_Pixels()
+        {
+            var rect = new Rect(189, 189, 26, 164);
+            var result = PixelRect.FromRectWithDpi(rect, new Vector(144, 144));
+
+            Assert.Equal(new PixelRect(283, 283, 40, 247), result);
+        }
+    }
+}

+ 15 - 2
tests/Avalonia.Visuals.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs

@@ -35,7 +35,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             double expectedWidth,
             double expectedHeight)
         {
-            var target = new TestDrawOperation(
+            var target = new TestRectangleDrawOperation(
                 new Rect(x, y, width, height),
                 Matrix.CreateScale(scaleX, scaleY),
                 penThickness.HasValue ? new Pen(Brushes.Black, penThickness.Value) : null);
@@ -74,10 +74,23 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph
             geometryNode.HitTest(new Point());
         }
 
+        private class TestRectangleDrawOperation : RectangleNode
+        {
+            public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) 
+                : base(transform, pen.Brush, pen, bounds, new BoxShadows())
+            {
+
+            }
+
+            public override bool HitTest(Point p) => false;
+
+            public override void Render(IDrawingContextImpl context) { }
+        }
+
         private class TestDrawOperation : DrawOperation
         {
             public TestDrawOperation(Rect bounds, Matrix transform, Pen pen)
-                :base(bounds, transform, pen)
+                :base(bounds, transform)
             {
             }