Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions Svg.Editor.Core.Tests/SelectionToolTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SelectionTool>().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<SelectionTool>().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()
{
Expand Down
5 changes: 4 additions & 1 deletion Svg.Editor.Core/Tools/SelectionTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment thread
Zeljko-Predjeskovic marked this conversation as resolved.
{
// 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);
}

Expand Down
23 changes: 23 additions & 0 deletions Svg.Tests.Win/SvgHitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = @"<svg>
<rect id=""bottom"" x=""100"" y=""100"" width=""100"" height=""100"" style=""fill:rgb(255,0,0);stroke:none"" />
<rect id=""top"" x=""100"" y=""100"" width=""100"" height=""100"" style=""fill:rgb(0,0,255);stroke:none"" />
</svg>";
var svg = SvgDocument.FromSvg<SvgDocument>(rawSvg);
var rect = RectangleF.Create(150f, 150f, 10, 10);

// Act
var result = svg.HitTest<SvgRectangle>(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");
}
}
}
Loading