From e33cd1e093ca5ed5a9a11b710f9c2d76ec6d6b50 Mon Sep 17 00:00:00 2001 From: Atria1234 Date: Fri, 16 Sep 2022 16:34:56 +0200 Subject: [PATCH] Implemented rectangle drag placing objects #8 - hold shift while starting placing single object to initiate --- .../Extensions/IEnumerableExtensions.cs | 60 ++++ AnnoDesigner/AnnoCanvas.xaml.cs | 283 +++++++++++------- .../IEnumerableExtensionsTests.cs | 54 ++++ 3 files changed, 281 insertions(+), 116 deletions(-) diff --git a/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs b/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs index f2023daf..3abd6531 100644 --- a/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs +++ b/AnnoDesigner.Core/Extensions/IEnumerableExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Windows; using AnnoDesigner.Core.Models; namespace AnnoDesigner.Core.Extensions @@ -115,5 +116,64 @@ public static bool IsIgnoredObject(this AnnoObject annoObject) { return string.Equals(annoObject.Template, "Blocker", StringComparison.OrdinalIgnoreCase); } + + /// + /// Generates sequence of double values from to . + /// Can generate sequences in descending order. + /// Generated sequence always have . By default is excluded from sequence unless is set. + /// + /// First value of the sequence. + /// Last value of the sequence. + /// Increment between iterations. + /// Returns value after last iteration if set to true. + /// Sequence of doubles between provided bounds. + public static IEnumerable Range(double from, double to, double step = 1, bool inclusiveTo = false) + { + if (step <= 0) + { + throw new ArgumentOutOfRangeException(nameof(step), "Value must be positive"); + } + + double i; + if (to > from) + { + for (i = from; i < to; i += step) + { + yield return i; + } + } + else + { + for (i = from; i > to; i -= step) + { + yield return i; + } + } + if (inclusiveTo) + { + yield return i; + } + } + + /// + /// Generates sequence of grid points in rectangle specified by and . + /// Uses to generate X and Y coordinates. + /// + /// Point where rect should start at. + /// Point where rect should end at. + /// Steps in horizontal and vertical directions between points. Defaults to (1, 1). + /// Includes one point after the . + /// Sequence of grid points in specified rectangle. + public static IEnumerable GeneratePointsInsideRect(Point start, Point end, Size? step = null, bool inclusiveEnd = false) + { + step ??= new Size(1, 1); + foreach (var i in Range(start.X, end.X, step.Value.Width, inclusiveEnd)) + { + foreach (var j in Range(start.Y, end.Y, step.Value.Height, inclusiveEnd)) + { + yield return new Point(i, j); + } + } + } } } diff --git a/AnnoDesigner/AnnoCanvas.xaml.cs b/AnnoDesigner/AnnoCanvas.xaml.cs index a1cce463..8e815603 100644 --- a/AnnoDesigner/AnnoCanvas.xaml.cs +++ b/AnnoDesigner/AnnoCanvas.xaml.cs @@ -389,7 +389,9 @@ private enum MouseMode DragAll, PlaceObjects, DeleteObject, - SelectSameIdentifier + SelectSameIdentifier, + // used to place objects by drawing rectangle + RectPlaceObjects } /// @@ -932,6 +934,24 @@ protected override void OnRender(DrawingContext drawingContext) objectsChanged = true; } + if (CurrentMode == MouseMode.RectPlaceObjects && CurrentObjects.Count == 1) + { + var objectToClone = CurrentObjects[0]; + var start = ScreenPointToObjectGridPosition(_mouseDragStart, objectToClone); + var end = ScreenPointToObjectGridPosition(_mousePosition, objectToClone); + var clones = Core.Extensions.IEnumerableExtensions.GeneratePointsInsideRect(start, end, objectToClone.Size, true) + .Select(point => + { + var obj = CloneLayoutObject(objectToClone); + obj.Position = point; + return obj; + }) + .Where(obj => !ObjectIntersectionExists(PlacedObjects.GetItemsIntersecting(obj.Bounds), obj)) + .ToList(); + + RenderObjectList(drawingContext, clones, true); + } + // draw placed objects if (_isRenderingForced || objectsChanged) { @@ -1350,16 +1370,18 @@ private void MoveCurrentObjectsToMouse() } else { - var pos = _coordinateHelper.ScreenToFractionalGrid(_mousePosition, GridSize); - var size = CurrentObjects[0].Size; - pos.X -= size.Width / 2; - pos.Y -= size.Height / 2; - pos = _viewport.OriginToViewport(pos); - pos = new Point(Math.Round(pos.X, MidpointRounding.AwayFromZero), Math.Round(pos.Y, MidpointRounding.AwayFromZero)); - CurrentObjects[0].Position = pos; + CurrentObjects[0].Position = ScreenPointToObjectGridPosition(_mousePosition, CurrentObjects[0]); } } + private Point ScreenPointToObjectGridPosition(Point point, LayoutObject obj) + { + point = _coordinateHelper.ScreenToFractionalGrid(point, GridSize); + point -= (Vector)obj.Size / 2; + point = _viewport.OriginToViewport(point); + return new Point(Math.Round(point.X, MidpointRounding.AwayFromZero), Math.Round(point.Y, MidpointRounding.AwayFromZero)); + } + /// /// Renders the given AnnoObject to the given DrawingContext. /// @@ -2095,8 +2117,17 @@ protected override void OnMouseDown(MouseButtonEventArgs e) } else if (e.LeftButton == MouseButtonState.Pressed && CurrentObjects.Count != 0) { - // place new object - TryPlaceCurrentObjects(isContinuousDrawing: false); + if (IsShiftPressed() && CurrentObjects.Count == 1) + { + // begin rectangle placing objects + CurrentMode = MouseMode.RectPlaceObjects; + } + else + { + // place single new object + TryPlaceCurrentObjects(isContinuousDrawing: false); + CurrentMode = MouseMode.PlaceObjects; + } } else if (e.LeftButton == MouseButtonState.Pressed && CurrentObjects.Count == 0) { @@ -2149,6 +2180,9 @@ protected override void OnMouseMove(MouseEventArgs e) break; case MouseMode.DragAllStart: CurrentMode = MouseMode.DragAll; + break; + case MouseMode.RectPlaceObjects: + break; } } @@ -2180,120 +2214,117 @@ protected override void OnMouseMove(MouseEventArgs e) } else if (e.LeftButton == MouseButtonState.Pressed) { - if (CurrentObjects.Count != 0) - { - CurrentMode = MouseMode.PlaceObjects; - // place new object - TryPlaceCurrentObjects(isContinuousDrawing: true); - } - else + switch (CurrentMode) { - // selection of multiple objects - switch (CurrentMode) - { - case MouseMode.SelectionRect: + case MouseMode.PlaceObjects: + if (CurrentObjects.Count != 0) + { + // place new object + TryPlaceCurrentObjects(isContinuousDrawing: true); + } + break; + case MouseMode.SelectionRect: + { + if (IsControlPressed() || IsShiftPressed()) { - if (IsControlPressed() || IsShiftPressed()) + // remove previously selected by the selection rect + if (ShouldAffectObjectsWithIdentifier()) { - // remove previously selected by the selection rect - if (ShouldAffectObjectsWithIdentifier()) - { - RemoveSelectedObjects( - SelectedObjects.Where(_ => _.CalculateScreenRect(GridSize).IntersectsWith(_selectionRect)).ToList(), - true - ); - } - else - { - RemoveSelectedObjects(x => x.CalculateScreenRect(GridSize).IntersectsWith(_selectionRect)); - } + RemoveSelectedObjects( + SelectedObjects.Where(_ => _.CalculateScreenRect(GridSize).IntersectsWith(_selectionRect)).ToList(), + true + ); } else { - SelectedObjects.Clear(); + RemoveSelectedObjects(x => x.CalculateScreenRect(GridSize).IntersectsWith(_selectionRect)); } + } + else + { + SelectedObjects.Clear(); + } - // adjust rect - _selectionRect = new Rect(_mouseDragStart, _mousePosition); - // select intersecting objects - var selectionRectGrid = _coordinateHelper.ScreenToGrid(_selectionRect, GridSize); - selectionRectGrid = _viewport.OriginToViewport(selectionRectGrid); - AddSelectedObjects(PlacedObjects.GetItemsIntersecting(selectionRectGrid), - ShouldAffectObjectsWithIdentifier()); - RecalculateSelectionContainsNotIgnoredObject(); + // adjust rect + _selectionRect = new Rect(_mouseDragStart, _mousePosition); + // select intersecting objects + var selectionRectGrid = _coordinateHelper.ScreenToGrid(_selectionRect, GridSize); + selectionRectGrid = _viewport.OriginToViewport(selectionRectGrid); + AddSelectedObjects(PlacedObjects.GetItemsIntersecting(selectionRectGrid), + ShouldAffectObjectsWithIdentifier()); + RecalculateSelectionContainsNotIgnoredObject(); + + StatisticsUpdated?.Invoke(this, UpdateStatisticsEventArgs.All); + break; + } + case MouseMode.DragSelection: + { + if (_oldObjectPositions.Count == 0) + { + _oldObjectPositions.AddRange(SelectedObjects.Select(obj => (obj, obj.GridRect))); + } - StatisticsUpdated?.Invoke(this, UpdateStatisticsEventArgs.All); + // move all selected objects + var dx = (int)_coordinateHelper.ScreenToGrid(_mousePosition.X - _mouseDragStart.X, GridSize); + var dy = (int)_coordinateHelper.ScreenToGrid(_mousePosition.Y - _mouseDragStart.Y, GridSize); + // check if the mouse has moved at least one grid cell in any direction + if (dx == 0 && dy == 0) + { + //no relevant mouse move -> no further action break; } - case MouseMode.DragSelection: + //Recompute _unselectedObjects + var offsetCollisionRect = _collisionRect; + offsetCollisionRect.Offset(dx, dy); + + //Its causing slowdowns when dragging large numbers of objects + _unselectedObjects = PlacedObjects.GetItemsIntersecting(offsetCollisionRect).Where(_ => !SelectedObjects.Contains(_)).ToList(); + var collisionsExist = false; + // temporarily move each object and check if collisions with unselected objects exist + foreach (var curLayoutObject in SelectedObjects) { - if (_oldObjectPositions.Count == 0) + var originalPosition = curLayoutObject.Position; + // move object + curLayoutObject.Position = new Point(curLayoutObject.Position.X + dx, curLayoutObject.Position.Y + dy); + // check for collisions + var collides = _unselectedObjects.Find(_ => ObjectIntersectionExists(curLayoutObject, _)) != null; + curLayoutObject.Position = originalPosition; + if (collides) { - _oldObjectPositions.AddRange(SelectedObjects.Select(obj => (obj, obj.GridRect))); - } - - // move all selected objects - var dx = (int)_coordinateHelper.ScreenToGrid(_mousePosition.X - _mouseDragStart.X, GridSize); - var dy = (int)_coordinateHelper.ScreenToGrid(_mousePosition.Y - _mouseDragStart.Y, GridSize); - // check if the mouse has moved at least one grid cell in any direction - if (dx == 0 && dy == 0) - { - //no relevant mouse move -> no further action + collisionsExist = true; break; } - //Recompute _unselectedObjects - var offsetCollisionRect = _collisionRect; - offsetCollisionRect.Offset(dx, dy); - - //Its causing slowdowns when dragging large numbers of objects - _unselectedObjects = PlacedObjects.GetItemsIntersecting(offsetCollisionRect).Where(_ => !SelectedObjects.Contains(_)).ToList(); - var collisionsExist = false; - // temporarily move each object and check if collisions with unselected objects exist + } + + // if no collisions were found, permanently move all selected objects + if (!collisionsExist) + { foreach (var curLayoutObject in SelectedObjects) { - var originalPosition = curLayoutObject.Position; - // move object curLayoutObject.Position = new Point(curLayoutObject.Position.X + dx, curLayoutObject.Position.Y + dy); - // check for collisions - var collides = _unselectedObjects.Find(_ => ObjectIntersectionExists(curLayoutObject, _)) != null; - curLayoutObject.Position = originalPosition; - if (collides) - { - collisionsExist = true; - break; - } } - - // if no collisions were found, permanently move all selected objects - if (!collisionsExist) + // adjust the drag start to compensate the amount we already moved + _mouseDragStart.X += _coordinateHelper.GridToScreen(dx, GridSize); + _mouseDragStart.Y += _coordinateHelper.GridToScreen(dy, GridSize); + + //update collision rect, so that collisions are correctly computed on next run + _collisionRect.X += dx; + _collisionRect.Y += dy; + + //position change -> update + StatisticsUpdated?.Invoke(this, new UpdateStatisticsEventArgs(UpdateMode.NoBuildingList)); + //always recompute bounds when moving, as we may be moving an item in from the edge of the layout + var oldLayoutBounds = _layoutBounds; + InvalidateBounds(); + if (oldLayoutBounds != _layoutBounds) { - foreach (var curLayoutObject in SelectedObjects) - { - curLayoutObject.Position = new Point(curLayoutObject.Position.X + dx, curLayoutObject.Position.Y + dy); - } - // adjust the drag start to compensate the amount we already moved - _mouseDragStart.X += _coordinateHelper.GridToScreen(dx, GridSize); - _mouseDragStart.Y += _coordinateHelper.GridToScreen(dy, GridSize); - - //update collision rect, so that collisions are correctly computed on next run - _collisionRect.X += dx; - _collisionRect.Y += dy; - - //position change -> update - StatisticsUpdated?.Invoke(this, new UpdateStatisticsEventArgs(UpdateMode.NoBuildingList)); - //always recompute bounds when moving, as we may be moving an item in from the edge of the layout - var oldLayoutBounds = _layoutBounds; - InvalidateBounds(); - if (oldLayoutBounds != _layoutBounds) - { - InvalidateScroll(); - } + InvalidateScroll(); } - - ForceRendering(); - return; } - } + + ForceRendering(); + return; + } } } @@ -2387,7 +2418,33 @@ protected override void OnMouseUp(MouseButtonEventArgs e) } else if (e.ChangedButton == MouseButton.Left && CurrentObjects.Count != 0) { - CurrentMode = MouseMode.PlaceObjects; + switch (CurrentMode) + { + case MouseMode.RectPlaceObjects: + var objectToClone = CurrentObjects[0]; + var start = ScreenPointToObjectGridPosition(_mouseDragStart, objectToClone); + var end = ScreenPointToObjectGridPosition(_mousePosition, objectToClone); + var clones = Core.Extensions.IEnumerableExtensions.GeneratePointsInsideRect(start, end, objectToClone.Size, true) + .Select(point => + { + var obj = CloneLayoutObject(objectToClone); + obj.Position = point; + return obj; + }) + .Where(obj => !ObjectIntersectionExists(PlacedObjects.GetItemsIntersecting(obj.Bounds), obj)) + .ToList(); + + UndoManager.RegisterOperation(new AddObjectsOperation() + { + Objects = clones, + Collection = PlacedObjects + }); + PlacedObjects.AddRange(clones); + StatisticsUpdated?.Invoke(this, UpdateStatisticsEventArgs.All); + ForceRendering(); + break; + } + CurrentMode = MouseMode.Standard; } else if (e.ChangedButton == MouseButton.Right) { @@ -2434,17 +2491,6 @@ protected override void OnMouseUp(MouseButtonEventArgs e) } } } - else if (e.ChangedButton == MouseButton.Right) - { - switch (CurrentMode) - { - case MouseMode.SelectSameIdentifier: - { - CurrentMode = MouseMode.Standard; - break; - } - } - } else if (e.ChangedButton == MouseButton.XButton1) { switch (CurrentMode) @@ -3106,14 +3152,19 @@ private void ExecuteEnableDebugMode(object param) #region Helper methods + private LayoutObject CloneLayoutObject(LayoutObject obj) + { + return new LayoutObject(new AnnoObject(obj.WrappedAnnoObject), _coordinateHelper, _brushCache, _penCache); + } + private List CloneLayoutObjects(ICollection list) { - return list.Select(x => new LayoutObject(new AnnoObject(x.WrappedAnnoObject), _coordinateHelper, _brushCache, _penCache)).ToListWithCapacity(list.Count); + return list.Select(CloneLayoutObject).ToListWithCapacity(list.Count); } private List CloneLayoutObjects(IEnumerable list, int capacity) { - return list.Select(x => new LayoutObject(new AnnoObject(x.WrappedAnnoObject), _coordinateHelper, _brushCache, _penCache)).ToListWithCapacity(capacity); + return list.Select(CloneLayoutObject).ToListWithCapacity(capacity); } private void UpdateScrollBarVisibility() diff --git a/Tests/AnnoDesigner.Core.Tests/IEnumerableExtensionsTests.cs b/Tests/AnnoDesigner.Core.Tests/IEnumerableExtensionsTests.cs index fa593603..241a25ae 100644 --- a/Tests/AnnoDesigner.Core.Tests/IEnumerableExtensionsTests.cs +++ b/Tests/AnnoDesigner.Core.Tests/IEnumerableExtensionsTests.cs @@ -86,5 +86,59 @@ public void WithoutIgnoredObjects_ListHasIgnorableObjects_ShouldReturnFilteredLi Assert.NotEqual("Blocker", x.Template, StringComparer.OrdinalIgnoreCase); }); } + + [Fact] + public void Range_Increasing_ShouldReturnIncreasingSequence() + { + // Arrange/Act + var result = IEnumerableExtensions.Range(1, 5).ToList(); + + // Assert + Assert.Equal(new double[] { 1, 2, 3, 4 }, result); + } + + [Fact] + public void Range_Decreasing_ShouldReturnDecreasingSequence() + { + // Arrange/Act + var result = IEnumerableExtensions.Range(5, 1).ToList(); + + // Assert + Assert.Equal(new double[] { 5, 4, 3, 2 }, result); + } + + [Fact] + public void Range_NonDefaultStep_ShouldSkipSomeValues() + { + // Arrange/Act + var result = IEnumerableExtensions.Range(1, 5, 2).ToList(); + + // Assert + Assert.Equal(new double[] { 1, 3 }, result); + } + + [Fact] + public void Range_ZeroStep_ShouldThrowException() + { + // Arrange/Act/Assert + Assert.Throws(() => IEnumerableExtensions.Range(1, 5, 0).ToList()); + } + + [Fact] + public void Range_NegativeStep_ShouldThrowException() + { + // Arrange/Act/Assert + Assert.Throws(() => IEnumerableExtensions.Range(5, 1, -1).ToList()); + } + + [Fact] + public void Range_InclusiveTo_ShouldReturnOneValueAfterEnd() + { + // Arrange/Act + var result = IEnumerableExtensions.Range(1, 5, inclusiveTo: true).ToList(); + + // Assert + Assert.Equal(new double[] { 1, 2, 3, 4, 5 }, result); + } } }