Add xml docs

This commit is contained in:
Stone_Red
2026-05-05 23:37:43 +02:00
parent 83bb3dc2f3
commit ced5509983
6 changed files with 169 additions and 12 deletions
+18
View File
@@ -5,15 +5,33 @@ using System.Collections.Generic;
namespace LargeGridPathfinding; namespace LargeGridPathfinding;
/// <summary>
/// Represents a moving entity that follows a precomputed grid path.
/// </summary>
internal class Agent(Vector2 position) internal class Agent(Vector2 position)
{ {
/// <summary>
/// Gets or sets the current world-grid position as floating point coordinates.
/// </summary>
public Vector2 Position { get; set; } = position; public Vector2 Position { get; set; } = position;
/// <summary>
/// Gets the integer grid cell for the current position.
/// </summary>
public Point GridPosition => new Point((int)Math.Round(Position.X), (int)Math.Round(Position.Y)); public Point GridPosition => new Point((int)Math.Round(Position.X), (int)Math.Round(Position.Y));
/// <summary>
/// Gets or sets the next waypoint currently being approached.
/// </summary>
public Vector2 NextPosition { get; set; } public Vector2 NextPosition { get; set; }
/// <summary>
/// Gets or sets the current target destination.
/// </summary>
public Point? Destination { get; set; } public Point? Destination { get; set; }
/// <summary>
/// Gets or sets the active path.
/// </summary>
public List<Vector2>? Path { get; set; } public List<Vector2>? Path { get; set; }
} }
+53 -3
View File
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding; namespace LargeGridPathfinding;
/// <summary>
/// Maintains grid labels/weights and generates rectangular walkable zones.
/// </summary>
public class GridFiller public class GridFiller
{ {
private const int RecalculationRadius = 24; private const int RecalculationRadius = 24;
@@ -21,6 +24,9 @@ public class GridFiller
public int Width { get; } public int Width { get; }
public int Height { get; } public int Height { get; }
/// <summary>
/// Initializes an empty grid with default tile weights.
/// </summary>
public GridFiller(int width, int height) public GridFiller(int width, int height)
{ {
Width = width; Width = width;
@@ -38,6 +44,9 @@ public class GridFiller
} }
} }
/// <summary>
/// Fills the selected region with maximal same-weight rectangles.
/// </summary>
public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress<float>? totalProgress = null, IProgress<float>? calculatingCandidatesProgress = null, IProgress<float>? placingCandidatesProgress = null) public void FillGrid(int? x1 = null, int? y1 = null, int? x2 = null, int? y2 = null, bool fillAll = false, IProgress<float>? totalProgress = null, IProgress<float>? calculatingCandidatesProgress = null, IProgress<float>? placingCandidatesProgress = null)
{ {
lock (mutationLock) lock (mutationLock)
@@ -185,16 +194,25 @@ public class GridFiller
} }
} }
/// <summary>
/// Places a single obstacle rectangle.
/// </summary>
public void PlaceObstacle(Rectangle rectangle) public void PlaceObstacle(Rectangle rectangle)
{ {
PlaceObstacles([rectangle]); PlaceObstacles([rectangle]);
} }
/// <summary>
/// Places obstacle rectangles and optionally recalculates affected zones.
/// </summary>
public void PlaceObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true) public void PlaceObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{ {
_ = PlaceObstaclesWithAffected(rectangles, recalculate); _ = PlaceObstaclesWithAffected(rectangles, recalculate);
} }
/// <summary>
/// Places obstacles and returns changed zone labels.
/// </summary>
public HashSet<int> PlaceObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true) public HashSet<int> PlaceObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{ {
lock (mutationLock) lock (mutationLock)
@@ -266,7 +284,7 @@ public class GridFiller
Debug.WriteLine("Placed obstacle"); Debug.WriteLine("Placed obstacle");
// Return zones that changed (added, removed, or had their rectangle modified) // Symmetric diff captures both split and merge operations from recalculation.
HashSet<int> affected = [.. zonesBefore]; HashSet<int> affected = [.. zonesBefore];
affected.SymmetricExceptWith(zonesAfter); // zones removed OR added affected.SymmetricExceptWith(zonesAfter); // zones removed OR added
@@ -286,16 +304,25 @@ public class GridFiller
} }
} }
/// <summary>
/// Removes a single obstacle rectangle.
/// </summary>
public void RemoveObstacle(Rectangle rectangle) public void RemoveObstacle(Rectangle rectangle)
{ {
RemoveObstacles([rectangle]); RemoveObstacles([rectangle]);
} }
/// <summary>
/// Removes obstacle rectangles and optionally recalculates affected zones.
/// </summary>
public void RemoveObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true) public void RemoveObstacles(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{ {
_ = RemoveObstaclesWithAffected(rectangles, recalculate); _ = RemoveObstaclesWithAffected(rectangles, recalculate);
} }
/// <summary>
/// Removes obstacles and returns changed zone labels.
/// </summary>
public HashSet<int> RemoveObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true) public HashSet<int> RemoveObstaclesWithAffected(IEnumerable<Rectangle> rectangles, bool recalculate = true)
{ {
lock (mutationLock) lock (mutationLock)
@@ -384,21 +411,33 @@ public class GridFiller
} }
} }
/// <summary>
/// Sets a single tile weight.
/// </summary>
public void SetTileWeight(int x, int y, int weight) public void SetTileWeight(int x, int y, int weight)
{ {
SetTileWeights([new Point(x, y)], weight); SetTileWeights([new Point(x, y)], weight);
} }
/// <summary>
/// Resets a single tile weight to default.
/// </summary>
public void ResetTileWeight(int x, int y) public void ResetTileWeight(int x, int y)
{ {
ResetTileWeights([new Point(x, y)]); ResetTileWeights([new Point(x, y)]);
} }
/// <summary>
/// Sets weights for tiles and optionally recalculates affected zones.
/// </summary>
public void SetTileWeights(IEnumerable<Point> points, int weight, bool recalculate = true) public void SetTileWeights(IEnumerable<Point> points, int weight, bool recalculate = true)
{ {
_ = SetTileWeightsWithAffected(points, weight, recalculate); _ = SetTileWeightsWithAffected(points, weight, recalculate);
} }
/// <summary>
/// Sets weights and returns changed zone labels.
/// </summary>
public HashSet<int> SetTileWeightsWithAffected(IEnumerable<Point> points, int weight, bool recalculate = true) public HashSet<int> SetTileWeightsWithAffected(IEnumerable<Point> points, int weight, bool recalculate = true)
{ {
lock (mutationLock) lock (mutationLock)
@@ -465,9 +504,17 @@ public class GridFiller
} }
} }
public void ResetTileWeights(IEnumerable<Point> points) public HashSet<int> ResetTileWeightsWithAffected(IEnumerable<Point> points, bool recalculate = true)
{ {
SetTileWeights(points, 1); return SetTileWeightsWithAffected(points, 1, recalculate);
}
/// <summary>
/// Resets weights for multiple tiles to default.
/// </summary>
public void ResetTileWeights(IEnumerable<Point> points, bool recalculate = true)
{
SetTileWeights(points, 1, recalculate);
} }
private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight) private bool IsAreaFree(int x, int y, int w, int h, int requiredWeight)
@@ -648,6 +695,7 @@ public class GridFiller
if (removedRectangles.Count == 0) if (removedRectangles.Count == 0)
{ {
applyChanges(); applyChanges();
// Even if no existing zone was removed, edits may have opened space that needs fresh zoning.
FillGrid(left, top, right, bottom); FillGrid(left, top, right, bottom);
return; return;
} }
@@ -657,6 +705,8 @@ public class GridFiller
int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right)); int fillRight = Math.Max(right, removedRectangles.Max(r => r.Right));
int fillBottom = Math.Max(bottom, removedRectangles.Max(r => r.Bottom)); int fillBottom = Math.Max(bottom, removedRectangles.Max(r => r.Bottom));
// Expanding fill bounds to include removed rectangle extents avoids edge artifacts
// where old zone boundaries would otherwise leave suboptimal splits near the edit area.
applyChanges(); applyChanges();
FillGrid(fillLeft, fillTop, fillRight, fillBottom); FillGrid(fillLeft, fillTop, fillRight, fillBottom);
} }
+6
View File
@@ -5,8 +5,14 @@ using MonoGame.Extended.Input;
namespace LargeGridPathfinding; namespace LargeGridPathfinding;
/// <summary>
/// Input extension helpers for game controls.
/// </summary>
internal static class InputHelper internal static class InputHelper
{ {
/// <summary>
/// Converts WASD keyboard input into a movement vector.
/// </summary>
public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState) public static Vector2 GetMovementDirection(this KeyboardStateExtended keyboardState)
{ {
Vector2 movementDirection = Vector2.Zero; Vector2 movementDirection = Vector2.Zero;
@@ -15,6 +15,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding; namespace LargeGridPathfinding;
/// <summary>
/// Main game host that handles UI, map editing, rendering, and background path updates.
/// </summary>
public class LargeGridPathfindingGame : Game public class LargeGridPathfindingGame : Game
{ {
private enum BrushMode private enum BrushMode
@@ -42,10 +45,24 @@ public class LargeGridPathfindingGame : Game
PenalizeStretchedRectangles PenalizeStretchedRectangles
} }
/// <summary>
/// Supported map presets for startup generation.
/// </summary>
private enum StartupMapPreset private enum StartupMapPreset
{ {
/// <summary>
/// Procedural room-like map with random walls and doors.
/// </summary>
Rooms, Rooms,
/// <summary>
/// No generated obstacles.
/// </summary>
Empty, Empty,
/// <summary>
/// Dense 3x3 room lattice with deterministic offset doors for heavy zone fragmentation.
/// </summary>
WorstCase WorstCase
} }
@@ -78,7 +95,6 @@ public class LargeGridPathfindingGame : Game
private Point? fillSelectionCurrent; private Point? fillSelectionCurrent;
private bool batchProcessingScheduled; private bool batchProcessingScheduled;
private bool startupMenuActive = true; private bool startupMenuActive = true;
private readonly bool startupInitializationStarted;
private StartupMenuItem startupSelectedItem; private StartupMenuItem startupSelectedItem;
private int startupGridWidth = 1000; private int startupGridWidth = 1000;
private int startupGridHeight = 1000; private int startupGridHeight = 1000;
@@ -216,21 +232,27 @@ public class LargeGridPathfindingGame : Game
case StartupMenuItem.GridWidth: case StartupMenuItem.GridWidth:
startupGridWidth = Math.Clamp(startupGridWidth + (direction * 100), 500, 30000); startupGridWidth = Math.Clamp(startupGridWidth + (direction * 100), 500, 30000);
break; break;
case StartupMenuItem.GridHeight: case StartupMenuItem.GridHeight:
startupGridHeight = Math.Clamp(startupGridHeight + (direction * 100), 500, 30000); startupGridHeight = Math.Clamp(startupGridHeight + (direction * 100), 500, 30000);
break; break;
case StartupMenuItem.AgentCount: case StartupMenuItem.AgentCount:
startupAgentCount = Math.Clamp(startupAgentCount + (direction * 100), 100, 200000); startupAgentCount = Math.Clamp(startupAgentCount + (direction * 100), 100, 200000);
break; break;
case StartupMenuItem.MapPreset: case StartupMenuItem.MapPreset:
startupMapPreset = (StartupMapPreset)Math.Clamp((int)startupMapPreset + direction, (int)StartupMapPreset.Rooms, (int)StartupMapPreset.WorstCase); startupMapPreset = (StartupMapPreset)Math.Clamp((int)startupMapPreset + direction, (int)StartupMapPreset.Rooms, (int)StartupMapPreset.WorstCase);
break; break;
case StartupMenuItem.ObstacleDivisor: case StartupMenuItem.ObstacleDivisor:
startupObstacleDivisor = Math.Clamp(startupObstacleDivisor + direction, 1, 50); startupObstacleDivisor = Math.Clamp(startupObstacleDivisor + direction, 1, 50);
break; break;
case StartupMenuItem.PathRandomization: case StartupMenuItem.PathRandomization:
startupPathRandomization = direction > 0 || (direction >= 0 && startupPathRandomization); startupPathRandomization = direction > 0 || (direction >= 0 && startupPathRandomization);
break; break;
case StartupMenuItem.PenalizeStretchedRectangles: case StartupMenuItem.PenalizeStretchedRectangles:
startupPenalizeStretchedRectangles = direction > 0 || (direction >= 0 && startupPenalizeStretchedRectangles); startupPenalizeStretchedRectangles = direction > 0 || (direction >= 0 && startupPenalizeStretchedRectangles);
break; break;
@@ -515,7 +537,7 @@ public class LargeGridPathfindingGame : Game
{ {
uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp); uiSpriteBatch.Begin(SpriteSortMode.Deferred, samplerState: SamplerState.PointClamp);
if (startupMenuActive && !startupInitializationStarted) if (startupMenuActive)
{ {
uiSpriteBatch.DrawString(uiFont, "Startup Configuration", new Vector2(40, 30), Color.Black); uiSpriteBatch.DrawString(uiFont, "Startup Configuration", new Vector2(40, 30), Color.Black);
uiSpriteBatch.DrawString(uiFont, "Arrow Up/Down: Select Arrow Left/Right: Change Enter: Start", new Vector2(40, 60), Color.Black); uiSpriteBatch.DrawString(uiFont, "Arrow Up/Down: Select Arrow Left/Right: Change Enter: Start", new Vector2(40, 60), Color.Black);
@@ -748,6 +770,9 @@ public class LargeGridPathfindingGame : Game
base.Draw(gameTime); base.Draw(gameTime);
} }
/// <summary>
/// Builds the initial grid and starts the path update worker using the startup configuration.
/// </summary>
private void StartInitializationTask(int width, int height, int agentCount, StartupMapPreset mapPreset, int obstacleDivisor, bool pathRandomization, bool penalizeStretchedRectangles) private void StartInitializationTask(int width, int height, int agentCount, StartupMapPreset mapPreset, int obstacleDivisor, bool pathRandomization, bool penalizeStretchedRectangles)
{ {
_ = Task.Run(() => _ = Task.Run(() =>
@@ -769,7 +794,10 @@ public class LargeGridPathfindingGame : Game
int obstacleIterations = Math.Max(0, (width + height) / Math.Max(1, obstacleDivisor)); int obstacleIterations = Math.Max(0, (width + height) / Math.Max(1, obstacleDivisor));
int minObstacleSize = 5; int minObstacleSize = 5;
int minDimension = Math.Min(width, height); int minDimension = Math.Min(width, height);
// Using a sub-linear exponent keeps small maps close to previous behavior,
// but prevents obstacle size from exploding on huge maps (e.g. 10k x 10k).
int scaledByDimension = (int)Math.Round(Math.Pow(minDimension, 0.4) * 2.1); int scaledByDimension = (int)Math.Round(Math.Pow(minDimension, 0.4) * 2.1);
// This upper cap prevents very long walls on thin maps where one dimension is small.
int maxByDimension = Math.Max(minObstacleSize + 1, minDimension / 4); int maxByDimension = Math.Max(minObstacleSize + 1, minDimension / 4);
int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension)); int maxObstacleSize = Math.Max(minObstacleSize + 1, Math.Min(scaledByDimension, maxByDimension));
@@ -832,6 +860,9 @@ public class LargeGridPathfindingGame : Game
int local = x % pitch; int local = x % pitch;
int baseX = x - local; int baseX = x - local;
bool oddSegment = (baseX / pitch % 2) == 1; bool oddSegment = (baseX / pitch % 2) == 1;
// We intentionally alternate door offsets between left and right side of each room segment.
// If we always place centered doors, many transitions become symmetric and produce fewer
// distinct rectangular splits. Offsetting increases boundary complexity while staying connected.
int offset = ((rowIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // left/right offset int offset = ((rowIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // left/right offset
bool isDoor = local == offset; bool isDoor = local == offset;
@@ -862,6 +893,8 @@ public class LargeGridPathfindingGame : Game
int local = y % pitch; int local = y % pitch;
int baseY = y - local; int baseY = y - local;
bool oddSegment = (baseY / pitch % 2) == 1; bool oddSegment = (baseY / pitch % 2) == 1;
// Same idea vertically: alternating top/bottom door offsets avoids repetitive straight corridors
// and keeps the whole lattice navigable with a high number of zone boundaries.
int offset = ((colIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // top/bottom offset int offset = ((colIndex % 2 == 0) ^ oddSegment) ? 1 : 3; // top/bottom offset
bool isDoor = local == offset; bool isDoor = local == offset;
@@ -912,7 +945,9 @@ public class LargeGridPathfindingGame : Game
}); });
} }
// Handles pathfinding calculations in a separate task /// <summary>
/// Handles graph/path refresh work on a dedicated long-running task.
/// </summary>
private void StartUpdatePathTask() private void StartUpdatePathTask()
{ {
_ = Task.Factory.StartNew(async () => _ = Task.Factory.StartNew(async () =>
@@ -1100,7 +1135,7 @@ public class LargeGridPathfindingGame : Game
if (resetWeightPoints.Length > 0) if (resetWeightPoints.Length > 0)
{ {
affectedZones.UnionWith(gridFiller.SetTileWeightsWithAffected(resetWeightPoints, 1)); affectedZones.UnionWith(gridFiller.ResetTileWeightsWithAffected(resetWeightPoints));
} }
if (placeObstacleRectangles.Length > 0) if (placeObstacleRectangles.Length > 0)
+26 -3
View File
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
namespace LargeGridPathfinding; namespace LargeGridPathfinding;
/// <summary>
/// Builds a zone adjacency graph and computes paths across rectangular zones.
/// </summary>
public class Pathfinder public class Pathfinder
{ {
private readonly ConcurrentDictionary<int, Rectangle> rectanglesSource; private readonly ConcurrentDictionary<int, Rectangle> rectanglesSource;
@@ -19,11 +22,17 @@ public class Pathfinder
private Dictionary<int, Rectangle> rectangleMap; private Dictionary<int, Rectangle> rectangleMap;
private Dictionary<int, List<int>> adjacencyList; private Dictionary<int, List<int>> adjacencyList;
/// <summary>
/// Returns the current zone adjacency list.
/// </summary>
public Dictionary<int, List<int>> GetAdjacencyList() public Dictionary<int, List<int>> GetAdjacencyList()
{ {
return adjacencyList; return adjacencyList;
} }
/// <summary>
/// Returns the zone label at a grid point, or 0 when out of bounds.
/// </summary>
public int GetGridValue(Point gridPoint) public int GetGridValue(Point gridPoint)
{ {
if (gridPoint.X < 0 || gridPoint.Y < 0 || gridPoint.X >= grid.GetLength(1) || gridPoint.Y >= grid.GetLength(0)) if (gridPoint.X < 0 || gridPoint.Y < 0 || gridPoint.X >= grid.GetLength(1) || gridPoint.Y >= grid.GetLength(0))
@@ -34,6 +43,9 @@ public class Pathfinder
return grid[gridPoint.Y, gridPoint.X]; return grid[gridPoint.Y, gridPoint.X];
} }
/// <summary>
/// Initializes a pathfinder over a mutable grid/rectangle source.
/// </summary>
public Pathfinder(ConcurrentDictionary<int, Rectangle> rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false) public Pathfinder(ConcurrentDictionary<int, Rectangle> rectangles, int[,] grid, int[,] weightGrid, bool pathRandomization = false, bool penalizeStretchedRectangles = false)
{ {
rectanglesSource = rectangles; rectanglesSource = rectangles;
@@ -45,6 +57,9 @@ public class Pathfinder
adjacencyList = []; adjacencyList = [];
} }
/// <summary>
/// Finds a path between two grid points as coordinate waypoints.
/// </summary>
public List<Vector2>? FindPath(Point startPoint, Point goalPoint) public List<Vector2>? FindPath(Point startPoint, Point goalPoint)
{ {
if (!IsInBounds(startPoint.X, startPoint.Y) || !IsInBounds(goalPoint.X, goalPoint.Y)) if (!IsInBounds(startPoint.X, startPoint.Y) || !IsInBounds(goalPoint.X, goalPoint.Y))
@@ -115,6 +130,9 @@ public class Pathfinder
return null; return null;
} }
/// <summary>
/// Rebuilds the full adjacency graph from current rectangles.
/// </summary>
public void BuildGraph() public void BuildGraph()
{ {
rectangleMap = new Dictionary<int, Rectangle>(rectanglesSource); rectangleMap = new Dictionary<int, Rectangle>(rectanglesSource);
@@ -131,6 +149,8 @@ public class Pathfinder
{ {
if (!rectangleMap.TryGetValue(rectangleLabel, out Rectangle rectangle)) if (!rectangleMap.TryGetValue(rectangleLabel, out Rectangle rectangle))
{ {
// Returning a very large cost keeps stale nodes from being attractive
// if they slip through due to transient graph inconsistencies.
return int.MaxValue / 4; return int.MaxValue / 4;
} }
@@ -247,7 +267,7 @@ public class Pathfinder
graph[id] = []; graph[id] = [];
} }
// For small graphs, use sequential algorithm (less overhead) // For small graphs, sequential processing is usually faster than parallel setup/synchronization.
if (count < 50) if (count < 50)
{ {
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
@@ -273,7 +293,7 @@ public class Pathfinder
} }
else else
{ {
// For larger graphs, use parallelization with partitioning // For larger graphs, parallel pair-checking pays off despite synchronization overhead.
object[] locks = new object[count]; object[] locks = new object[count];
for (int i = 0; i < count; i++) for (int i = 0; i < count; i++)
{ {
@@ -296,7 +316,7 @@ public class Pathfinder
{ {
if (AreRectanglesAdjacent(rect1, rect2)) if (AreRectanglesAdjacent(rect1, rect2))
{ {
// Use per-rectangle locks to minimize contention // Per-index locks reduce contention compared to one global graph lock.
lock (locks[i]) lock (locks[i])
{ {
graph[id1].Add(id2); graph[id1].Add(id2);
@@ -314,6 +334,9 @@ public class Pathfinder
return graph; return graph;
} }
/// <summary>
/// Incrementally refreshes affected parts of the graph after local grid edits.
/// </summary>
public void IncrementalUpdateGraph(HashSet<int> affectedZones) public void IncrementalUpdateGraph(HashSet<int> affectedZones)
{ {
if (affectedZones.Count == 0) if (affectedZones.Count == 0)
@@ -3,20 +3,32 @@ using System.Collections.Generic;
namespace LargeGridPathfinding; namespace LargeGridPathfinding;
/// <summary>
/// Tracks named progress entries for long-running operations.
/// </summary>
internal class ProgressTracker internal class ProgressTracker
{ {
private readonly List<ProgressData> progresses = []; private readonly List<ProgressData> progresses = [];
/// <summary>
/// Adds a determinate progress entry.
/// </summary>
public ProgressData AddProgress(string name, out IProgress<float> progress) public ProgressData AddProgress(string name, out IProgress<float> progress)
{ {
return AddProgress(name, false, false, out progress); return AddProgress(name, false, false, out progress);
} }
/// <summary>
/// Adds an optionally indeterminate progress entry.
/// </summary>
public ProgressData AddProgress(string name, bool indeterminate, out IProgress<float> progress) public ProgressData AddProgress(string name, bool indeterminate, out IProgress<float> progress)
{ {
return AddProgress(name, indeterminate, false, out progress); return AddProgress(name, indeterminate, false, out progress);
} }
/// <summary>
/// Adds a progress entry with full control over removal behavior.
/// </summary>
public ProgressData AddProgress(string name, bool indeterminate, bool disableAutoRemove, out IProgress<float> progress) public ProgressData AddProgress(string name, bool indeterminate, bool disableAutoRemove, out IProgress<float> progress)
{ {
CustomProgress<float> newProgress = new CustomProgress<float>(); CustomProgress<float> newProgress = new CustomProgress<float>();
@@ -33,6 +45,9 @@ internal class ProgressTracker
return progressData; return progressData;
} }
/// <summary>
/// Removes a previously added progress entry.
/// </summary>
public void RemoveProgress(ProgressData progressData) public void RemoveProgress(ProgressData progressData)
{ {
lock (progresses) lock (progresses)
@@ -41,6 +56,9 @@ internal class ProgressTracker
} }
} }
/// <summary>
/// Returns active progress entries.
/// </summary>
public IReadOnlyList<ProgressData> GetProgresses() public IReadOnlyList<ProgressData> GetProgresses()
{ {
return progresses; return progresses;
@@ -50,12 +68,16 @@ internal class ProgressTracker
{ {
progressData.Progress = progress; progressData.Progress = progress;
// Auto-removal keeps the HUD uncluttered once tasks finish.
if (progress >= 1 && !progressData.DoNotRemove) if (progress >= 1 && !progressData.DoNotRemove)
{ {
RemoveProgress(progressData); RemoveProgress(progressData);
} }
} }
/// <summary>
/// Immutable metadata with mutable progress value for one tracked task.
/// </summary>
public class ProgressData(string name, bool indeterminate, bool doNotRemove) public class ProgressData(string name, bool indeterminate, bool doNotRemove)
{ {
public string Name { get; } = name; public string Name { get; } = name;
@@ -64,6 +86,9 @@ internal class ProgressTracker
public bool DoNotRemove { get; } = doNotRemove; public bool DoNotRemove { get; } = doNotRemove;
} }
/// <summary>
/// Minimal IProgress implementation exposing ProgressChanged events.
/// </summary>
public class CustomProgress<T>() : IProgress<T> public class CustomProgress<T>() : IProgress<T>
{ {
public EventHandler<T>? ProgressChanged { get; set; } public EventHandler<T>? ProgressChanged { get; set; }