#nullable enable namespace Terminal.Gui; /// /// Represents a region composed of one or more rectangles, providing methods for geometric set operations such as /// union, /// intersection, exclusion, and complement. This class is designed for use in graphical or terminal-based user /// interfaces /// where regions need to be manipulated to manage screen areas, clipping, or drawing boundaries. /// /// /// /// This class is thread-safe. All operations are synchronized to ensure consistent state when accessed concurrently. /// /// /// The class adopts a philosophy of efficiency and flexibility, balancing performance with /// usability for GUI applications. It maintains a list of objects, representing disjoint /// (non-overlapping) rectangular areas, and supports operations inspired by set theory. These operations allow /// combining regions in various ways, such as merging areas ( or /// ), /// finding common areas (), or removing portions ( /// or /// ). /// /// /// To achieve high performance, the class employs a sweep-line algorithm for merging rectangles, which efficiently /// processes large sets of rectangles in O(n log n) time by scanning along the x-axis and tracking active vertical /// intervals. This approach ensures scalability for typical GUI scenarios with moderate numbers of rectangles. For /// operations like and , an optional minimization /// step ( /// /// ) is used to reduce the number of rectangles to a minimal set, producing the smallest /// possible collection of non-overlapping rectangles that cover the same area. This minimization, while O(n²) in /// worst-case complexity, is optimized for small-to-medium collections and provides a compact representation ideal /// for drawing or logical operations. /// /// /// The class is immutable in its operations (returning new regions or modifying in-place via methods like /// ), supports nullability for robustness, and implements /// to manage /// resources by clearing internal state. Developers can choose between granular (detailed) or minimal (compact) /// outputs for union operations via and , catering /// to diverse use cases such as rendering optimization, event handling, or visualization. /// /// public class Region { private readonly List _rectangles = []; // Add a single reusable list for temp operations private readonly List _tempRectangles = new(); // Object used for synchronization private readonly object _syncLock = new object(); /// /// Initializes a new instance of the class. /// public Region () { } /// /// Initializes a new instance of the class with the specified rectangle. /// /// The initial rectangle for the region. public Region (Rectangle rectangle) { lock (_syncLock) { _rectangles.Add (rectangle); } } /// /// Creates an exact copy of the region. /// /// A new that is a copy of this instance. public Region Clone () { lock (_syncLock) { var clone = new Region (); clone._rectangles.Capacity = _rectangles.Count; // Pre-allocate capacity clone._rectangles.AddRange (_rectangles); return clone; } } /// /// Combines with the region using the specified operation. /// /// The rectangle to combine. /// The operation to perform. public void Combine (Rectangle rectangle, RegionOp operation) { lock (_syncLock) { if (rectangle.IsEmpty && operation != RegionOp.Replace) { if (operation == RegionOp.Intersect) { _rectangles.Clear (); } return; } Combine (new Region (rectangle), operation); } } /// /// Combines with the region using the specified operation. /// /// The region to combine. /// The operation to perform. public void Combine (Region? region, RegionOp operation) { lock (_syncLock) { CombineInternal(region, operation); } } // Private method to implement the combine logic within a lock private void CombineInternal(Region? region, RegionOp operation) { if (region is null || region._rectangles.Count == 0) { if (operation is RegionOp.Intersect or RegionOp.Replace) { _rectangles.Clear (); } return; } switch (operation) { case RegionOp.Difference: // region is regionB // We'll chain the difference: (regionA - rect1) - rect2 - rect3 ... List newRectangles = new (_rectangles); foreach (Rectangle rect in region._rectangles) { List temp = new (); foreach (Rectangle r in newRectangles) { temp.AddRange (SubtractRectangle (r, rect)); } newRectangles = temp; } _rectangles.Clear (); _rectangles.AddRange (newRectangles); break; case RegionOp.Intersect: List intersections = new (_rectangles.Count); // Pre-allocate // Null is same as empty region region ??= new (); foreach (Rectangle rect1 in _rectangles) { foreach (Rectangle rect2 in region!._rectangles) { Rectangle intersected = Rectangle.Intersect (rect1, rect2); if (!intersected.IsEmpty) { intersections.Add (intersected); } } } _rectangles.Clear (); _rectangles.AddRange (intersections); break; case RegionOp.Union: // Avoid collection initialization with spread operator _tempRectangles.Clear(); _tempRectangles.AddRange(_rectangles); if (region != null) { // Get the region's rectangles safely lock (region._syncLock) { _tempRectangles.AddRange(region._rectangles); } } List mergedUnion = MergeRectangles(_tempRectangles, false); _rectangles.Clear(); _rectangles.AddRange(mergedUnion); break; case RegionOp.MinimalUnion: // Avoid collection initialization with spread operator _tempRectangles.Clear(); _tempRectangles.AddRange(_rectangles); if (region != null) { // Get the region's rectangles safely lock (region._syncLock) { _tempRectangles.AddRange(region._rectangles); } } List mergedMinimalUnion = MergeRectangles(_tempRectangles, true); _rectangles.Clear(); _rectangles.AddRange(mergedMinimalUnion); break; case RegionOp.XOR: Exclude (region); region.Combine (this, RegionOp.Difference); _rectangles.AddRange (region._rectangles); break; case RegionOp.ReverseDifference: region.Combine (this, RegionOp.Difference); _rectangles.Clear (); _rectangles.AddRange (region._rectangles); break; case RegionOp.Replace: _rectangles.Clear (); _rectangles.Capacity = region._rectangles.Count; // Pre-allocate _rectangles.AddRange (region._rectangles); break; } } /// /// Updates the region to be the complement of itself within the specified bounds. /// /// The bounding rectangle to use for complementing the region. public void Complement (Rectangle bounds) { if (bounds.IsEmpty || _rectangles.Count == 0) { _rectangles.Clear (); return; } List complementRectangles = new (4) { bounds }; // Typical max initial capacity foreach (Rectangle rect in _rectangles) { complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList (); } _rectangles.Clear (); _rectangles.AddRange (complementRectangles); } /// /// Determines whether the specified point is contained within the region. /// /// The x-coordinate of the point. /// The y-coordinate of the point. /// true if the point is contained within the region; otherwise, false. public bool Contains (int x, int y) { lock (_syncLock) { foreach (Rectangle r in _rectangles) { if (r.Contains (x, y)) { return true; } } return false; } } /// /// Determines whether the specified rectangle is contained within the region. /// /// The rectangle to check for containment. /// true if the rectangle is contained within the region; otherwise, false. public bool Contains (Rectangle rectangle) { lock (_syncLock) { foreach (Rectangle r in _rectangles) { if (r.Contains (rectangle)) { return true; } } return false; } } /// /// Determines whether the specified object is equal to this region. /// /// The object to compare with this region. /// true if the objects are equal; otherwise, false. public override bool Equals (object? obj) { return obj is Region other && Equals (other); } private static bool IsRegionEmpty (List rectangles) { if (rectangles.Count == 0) { return true; } foreach (Rectangle r in rectangles) { if (r is { IsEmpty: false, Width: > 0, Height: > 0 }) { return false; } } return true; } /// /// Determines whether the specified region is equal to this region. /// /// The region to compare with this region. /// true if the regions are equal; otherwise, false. public bool Equals (Region? other) { if (other is null) { return false; } if (ReferenceEquals (this, other)) { return true; } // Check if either region is empty bool thisEmpty = IsRegionEmpty (_rectangles); bool otherEmpty = IsRegionEmpty (other._rectangles); // If either is empty, they're equal only if both are empty if (thisEmpty || otherEmpty) { return thisEmpty == otherEmpty; } // For non-empty regions, compare rectangle counts if (_rectangles.Count != other._rectangles.Count) { return false; } // Compare all rectangles - order matters since we maintain canonical form for (var i = 0; i < _rectangles.Count; i++) { if (!_rectangles [i].Equals (other._rectangles [i])) { return false; } } return true; } /// /// Removes the specified rectangle from the region. /// /// /// /// This is a helper method that is equivalent to calling with /// . /// /// /// The rectangle to exclude from the region. public void Exclude (Rectangle rectangle) { Combine (rectangle, RegionOp.Difference); } /// /// Removes the portion of the specified region from this region. /// /// /// /// This is a helper method that is equivalent to calling with /// . /// /// /// The region to exclude from this region. public void Exclude (Region? region) { Combine (region, RegionOp.Difference); } /// /// Gets a bounding rectangle for the entire region. /// /// A that bounds the region. public Rectangle GetBounds () { if (_rectangles.Count == 0) { return Rectangle.Empty; } Rectangle first = _rectangles [0]; int left = first.Left; int top = first.Top; int right = first.Right; int bottom = first.Bottom; for (var i = 1; i < _rectangles.Count; i++) { Rectangle r = _rectangles [i]; left = Math.Min (left, r.Left); top = Math.Min (top, r.Top); right = Math.Max (right, r.Right); bottom = Math.Max (bottom, r.Bottom); } return new (left, top, right - left, bottom - top); } /// /// Returns a hash code for this region. /// /// A hash code for this region. public override int GetHashCode () { var hash = new HashCode (); foreach (Rectangle rect in _rectangles) { hash.Add (rect); } return hash.ToHashCode (); } /// /// Returns an array of rectangles that represent the region. /// /// An array of objects that make up the region. public Rectangle [] GetRectangles () { return _rectangles.ToArray (); } /// /// Updates the region to be the intersection of itself with the specified rectangle. /// /// /// /// This is a helper method that is equivalent to calling with /// . /// /// /// The rectangle to intersect with the region. public void Intersect (Rectangle rectangle) { Combine (rectangle, RegionOp.Intersect); } /// /// Updates the region to be the intersection of itself with the specified region. /// /// /// /// This is a helper method that is equivalent to calling with /// . /// /// /// The region to intersect with this region. public void Intersect (Region? region) { Combine (region, RegionOp.Intersect); } /// /// Determines whether the region is empty. /// /// true if the region is empty; otherwise, false. public bool IsEmpty () { if (_rectangles.Count == 0) { return true; } foreach (Rectangle r in _rectangles) { if (r is { IsEmpty: false, Width: > 0, Height: > 0 }) { return false; } } return true; } /// /// Translates all rectangles in the region by the specified offsets. /// /// The amount to offset along the x-axis. /// The amount to offset along the y-axis. public void Translate (int offsetX, int offsetY) { if (offsetX == 0 && offsetY == 0) { return; } for (var i = 0; i < _rectangles.Count; i++) { Rectangle rect = _rectangles [i]; _rectangles [i] = rect with { X = rect.Left + offsetX, Y = rect.Top + offsetY }; } } /// /// Adds the specified rectangle to the region. Merges all rectangles into a minimal or granular bounding shape. /// /// The rectangle to add to the region. public void Union (Rectangle rectangle) { Combine (rectangle, RegionOp.Union); } /// /// Adds the specified region to this region. Merges all rectangles into a minimal or granular bounding shape. /// /// The region to add to this region. public void Union (Region? region) { Combine (region, RegionOp.Union); } /// /// Adds the specified rectangle to the region. Merges all rectangles into the smallest possible bounding shape. /// /// The rectangle to add to the region. public void MinimalUnion (Rectangle rectangle) { Combine (rectangle, RegionOp.MinimalUnion); } /// /// Adds the specified region to this region. Merges all rectangles into the smallest possible bounding shape. /// /// The region to add to this region. public void MinimalUnion (Region? region) { Combine (region, RegionOp.MinimalUnion); } /// /// Merges overlapping rectangles into a minimal or granular set of non-overlapping rectangles with a minimal bounding /// shape. /// /// The list of rectangles to merge. /// /// If true, minimizes the set to the smallest possible number of rectangles; otherwise, /// returns a granular set. /// /// A list of merged rectangles. internal static List MergeRectangles (List rectangles, bool minimize) { if (rectangles.Count == 0) { return []; } // Sweep-line algorithm to merge rectangles List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2); // Pre-allocate foreach (Rectangle r in rectangles) { if (!r.IsEmpty) { events.Add ((r.Left, true, r.Top, r.Bottom)); // Start event events.Add ((r.Right, false, r.Top, r.Bottom)); // End event } } if (events.Count == 0) { return []; // Return empty list if no non-empty rectangles exist } events.Sort ( (a, b) => { int cmp = a.x.CompareTo (b.x); if (cmp != 0) { return cmp; } return a.isStart.CompareTo (b.isStart); // Start events before end events at same x }); List merged = []; SortedSet<(int yTop, int yBottom)> active = new ( Comparer<(int yTop, int yBottom)>.Create ( (a, b) => { int cmp = a.yTop.CompareTo (b.yTop); return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom); })); int lastX = events [0].x; foreach ((int x, bool isStart, int yTop, int yBottom) evt in events) { // Output rectangles for the previous segment if there are active rectangles if (active.Count > 0 && evt.x > lastX) { merged.AddRange (MergeVerticalIntervals (active, lastX, evt.x)); } // Process the event if (evt.isStart) { active.Add ((evt.yTop, evt.yBottom)); } else { active.Remove ((evt.yTop, evt.yBottom)); } lastX = evt.x; } return minimize ? MinimizeRectangles (merged) : merged; } /// /// Merges overlapping vertical intervals into a minimal set of non-overlapping rectangles. /// /// The set of active vertical intervals. /// The starting x-coordinate for the rectangles. /// The ending x-coordinate for the rectangles. /// A list of merged rectangles. internal static List MergeVerticalIntervals (SortedSet<(int yTop, int yBottom)> active, int startX, int endX) { if (active.Count == 0) { return []; } List result = new (active.Count); // Pre-allocate int? currentTop = null; int? currentBottom = null; foreach ((int yTop, int yBottom) in active) { if (currentTop == null) { currentTop = yTop; currentBottom = yBottom; } else if (yTop <= currentBottom) { currentBottom = Math.Max (currentBottom.Value, yBottom); } else { result.Add (new (startX, currentTop.Value, endX - startX, currentBottom.Value - currentTop.Value)); currentTop = yTop; currentBottom = yBottom; } } if (currentTop != null) { result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value)); } return result; } /// /// Minimizes a list of rectangles into the smallest possible set of non-overlapping rectangles /// by merging adjacent rectangles where possible. /// /// The list of rectangles to minimize. /// A list of minimized rectangles. internal static List MinimizeRectangles (List rectangles) { if (rectangles.Count <= 1) { return rectangles.ToList (); } List minimized = new (rectangles.Count); // Pre-allocate List current = new (rectangles); // Work with a copy bool changed; do { changed = false; minimized.Clear (); // Sort by Y then X for consistent processing current.Sort ( (a, b) => { int cmp = a.Top.CompareTo (b.Top); return cmp != 0 ? cmp : a.Left.CompareTo (b.Left); }); var i = 0; while (i < current.Count) { Rectangle r = current [i]; int j = i + 1; while (j < current.Count) { Rectangle next = current [j]; // Check if rectangles can be merged horizontally (same Y range, adjacent X) if (r.Top == next.Top && r.Bottom == next.Bottom && (r.Right == next.Left || next.Right == r.Left || r.IntersectsWith (next))) { r = new ( Math.Min (r.Left, next.Left), r.Top, Math.Max (r.Right, next.Right) - Math.Min (r.Left, next.Left), r.Height ); current.RemoveAt (j); changed = true; } // Check if rectangles can be merged vertically (same X range, adjacent Y) else if (r.Left == next.Left && r.Right == next.Right && (r.Bottom == next.Top || next.Bottom == r.Top || r.IntersectsWith (next))) { r = new ( r.Left, Math.Min (r.Top, next.Top), r.Width, Math.Max (r.Bottom, next.Bottom) - Math.Min (r.Top, next.Top) ); current.RemoveAt (j); changed = true; } else { j++; } } minimized.Add (r); i++; } current = minimized.ToList (); // Prepare for next iteration } while (changed); return minimized; } /// /// Subtracts the specified rectangle from the original rectangle, returning the resulting rectangles. /// /// The original rectangle. /// The rectangle to subtract from the original. /// An enumerable collection of resulting rectangles after subtraction. internal static IEnumerable SubtractRectangle (Rectangle original, Rectangle subtract) { // Handle empty or invalid rectangles if (original.IsEmpty || original.Width <= 0 || original.Height <= 0) { yield break; // Return empty enumeration for empty or invalid original } if (subtract.IsEmpty || subtract.Width <= 0 || subtract.Height <= 0) { yield return original; yield break; } // Check for complete overlap (subtract fully contains or equals original) if (subtract.Left <= original.Left && subtract.Top <= original.Top && subtract.Right >= original.Right && subtract.Bottom >= original.Bottom) { yield break; // Return empty if subtract completely overlaps original } // Check for no overlap if (!original.IntersectsWith (subtract)) { yield return original; yield break; } // Fragment the original rectangle into segments excluding the subtract rectangle // Top segment (above subtract) if (original.Top < subtract.Top) { yield return new ( original.Left, original.Top, original.Width, subtract.Top - original.Top); } // Bottom segment (below subtract) if (original.Bottom > subtract.Bottom) { yield return new ( original.Left, subtract.Bottom, original.Width, original.Bottom - subtract.Bottom); } // Left segment (to the left of subtract) if (original.Left < subtract.Left) { int top = Math.Max (original.Top, subtract.Top); int bottom = Math.Min (original.Bottom, subtract.Bottom); if (bottom > top) { yield return new ( original.Left, top, subtract.Left - original.Left, bottom - top); } } // Right segment (to the right of subtract) if (original.Right > subtract.Right) { int top = Math.Max (original.Top, subtract.Top); int bottom = Math.Min (original.Bottom, subtract.Bottom); if (bottom > top) { yield return new ( subtract.Right, top, original.Right - subtract.Right, bottom - top); } } } /// /// Fills the interior of all rectangles in the region with the specified attribute and fill rune. /// /// The attribute (color/style) to use. /// /// The rune to fill the interior of the rectangles with. If space will be /// used. /// public void FillRectangles (Attribute attribute, Rune? fillRune = null) { if (_rectangles.Count == 0) { return; } foreach (Rectangle rect in _rectangles) { if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0) { continue; } Application.Driver?.SetAttribute (attribute); for (int y = rect.Top; y < rect.Bottom; y++) { for (int x = rect.Left; x < rect.Right; x++) { Application.Driver?.Move (x, y); Application.Driver?.AddRune (fillRune ?? (Rune)' '); } } } } /// /// Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big /// enough. /// /// The canvas to draw on. /// The line style to use for drawing. /// The attribute (color/style) to use for the lines. If null. public void DrawBoundaries (LineCanvas canvas, LineStyle style, Attribute? attribute = null) { if (_rectangles.Count == 0) { return; } foreach (Rectangle rect in _rectangles) { if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0) { continue; } // Only draw boundaries if the rectangle is "big enough" (e.g., width and height > 1) //if (rect.Width > 2 && rect.Height > 2) { if (rect.Width > 1) { // Add horizontal lines canvas.AddLine (new (rect.Left, rect.Top), rect.Width, Orientation.Horizontal, style, attribute); canvas.AddLine (new (rect.Left, rect.Bottom - 1), rect.Width, Orientation.Horizontal, style, attribute); } if (rect.Height > 1) { // Add vertical lines canvas.AddLine (new (rect.Left, rect.Top), rect.Height, Orientation.Vertical, style, attribute); canvas.AddLine (new (rect.Right - 1, rect.Top), rect.Height, Orientation.Vertical, style, attribute); } } } } // BUGBUG: DrawOuterBoundary does not work right. it draws all regions +1 too tall/wide. It should draw single width/height regions as just a line. // // Example: There are 3 regions here. the first is a rect (0,0,1,4). Second is (10, 0, 2, 4). // This is how they should draw: // // |123456789|123456789|123456789 // 1 │ ┌┐ ┌─┐ // 2 │ ││ │ │ // 3 │ ││ │ │ // 4 │ └┘ └─┘ // // But this is what it draws: // |123456789|123456789|123456789 // 1┌┐ ┌─┐ ┌──┐ // 2││ │ │ │ │ // 3││ │ │ │ │ // 4││ │ │ │ │ // 5└┘ └─┘ └──┘ // // Example: There are two rectangles in this region. (0,0,3,3) and (3, 3, 3, 3). // This is fill - correct: // |123456789 // 1░░░ // 2░░░ // 3░░░░░ // 4 ░░░ // 5 ░░░ // 6 // // This is what DrawOuterBoundary should draw // |123456789|123456789 // 1┌─┐ // 2│ │ // 3└─┼─┐ // 4 │ │ // 5 └─┘ // 6 // // This is what DrawOuterBoundary actually draws // |123456789|123456789 // 1┌──┐ // 2│ │ // 3│ └─┐ // 4└─┐ │ // 5 │ │ // 6 └──┘ /// /// Draws the outer perimeter of the region to using and /// . /// The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing /// boundaries and excluding internal lines. /// /// The LineCanvas to draw on. /// The line style to use for drawing. /// The attribute (color/style) to use for the lines. If null. public void DrawOuterBoundary (LineCanvas lineCanvas, LineStyle style, Attribute? attribute = null) { if (_rectangles.Count == 0) { return; } // Get the bounds of the region Rectangle bounds = GetBounds (); // Add protection against extremely large allocations if (bounds.Width > 1000 || bounds.Height > 1000) { // Fall back to drawing each rectangle's boundary DrawBoundaries(lineCanvas, style, attribute); return; } // Create a grid to track which cells are inside the region var insideRegion = new bool [bounds.Width + 1, bounds.Height + 1]; // Fill the grid based on rectangles foreach (Rectangle rect in _rectangles) { if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0) { continue; } for (int x = rect.Left; x < rect.Right; x++) { for (int y = rect.Top; y < rect.Bottom; y++) { // Adjust coordinates to grid space int gridX = x - bounds.Left; int gridY = y - bounds.Top; if (gridX >= 0 && gridX < bounds.Width && gridY >= 0 && gridY < bounds.Height) { insideRegion [gridX, gridY] = true; } } } } // Find horizontal boundary lines for (var y = 0; y <= bounds.Height; y++) { int startX = -1; for (var x = 0; x <= bounds.Width; x++) { bool above = y > 0 && insideRegion [x, y - 1]; bool below = y < bounds.Height && insideRegion [x, y]; // A boundary exists where one side is inside and the other is outside bool isBoundary = above != below; if (isBoundary) { // Start a new segment or continue the current one if (startX == -1) { startX = x; } } else { // End the current segment if one exists if (startX != -1) { int length = x - startX + 1; // Add 1 to make sure lines connect lineCanvas.AddLine ( new (startX + bounds.Left, y + bounds.Top), length, Orientation.Horizontal, style, attribute ); startX = -1; } } } // End any segment that reaches the right edge if (startX != -1) { int length = bounds.Width + 1 - startX + 1; // Add 1 to make sure lines connect lineCanvas.AddLine ( new (startX + bounds.Left, y + bounds.Top), length, Orientation.Horizontal, style, attribute ); } } // Find vertical boundary lines for (var x = 0; x <= bounds.Width; x++) { int startY = -1; for (var y = 0; y <= bounds.Height; y++) { bool left = x > 0 && insideRegion [x - 1, y]; bool right = x < bounds.Width && insideRegion [x, y]; // A boundary exists where one side is inside and the other is outside bool isBoundary = left != right; if (isBoundary) { // Start a new segment or continue the current one if (startY == -1) { startY = y; } } else { // End the current segment if one exists if (startY != -1) { int length = y - startY + 1; // Add 1 to make sure lines connect lineCanvas.AddLine ( new (x + bounds.Left, startY + bounds.Top), length, Orientation.Vertical, style, attribute ); startY = -1; } } } // End any segment that reaches the bottom edge if (startY != -1) { int length = bounds.Height + 1 - startY + 1; // Add 1 to make sure lines connect lineCanvas.AddLine ( new (x + bounds.Left, startY + bounds.Top), length, Orientation.Vertical, style, attribute ); } } } }