diff --git a/Svg.Editor.Core.Tests/SelectionToolTests.cs b/Svg.Editor.Core.Tests/SelectionToolTests.cs index 8dec5e837..4595e3d34 100644 --- a/Svg.Editor.Core.Tests/SelectionToolTests.cs +++ b/Svg.Editor.Core.Tests/SelectionToolTests.cs @@ -74,6 +74,95 @@ public async Task IfUserTapsCanvas_AndTapPositionDoesNotIntersectWithSelectedEle Assert.AreEqual(0, Canvas.SelectedElements.Count); } + [Test] + public async Task IfUserTapsOverlappingElements_OnlySelectsTopMostElement() + { + // Arrange + await Canvas.EnsureInitialized(); + var txtTool = Canvas.Tools.OfType().Single(); + Canvas.ActiveTool = txtTool; + Canvas.ScreenWidth = 800; + Canvas.ScreenHeight = 500; + + // "bottom" is declared first, so "top" (declared after, hence higher z-order) fully occludes it + var bottom = new SvgRectangle + { + X = new SvgUnit(SvgUnitType.Pixel, 200), + Y = new SvgUnit(SvgUnitType.Pixel, 200), + Width = new SvgUnit(SvgUnitType.Pixel, 100), + Height = new SvgUnit(SvgUnitType.Pixel, 100), + Fill = new SvgColourServer(Color.Create(255, 0, 0)) + }; + var top = new SvgRectangle + { + X = new SvgUnit(SvgUnitType.Pixel, 200), + Y = new SvgUnit(SvgUnitType.Pixel, 200), + Width = new SvgUnit(SvgUnitType.Pixel, 100), + Height = new SvgUnit(SvgUnitType.Pixel, 100), + Fill = new SvgColourServer(Color.Create(0, 0, 255)) + }; + Canvas.Document.Children.Add(bottom); + Canvas.Document.Children.Add(top); + + // Preassert + Assert.AreEqual(0, Canvas.SelectedElements.Count); + + // Act - tap somewhere in the middle of both fully-overlapping rectangles + var pt1 = PointF.Create(250, 250); + await Canvas.OnEvent(new PointerEvent(EventType.PointerDown, pt1, pt1, pt1, 1)); + await Canvas.OnEvent(new PointerEvent(EventType.PointerUp, pt1, pt1, pt1, 1)); + ((TestScheduler)SchedulerProvider.BackgroundScheduler).AdvanceBy(TimeSpan.FromSeconds(1).Ticks); + + // Assert - only the top-most element is selected, even though both are hit + Assert.AreEqual(1, Canvas.SelectedElements.Count); + Assert.AreSame(top, Canvas.SelectedElements.Single()); + } + + [Test] + public async Task IfUserDrawsSelectionRectangle_AndOverlappingElementsAreContained_AllOverlappingElementsAreSelected() + { + // Arrange + await Canvas.EnsureInitialized(); + var txtTool = Canvas.Tools.OfType().Single(); + Canvas.ActiveTool = txtTool; + Canvas.ScreenWidth = 800; + Canvas.ScreenHeight = 500; + + var bottom = new SvgRectangle + { + X = new SvgUnit(SvgUnitType.Pixel, 200), + Y = new SvgUnit(SvgUnitType.Pixel, 200), + Width = new SvgUnit(SvgUnitType.Pixel, 100), + Height = new SvgUnit(SvgUnitType.Pixel, 100), + Fill = new SvgColourServer(Color.Create(255, 0, 0)) + }; + var top = new SvgRectangle + { + X = new SvgUnit(SvgUnitType.Pixel, 200), + Y = new SvgUnit(SvgUnitType.Pixel, 200), + Width = new SvgUnit(SvgUnitType.Pixel, 100), + Height = new SvgUnit(SvgUnitType.Pixel, 100), + Fill = new SvgColourServer(Color.Create(0, 0, 255)) + }; + Canvas.Document.Children.Add(bottom); + Canvas.Document.Children.Add(top); + + // Preassert + Assert.AreEqual(0, Canvas.SelectedElements.Count); + + // Act - drag a selection area that encloses both fully-overlapping rectangles + var start = PointF.Create(190, 190); + var end = PointF.Create(310, 310); + await Canvas.OnEvent(new PointerEvent(EventType.PointerDown, start, start, start, 1)); + await Canvas.OnEvent(new MoveEvent(start, start, end, end - start, 1)); + await Canvas.OnEvent(new PointerEvent(EventType.PointerUp, start, end, end, 1)); + + // Assert - unlike a tap, an area selection selects every element it contains + Assert.AreEqual(2, Canvas.SelectedElements.Count); + CollectionAssert.Contains(Canvas.SelectedElements, bottom); + CollectionAssert.Contains(Canvas.SelectedElements, top); + } + [Test] public async Task IfUserDrawsSelectionRectangle_AndElementsAreContained_ElementsAreSelected() { diff --git a/Svg.Editor.Core/Tools/SelectionTool.cs b/Svg.Editor.Core/Tools/SelectionTool.cs index 0ceae6668..5245c3dda 100644 --- a/Svg.Editor.Core/Tools/SelectionTool.cs +++ b/Svg.Editor.Core/Tools/SelectionTool.cs @@ -260,10 +260,13 @@ private void SelectElementsUnderArea(RectangleF selectionRectangle, ISvgDrawingC SelectElementsUnder(selectionRectangle, ws, selectionType, maxItems, 1); } - private void SelectElementsUnderPoint(RectangleF selectionRectangle, ISvgDrawingCanvas ws, SelectionType selectionType, int maxItems = int.MaxValue) + private void SelectElementsUnderPoint(RectangleF selectionRectangle, ISvgDrawingCanvas ws, SelectionType selectionType, int maxItems = 1) { // when selecting by tapping a point, we do want an element to be selected, if the point intersects with ANY child element of it // therefore we need "recursionLevel=max"! + // a tap is a single point, so it should only ever select the top-most (highest z-order) element under + // it, not every overlapping element - that's what dragging out a selection area is for (see SelectElementsUnderArea). + // HitTest already returns matches in top-to-bottom z-order, so taking just the first one is correct. SelectElementsUnder(selectionRectangle, ws, selectionType, maxItems, int.MaxValue); } diff --git a/Svg.Tests.Win/SvgHitTests.cs b/Svg.Tests.Win/SvgHitTests.cs index 86436681d..bfacd411a 100644 --- a/Svg.Tests.Win/SvgHitTests.cs +++ b/Svg.Tests.Win/SvgHitTests.cs @@ -390,5 +390,28 @@ public void Contains_Group_HasTransformations_IsHit(float x, float y, float wh, } } } + + [Test] + public void Intersect_TwoLayeredRectangles_OnlySelectsTopLayerRectangle() + { + // Arrange + // "top" is declared last, so it is rendered on top of (and occludes) "bottom" + var rawSvg = @" + + +"; + var svg = SvgDocument.FromSvg(rawSvg); + var rect = RectangleF.Create(150f, 150f, 10, 10); + + // Act + var result = svg.HitTest(rect, SelectionType.Intersect, HitTestResultMode.ReturnAllMatchingDescendants) + .ToArray(); + + // Assert + // both rectangles are hit, but the top-most (highest z-order) one must be reported first, + // so that a caller who only cares about a single selection (e.g. result.First()) selects "top" + result.Count().ShouldBe(2); + result[0].ID.ShouldBe("top"); + } } }