From c427ab49f624b51e309fc3a9219edfe1efc2e784 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Dec 2015 22:09:33 +0000 Subject: [PATCH] Cull controls outside clip bounds. --- src/Perspex.SceneGraph/Point.cs | 18 ++ src/Perspex.SceneGraph/Rect.cs | 42 ++++ .../Rendering/RendererMixin.cs | 55 ++++- .../VisualTree/BoundsTracker.cs | 2 +- .../Perspex.SceneGraph.UnitTests.csproj | 5 + .../RenderTests_Culling.cs | 188 ++++++++++++++++++ .../packages.config | 1 + 7 files changed, 304 insertions(+), 7 deletions(-) create mode 100644 tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs diff --git a/src/Perspex.SceneGraph/Point.cs b/src/Perspex.SceneGraph/Point.cs index d87c570e9c..6a618dd893 100644 --- a/src/Perspex.SceneGraph/Point.cs +++ b/src/Perspex.SceneGraph/Point.cs @@ -229,6 +229,24 @@ namespace Perspex return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); } + /// + /// Transforms the point by a matrix. + /// + /// The transform. + /// The transformed point. + public Point Transform(Matrix transform) + { + var x = X; + var y = Y; + var xadd = y * transform.M21 + transform.M31; + var yadd = x * transform.M12 + transform.M32; + x *= transform.M11; + x += xadd; + y *= transform.M22; + y += yadd; + return new Point(x, y); + } + /// /// Returns a new point with the specified X coordinate. /// diff --git a/src/Perspex.SceneGraph/Rect.cs b/src/Perspex.SceneGraph/Rect.cs index 0b3658ebfc..af8a32a593 100644 --- a/src/Perspex.SceneGraph/Rect.cs +++ b/src/Perspex.SceneGraph/Rect.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Linq; namespace Perspex { @@ -360,6 +361,47 @@ namespace Perspex return (rect.X < Right) && (X < rect.Right) && (rect.Y < Bottom) && (Y < rect.Bottom); } + /// + /// Returns the axis-aligned bounding box of a transformed rectangle. + /// + /// The transform. + /// The bounding box + public Rect TransformToAABB(Matrix matrix) + { + var points = new[] + { + TopLeft.Transform(matrix), + TopRight.Transform(matrix), + BottomRight.Transform(matrix), + BottomLeft.Transform(matrix), + }; + + var left = double.MaxValue; + var right = double.MinValue; + var top = double.MaxValue; + var bottom = double.MinValue; + + foreach (var p in points) + { + if (p.X < left) left = p.X; + if (p.X > right) right = p.X; + if (p.Y < top) top = p.Y; + if (p.Y > bottom) bottom = p.Y; + } + + return new Rect(new Point(left, top), new Point(right, bottom)); + } + + /// + /// Translates the rectangle by an offset. + /// + /// The offset. + /// The translated rectangle. + public Rect Translate(Vector offset) + { + return new Rect(Position + offset, Size); + } + /// /// Returns the string representation of the rectangle. /// diff --git a/src/Perspex.SceneGraph/Rendering/RendererMixin.cs b/src/Perspex.SceneGraph/Rendering/RendererMixin.cs index 89c3ed4d0f..7997ea3d50 100644 --- a/src/Perspex.SceneGraph/Rendering/RendererMixin.cs +++ b/src/Perspex.SceneGraph/Rendering/RendererMixin.cs @@ -76,8 +76,24 @@ namespace Perspex.Rendering /// The visual to render. /// The drawing context. public static void Render(this DrawingContext context, IVisual visual) + { + context.Render(visual, visual.Bounds); + } + + /// + /// Renders the specified visual. + /// + /// The visual to render. + /// The drawing context. + /// + /// The current clip rect, in coordinates relative to . + /// + private static void Render(this DrawingContext context, IVisual visual, Rect clipRect) { var opacity = visual.Opacity; + var clipToBounds = visual.ClipToBounds; + var bounds = new Rect(visual.Bounds.Size); + if (visual.IsVisible && opacity > 0) { var m = Matrix.CreateTranslation(visual.Bounds.Position); @@ -88,33 +104,48 @@ namespace Perspex.Rendering { var origin = visual.TransformOrigin.ToPixels(new Size(visual.Bounds.Width, visual.Bounds.Height)); var offset = Matrix.CreateTranslation(origin); - renderTransform = (-offset)*visual.RenderTransform.Value*(offset); + renderTransform = (-offset) * visual.RenderTransform.Value * (offset); + } + + m = renderTransform * m; + + if (clipToBounds) + { + clipRect = clipRect.Intersect(new Rect(visual.Bounds.Size)); } - m = renderTransform*m; using (context.PushPostTransform(m)) using (context.PushOpacity(opacity)) - using (visual.ClipToBounds ? context.PushClip(new Rect(visual.Bounds.Size)) : default(DrawingContext.PushedState)) + using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState)) using (context.PushTransformContainer()) { visual.Render(context); + var lst = GetSortedVisualList(visual.VisualChildren); + foreach (var child in lst) { - context.Render(child); + var childBounds = GetTransformedBounds(child); + + if (clipRect.Intersects(childBounds)) + { + var childClipRect = clipRect.Translate(-childBounds.Position); + context.Render(child, childClipRect); + } } + ReturnListToPool(lst); } } } - static void ReturnListToPool(List lst) + private static void ReturnListToPool(List lst) { lst.Clear(); s_listPool.Push(lst); } - static List GetSortedVisualList(IReadOnlyList source) + private static List GetSortedVisualList(IReadOnlyList source) { var lst = s_listPool.Count == 0 ? new List() : s_listPool.Pop(); for (var c = 0; c < source.Count; c++) @@ -123,6 +154,18 @@ namespace Perspex.Rendering return lst; } + private static Rect GetTransformedBounds(IVisual visual) + { + if (visual.RenderTransform == null) + { + return visual.Bounds; + } + else + { + return visual.Bounds.TransformToAABB(visual.RenderTransform.Value); + } + } + class ZIndexComparer : IComparer { public int Compare(IVisual x, IVisual y) => x.ZIndex.CompareTo(y.ZIndex); diff --git a/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs b/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs index d0bc9d763a..9c243efc0c 100644 --- a/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs +++ b/src/Perspex.SceneGraph/VisualTree/BoundsTracker.cs @@ -47,7 +47,7 @@ namespace Perspex.VisualTree var bounds = boundsSubscriptions.CombineLatest().Select(ExtractBounds); // TODO: Track transform and clip rectangle. - return bounds.Select(x => new TransformedBounds((Rect)x, (Rect)new Rect(), (Matrix)Matrix.Identity)); + return bounds.Select(x => new TransformedBounds(x, new Rect(), Matrix.Identity)); } /// diff --git a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj index d161b1264f..d1a6119151 100644 --- a/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj +++ b/tests/Perspex.SceneGraph.UnitTests/Perspex.SceneGraph.UnitTests.csproj @@ -40,6 +40,10 @@ 4 + + ..\..\packages\Moq.4.2.1507.0118\lib\net40\Moq.dll + True + ..\..\packages\xunit.assert.2.0.0\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid+Xamarin.iOS\xunit.assert.dll @@ -80,6 +84,7 @@ + diff --git a/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs b/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs new file mode 100644 index 0000000000..29a4a6654f --- /dev/null +++ b/tests/Perspex.SceneGraph.UnitTests/RenderTests_Culling.cs @@ -0,0 +1,188 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Moq; +using Perspex.Controls; +using Perspex.Media; +using Perspex.Rendering; +using Xunit; + +namespace Perspex.SceneGraph.UnitTests +{ + public class RenderTests_Culling + { + [Fact] + public void In_Bounds_Control_Should_Be_Rendered() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 98, + [Canvas.TopProperty] = 98, + }) + } + }; + + Render(container); + + Assert.True(target.Rendered); + } + + [Fact] + public void Out_Of_Bounds_Control_Should_Not_Be_Rendered() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 110, + [Canvas.TopProperty] = 110, + }) + } + }; + + Render(container); + + Assert.False(target.Rendered); + } + + [Fact] + public void Out_Of_Bounds_Child_Control_Should_Not_Be_Rendered() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + new Canvas + { + Width = 100, + Height = 100, + [Canvas.LeftProperty] = 50, + [Canvas.TopProperty] = 50, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 50, + [Canvas.TopProperty] = 50, + }) + } + } + } + }; + + Render(container); + + Assert.False(target.Rendered); + } + + + [Fact] + public void Nested_ClipToBounds_Should_Be_Respected() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + new Canvas + { + Width = 50, + Height = 50, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 50, + [Canvas.TopProperty] = 50, + }) + } + } + } + }; + + Render(container); + + Assert.False(target.Rendered); + } + + [Fact] + public void RenderTransform_Should_Be_Respected() + { + TestControl target; + var container = new Canvas + { + Width = 100, + Height = 100, + ClipToBounds = true, + Children = new Controls.Controls + { + (target = new TestControl + { + Width = 10, + Height = 10, + [Canvas.LeftProperty] = 110, + [Canvas.TopProperty] = 110, + RenderTransform = new TranslateTransform(-100, -100), + }) + } + }; + + Render(container); + + Assert.True(target.Rendered); + } + + private void Render(IControl control) + { + var ctx = CreateDrawingContext(); + control.Measure(Size.Infinity); + control.Arrange(new Rect(control.DesiredSize)); + ctx.Render(control); + } + + private DrawingContext CreateDrawingContext() + { + return new DrawingContext(Mock.Of()); + } + + private class TestControl : Control + { + public bool Rendered { get; private set; } + + public override void Render(DrawingContext context) + { + Rendered = true; + } + } + } +} diff --git a/tests/Perspex.SceneGraph.UnitTests/packages.config b/tests/Perspex.SceneGraph.UnitTests/packages.config index 8cbc4ec26f..3cf2b984d5 100644 --- a/tests/Perspex.SceneGraph.UnitTests/packages.config +++ b/tests/Perspex.SceneGraph.UnitTests/packages.config @@ -1,5 +1,6 @@  +