From 8b604070ee57ab84dcbcf73c60dbcd7f8c0b50e9 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Tue, 5 May 2026 02:07:13 +0200 Subject: [PATCH] Add Shift+Click area fill for brush tools and allow batch tile operations --- src/LargeGridPathfinding/GridFiller.cs | 20 ++- .../LargeGridPathfindingGame.cs | 133 +++++++++++++++++- 2 files changed, 140 insertions(+), 13 deletions(-) diff --git a/src/LargeGridPathfinding/GridFiller.cs b/src/LargeGridPathfinding/GridFiller.cs index 2ebd955..d244074 100644 --- a/src/LargeGridPathfinding/GridFiller.cs +++ b/src/LargeGridPathfinding/GridFiller.cs @@ -21,7 +21,7 @@ internal class GridFiller public int Width { get; } public int Height { get; } - public GridFiller(int width, int height, List obstacles) + public GridFiller(int width, int height) { Width = width; Height = height; @@ -36,8 +36,6 @@ internal class GridFiller WeightGrid[y, x] = 1; } } - - PlaceObstacles(obstacles); } public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress? totalProgress = null, IProgress? calculatingCandidatesProgress = null, IProgress? placingCandidatesProgress = null) @@ -167,7 +165,7 @@ internal class GridFiller PlaceObstacles([rectangle]); } - public void PlaceObstacles(IEnumerable rectangles) + public void PlaceObstacles(IEnumerable rectangles, bool recalculate = true) { lock (mutationLock) { @@ -207,7 +205,7 @@ internal class GridFiller if (changed) { - RecalculateAroundArea(minX, minY, maxX, maxY, () => + RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () => { foreach (Rectangle rectangle in clampedRectangles) { @@ -237,7 +235,7 @@ internal class GridFiller RemoveObstacles([rectangle]); } - public void RemoveObstacles(IEnumerable rectangles) + public void RemoveObstacles(IEnumerable rectangles, bool recalculate = true) { lock (mutationLock) { @@ -277,7 +275,7 @@ internal class GridFiller if (changed) { - RecalculateAroundArea(minX, minY, maxX, maxY, () => + RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () => { foreach (Rectangle rectangle in clampedRectangles) { @@ -310,7 +308,7 @@ internal class GridFiller ResetTileWeights([new Point(x, y)]); } - public void SetTileWeights(IEnumerable points, int weight) + public void SetTileWeights(IEnumerable points, int weight, bool recalculate = true) { lock (mutationLock) { @@ -337,7 +335,7 @@ internal class GridFiller if (changed) { - RecalculateAroundArea(minX, minY, maxX, maxY, () => + RecalculateAroundArea(minX, minY, maxX, maxY, recalculate, () => { foreach (Point point in points) { @@ -456,14 +454,14 @@ internal class GridFiller return rectangle; } - private void RecalculateAroundArea(int minX, int minY, int maxX, int maxY, Action applyChanges) + private void RecalculateAroundArea(int minX, int minY, int maxX, int maxY, bool enabled, Action applyChanges) { int left = int.Clamp(minX - RecalculationRadius, 0, Width); int top = int.Clamp(minY - RecalculationRadius, 0, Height); int right = int.Clamp(maxX + RecalculationRadius, 0, Width); int bottom = int.Clamp(maxY + RecalculationRadius, 0, Height); - if (left >= right || top >= bottom) + if (left >= right || top >= bottom || !enabled) { applyChanges(); return; diff --git a/src/LargeGridPathfinding/LargeGridPathfindingGame.cs b/src/LargeGridPathfinding/LargeGridPathfindingGame.cs index 6df2d6e..4e9b1dc 100644 --- a/src/LargeGridPathfinding/LargeGridPathfindingGame.cs +++ b/src/LargeGridPathfinding/LargeGridPathfindingGame.cs @@ -52,6 +52,10 @@ public class LargeGridPathfindingGame : Game private BrushMode brushMode = BrushMode.Weight; private int paintWeight = 5; private Vector2? previousMousePosition; + private bool wasShiftLeftDown; + private bool wasShiftRightDown; + private Point? fillSelectionStart; + private Point? fillSelectionCurrent; private bool batchProcessingScheduled; public LargeGridPathfindingGame() @@ -147,6 +151,13 @@ public class LargeGridPathfindingGame : Game MouseStateExtended mouseState = MouseExtended.GetState(); KeyboardStateExtended keyboardState = KeyboardExtended.GetState(); + bool shiftPressed = keyboardState.IsKeyDown(Keys.LeftShift) || keyboardState.IsKeyDown(Keys.RightShift); + bool shiftLeftDown = shiftPressed && mouseState.IsButtonDown(MouseButton.Left); + bool shiftLeftClicked = shiftLeftDown && !wasShiftLeftDown; + bool shiftRightDown = shiftPressed && mouseState.IsButtonDown(MouseButton.Right); + bool shiftRightClicked = shiftRightDown && !wasShiftRightDown; + wasShiftLeftDown = shiftLeftDown; + wasShiftRightDown = shiftRightDown; float movementSpeed = (float)Math.Pow(200, 2 - camera.Zoom); @@ -254,6 +265,73 @@ public class LargeGridPathfindingGame : Game }); } + if (shiftPressed) + { + if (fillSelectionStart.HasValue && TryGetMouseGridPoint(mouseState, out Point hoverGridPoint)) + { + fillSelectionCurrent = hoverGridPoint; + } + else + { + fillSelectionCurrent = null; + } + + if ((shiftLeftClicked || shiftRightClicked) && TryGetMouseGridPoint(mouseState, out Point clickedGridPoint)) + { + if (!fillSelectionStart.HasValue) + { + fillSelectionStart = clickedGridPoint; + fillSelectionCurrent = clickedGridPoint; + } + else + { + List selectionPoints = GetRectanglePoints(fillSelectionStart.Value, clickedGridPoint); + Color indicatorColor = brushMode == BrushMode.Weight ? Color.Orange : Color.Red; + + foreach (Point point in selectionPoints) + { + temporaryIndicators[point.ToVector2()] = indicatorColor; + } + + PendingOperation operation; + if (brushMode == BrushMode.Weight && shiftLeftClicked) + { + operation = new PendingOperation(PendingOperationKind.SetWeight, paintWeight); + } + else if (brushMode == BrushMode.Weight && shiftRightClicked) + { + operation = new PendingOperation(PendingOperationKind.ResetWeight, 0); + } + else if (brushMode == BrushMode.Obstacle && shiftLeftClicked) + { + operation = new PendingOperation(PendingOperationKind.PlaceObstacle, 0); + } + else if (brushMode == BrushMode.Obstacle && shiftRightClicked) + { + operation = new PendingOperation(PendingOperationKind.RemoveObstacle, 0); + } + else + { + throw new InvalidOperationException("Invalid brush mode or mouse button state."); + } + + EnqueuePendingOperations(selectionPoints, operation); + fillSelectionStart = null; + fillSelectionCurrent = null; + } + } + + previousMousePosition = null; + base.Update(gameTime); + return; + } + else + { + fillSelectionStart = null; + fillSelectionCurrent = null; + fillSelectionCurrent = null; + } + // Paint and clear tiles via mouse input if (mouseState.IsButtonDown(MouseButton.Left)) @@ -420,6 +498,20 @@ public class LargeGridPathfindingGame : Game } } } + + if (fillSelectionStart.HasValue) + { + Point selectionEnd = fillSelectionCurrent ?? fillSelectionStart.Value; + int left = Math.Min(fillSelectionStart.Value.X, selectionEnd.X); + int top = Math.Min(fillSelectionStart.Value.Y, selectionEnd.Y); + int right = Math.Max(fillSelectionStart.Value.X, selectionEnd.X); + int bottom = Math.Max(fillSelectionStart.Value.Y, selectionEnd.Y); + + Rectangle selectionRectangle = new Rectangle(left * 10, top * 10, (right - left + 1) * 10, (bottom - top + 1) * 10); + spriteBatch.FillRectangle(selectionRectangle, Color.LightBlue * 0.2f, layerDepth: 0.24f); + spriteBatch.DrawRectangle(selectionRectangle, Color.Blue, 2f, layerDepth: 0.19f); + } + spriteBatch.End(); // Draw paths in a separate batch if needed @@ -478,6 +570,7 @@ public class LargeGridPathfindingGame : Game uiSpriteBatch.DrawString(uiFont, $"Agents: {agents.Count}", new Vector2(10, 70), Color.Black); uiSpriteBatch.DrawString(uiFont, $"Brush mode: {brushMode} [O]", new Vector2(10, 90), Color.Black); uiSpriteBatch.DrawString(uiFont, $"Weight: {paintWeight} [Keys 1-9, Weight mode]", new Vector2(10, 110), Color.Black); + uiSpriteBatch.DrawString(uiFont, "Area fill: hold Shift + left click twice", new Vector2(10, 130), Color.Black); IReadOnlyList progresses = progressTracker.GetProgresses(); @@ -487,7 +580,7 @@ public class LargeGridPathfindingGame : Game string progress = progressData.Indeterminate ? "..." : progressData.Progress.ToString("P0"); - uiSpriteBatch.DrawString(uiFont, $"{progressData.Name}: {progress}", new Vector2(10, 130 + (i * 20)), Color.Black); + uiSpriteBatch.DrawString(uiFont, $"{progressData.Name}: {progress}", new Vector2(10, 150 + (i * 20)), Color.Black); } uiSpriteBatch.End(); @@ -592,7 +685,8 @@ public class LargeGridPathfindingGame : Game Debug.WriteLine("Initializing grid..."); ProgressTracker.ProgressData progressData = progressTracker.AddProgress("Initializing grid", true, out _); - gridFiller = new GridFiller(width, height, obstacles); + gridFiller = new GridFiller(width, height); + gridFiller.PlaceObstacles(obstacles, false); progressTracker.RemoveProgress(progressData); Debug.WriteLine("Filling grid..."); @@ -769,6 +863,41 @@ public class LargeGridPathfindingGame : Game } } + private bool TryGetMouseGridPoint(MouseStateExtended mouseState, out Point gridPoint) + { + Vector2 mousePosition = camera.ScreenToWorld(mouseState.Position.ToVector2()); + Point point = new Point((int)mousePosition.X / 10, (int)mousePosition.Y / 10); + + if (point.X < 0 || point.X >= gridFiller.Width || point.Y < 0 || point.Y >= gridFiller.Height) + { + gridPoint = default; + return false; + } + + gridPoint = point; + return true; + } + + private static List GetRectanglePoints(Point start, Point end) + { + int minX = Math.Min(start.X, end.X); + int minY = Math.Min(start.Y, end.Y); + int maxX = Math.Max(start.X, end.X); + int maxY = Math.Max(start.Y, end.Y); + + List points = new((maxX - minX + 1) * (maxY - minY + 1)); + + for (int y = minY; y <= maxY; y++) + { + for (int x = minX; x <= maxX; x++) + { + points.Add(new Point(x, y)); + } + } + + return points; + } + private static List GetBrushPoints(Vector2 currentGridPosition, Vector2? previousGridPosition) { HashSet points = [];