|
@@ -1,90 +1,245 @@
|
|
|
-/// <summary>
|
|
|
-/// Represents a region composed of one or more rectangles, providing methods for union, intersection, exclusion, and
|
|
|
-/// complement operations.
|
|
|
+#nullable enable
|
|
|
+
|
|
|
+namespace Terminal.Gui;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// 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.
|
|
|
/// </summary>
|
|
|
-public class Region : IDisposable
|
|
|
+/// <remarks>
|
|
|
+/// <para>
|
|
|
+/// This class is thread-safe. All operations are synchronized to ensure consistent state when accessed concurrently.
|
|
|
+/// </para>
|
|
|
+/// <para>
|
|
|
+/// The <see cref="Region"/> class adopts a philosophy of efficiency and flexibility, balancing performance with
|
|
|
+/// usability for GUI applications. It maintains a list of <see cref="Rectangle"/> 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 (<see cref="RegionOp.Union"/> or
|
|
|
+/// <see cref="RegionOp.MinimalUnion"/>),
|
|
|
+/// finding common areas (<see cref="RegionOp.Intersect"/>), or removing portions (
|
|
|
+/// <see cref="RegionOp.Difference"/> or
|
|
|
+/// <see cref="Exclude(Rectangle)"/>).
|
|
|
+/// </para>
|
|
|
+/// <para>
|
|
|
+/// 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 <see cref="RegionOp.Union"/> and <see cref="RegionOp.MinimalUnion"/>, an optional minimization
|
|
|
+/// step (
|
|
|
+/// <see
|
|
|
+/// cref="MinimizeRectangles"/>
|
|
|
+/// ) 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.
|
|
|
+/// </para>
|
|
|
+/// <para>
|
|
|
+/// The class is immutable in its operations (returning new regions or modifying in-place via methods like
|
|
|
+/// <see cref="Combine(Rectangle,RegionOp)"/>), supports nullability for robustness, and implements
|
|
|
+/// <see cref="IDisposable"/> to manage
|
|
|
+/// resources by clearing internal state. Developers can choose between granular (detailed) or minimal (compact)
|
|
|
+/// outputs for union operations via <see cref="RegionOp.Union"/> and <see cref="RegionOp.MinimalUnion"/>, catering
|
|
|
+/// to diverse use cases such as rendering optimization, event handling, or visualization.
|
|
|
+/// </para>
|
|
|
+/// </remarks>
|
|
|
+public class Region
|
|
|
{
|
|
|
- private List<Rectangle> _rectangles;
|
|
|
+ private readonly List<Rectangle> _rectangles = [];
|
|
|
+
|
|
|
+ // Add a single reusable list for temp operations
|
|
|
+ private readonly List<Rectangle> _tempRectangles = new();
|
|
|
+
|
|
|
+ // Object used for synchronization
|
|
|
+ private readonly object _syncLock = new object();
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance of the <see cref="Region"/> class.
|
|
|
/// </summary>
|
|
|
- public Region () { _rectangles = new (); }
|
|
|
+ public Region () { }
|
|
|
|
|
|
/// <summary>
|
|
|
/// Initializes a new instance of the <see cref="Region"/> class with the specified rectangle.
|
|
|
/// </summary>
|
|
|
/// <param name="rectangle">The initial rectangle for the region.</param>
|
|
|
- public Region (Rectangle rectangle) { _rectangles = new () { rectangle }; }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Adds the specified rectangle to the region.
|
|
|
- /// </summary>
|
|
|
- /// <param name="rectangle">The rectangle to add to the region.</param>
|
|
|
- public void Union (Rectangle rectangle)
|
|
|
+ public Region (Rectangle rectangle)
|
|
|
{
|
|
|
- _rectangles.Add (rectangle);
|
|
|
- _rectangles = MergeRectangles (_rectangles);
|
|
|
+ lock (_syncLock)
|
|
|
+ {
|
|
|
+ _rectangles.Add (rectangle);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Adds the specified region to this region.
|
|
|
+ /// Creates an exact copy of the region.
|
|
|
/// </summary>
|
|
|
- /// <param name="region">The region to add to this region.</param>
|
|
|
- public void Union (Region region)
|
|
|
+ /// <returns>A new <see cref="Region"/> that is a copy of this instance.</returns>
|
|
|
+ public Region Clone ()
|
|
|
{
|
|
|
- _rectangles.AddRange (region._rectangles);
|
|
|
- _rectangles = MergeRectangles (_rectangles);
|
|
|
- }
|
|
|
+ lock (_syncLock)
|
|
|
+ {
|
|
|
+ var clone = new Region ();
|
|
|
+ clone._rectangles.Capacity = _rectangles.Count; // Pre-allocate capacity
|
|
|
+ clone._rectangles.AddRange (_rectangles);
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Updates the region to be the intersection of itself with the specified rectangle.
|
|
|
- /// </summary>
|
|
|
- /// <param name="rectangle">The rectangle to intersect with the region.</param>
|
|
|
- public void Intersect (Rectangle rectangle)
|
|
|
- {
|
|
|
- _rectangles = _rectangles.Select (r => Rectangle.Intersect (r, rectangle)).Where (r => !r.IsEmpty).ToList ();
|
|
|
+ return clone;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Updates the region to be the intersection of itself with the specified region.
|
|
|
+ /// Combines <paramref name="rectangle"/> with the region using the specified operation.
|
|
|
/// </summary>
|
|
|
- /// <param name="region">The region to intersect with this region.</param>
|
|
|
- public void Intersect (Region region)
|
|
|
+ /// <param name="rectangle">The rectangle to combine.</param>
|
|
|
+ /// <param name="operation">The operation to perform.</param>
|
|
|
+ public void Combine (Rectangle rectangle, RegionOp operation)
|
|
|
{
|
|
|
- List<Rectangle> intersections = new List<Rectangle> ();
|
|
|
-
|
|
|
- foreach (Rectangle rect1 in _rectangles)
|
|
|
+ lock (_syncLock)
|
|
|
{
|
|
|
- foreach (Rectangle rect2 in region._rectangles)
|
|
|
+ if (rectangle.IsEmpty && operation != RegionOp.Replace)
|
|
|
{
|
|
|
- Rectangle intersected = Rectangle.Intersect (rect1, rect2);
|
|
|
-
|
|
|
- if (!intersected.IsEmpty)
|
|
|
+ if (operation == RegionOp.Intersect)
|
|
|
{
|
|
|
- intersections.Add (intersected);
|
|
|
+ _rectangles.Clear ();
|
|
|
}
|
|
|
+
|
|
|
+ return;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- _rectangles = intersections;
|
|
|
+ Combine (new Region (rectangle), operation);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Removes the specified rectangle from the region.
|
|
|
+ /// Combines <paramref name="region"/> with the region using the specified operation.
|
|
|
/// </summary>
|
|
|
- /// <param name="rectangle">The rectangle to exclude from the region.</param>
|
|
|
- public void Exclude (Rectangle rectangle) { _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rectangle)).ToList (); }
|
|
|
+ /// <param name="region">The region to combine.</param>
|
|
|
+ /// <param name="operation">The operation to perform.</param>
|
|
|
+ public void Combine (Region? region, RegionOp operation)
|
|
|
+ {
|
|
|
+ lock (_syncLock)
|
|
|
+ {
|
|
|
+ CombineInternal(region, operation);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Removes the portion of the specified region from this region.
|
|
|
- /// </summary>
|
|
|
- /// <param name="region">The region to exclude from this region.</param>
|
|
|
- public void Exclude (Region region)
|
|
|
+ // Private method to implement the combine logic within a lock
|
|
|
+ private void CombineInternal(Region? region, RegionOp operation)
|
|
|
{
|
|
|
- foreach (Rectangle rect in region._rectangles)
|
|
|
+ if (region is null || region._rectangles.Count == 0)
|
|
|
{
|
|
|
- _rectangles = _rectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
|
|
|
+ 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<Rectangle> newRectangles = new (_rectangles);
|
|
|
+
|
|
|
+ foreach (Rectangle rect in region._rectangles)
|
|
|
+ {
|
|
|
+ List<Rectangle> temp = new ();
|
|
|
+
|
|
|
+ foreach (Rectangle r in newRectangles)
|
|
|
+ {
|
|
|
+ temp.AddRange (SubtractRectangle (r, rect));
|
|
|
+ }
|
|
|
+
|
|
|
+ newRectangles = temp;
|
|
|
+ }
|
|
|
+
|
|
|
+ _rectangles.Clear ();
|
|
|
+ _rectangles.AddRange (newRectangles);
|
|
|
+
|
|
|
+ break;
|
|
|
+
|
|
|
+ case RegionOp.Intersect:
|
|
|
+ List<Rectangle> 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<Rectangle> 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<Rectangle> 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;
|
|
|
}
|
|
|
}
|
|
|
|
|
@@ -101,28 +256,154 @@ public class Region : IDisposable
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- List<Rectangle> complementRectangles = new List<Rectangle> { bounds };
|
|
|
+ List<Rectangle> complementRectangles = new (4) { bounds }; // Typical max initial capacity
|
|
|
|
|
|
foreach (Rectangle rect in _rectangles)
|
|
|
{
|
|
|
complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
|
|
|
}
|
|
|
|
|
|
- _rectangles = complementRectangles;
|
|
|
+ _rectangles.Clear ();
|
|
|
+ _rectangles.AddRange (complementRectangles);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Creates an exact copy of the region.
|
|
|
+ /// Determines whether the specified point is contained within the region.
|
|
|
/// </summary>
|
|
|
- /// <returns>A new <see cref="Region"/> that is a copy of this instance.</returns>
|
|
|
- public Region Clone ()
|
|
|
+ /// <param name="x">The x-coordinate of the point.</param>
|
|
|
+ /// <param name="y">The y-coordinate of the point.</param>
|
|
|
+ /// <returns><c>true</c> if the point is contained within the region; otherwise, <c>false</c>.</returns>
|
|
|
+ public bool Contains (int x, int y)
|
|
|
+ {
|
|
|
+ lock (_syncLock)
|
|
|
+ {
|
|
|
+ foreach (Rectangle r in _rectangles)
|
|
|
+ {
|
|
|
+ if (r.Contains (x, y))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Determines whether the specified rectangle is contained within the region.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="rectangle">The rectangle to check for containment.</param>
|
|
|
+ /// <returns><c>true</c> if the rectangle is contained within the region; otherwise, <c>false</c>.</returns>
|
|
|
+ public bool Contains (Rectangle rectangle)
|
|
|
+ {
|
|
|
+ lock (_syncLock)
|
|
|
+ {
|
|
|
+ foreach (Rectangle r in _rectangles)
|
|
|
+ {
|
|
|
+ if (r.Contains (rectangle))
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Determines whether the specified object is equal to this region.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="obj">The object to compare with this region.</param>
|
|
|
+ /// <returns><c>true</c> if the objects are equal; otherwise, <c>false</c>.</returns>
|
|
|
+ public override bool Equals (object? obj) { return obj is Region other && Equals (other); }
|
|
|
+
|
|
|
+ private static bool IsRegionEmpty (List<Rectangle> rectangles)
|
|
|
{
|
|
|
- var clone = new Region ();
|
|
|
- clone._rectangles = new (_rectangles);
|
|
|
+ if (rectangles.Count == 0)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
|
|
|
- return clone;
|
|
|
+ foreach (Rectangle r in rectangles)
|
|
|
+ {
|
|
|
+ if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
+ /// <summary>
|
|
|
+ /// Determines whether the specified region is equal to this region.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="other">The region to compare with this region.</param>
|
|
|
+ /// <returns><c>true</c> if the regions are equal; otherwise, <c>false</c>.</returns>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Removes the specified rectangle from the region.
|
|
|
+ /// </summary>
|
|
|
+ /// <remarks>
|
|
|
+ /// <para>
|
|
|
+ /// This is a helper method that is equivalent to calling <see cref="Combine(Rectangle,RegionOp)"/> with
|
|
|
+ /// <see cref="RegionOp.Difference"/>.
|
|
|
+ /// </para>
|
|
|
+ /// </remarks>
|
|
|
+ /// <param name="rectangle">The rectangle to exclude from the region.</param>
|
|
|
+ public void Exclude (Rectangle rectangle) { Combine (rectangle, RegionOp.Difference); }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Removes the portion of the specified region from this region.
|
|
|
+ /// </summary>
|
|
|
+ /// <remarks>
|
|
|
+ /// <para>
|
|
|
+ /// This is a helper method that is equivalent to calling <see cref="Combine(Region,RegionOp)"/> with
|
|
|
+ /// <see cref="RegionOp.Difference"/>.
|
|
|
+ /// </para>
|
|
|
+ /// </remarks>
|
|
|
+ /// <param name="region">The region to exclude from this region.</param>
|
|
|
+ public void Exclude (Region? region) { Combine (region, RegionOp.Difference); }
|
|
|
+
|
|
|
/// <summary>
|
|
|
/// Gets a bounding rectangle for the entire region.
|
|
|
/// </summary>
|
|
@@ -134,94 +415,346 @@ public class Region : IDisposable
|
|
|
return Rectangle.Empty;
|
|
|
}
|
|
|
|
|
|
- int left = _rectangles.Min (r => r.Left);
|
|
|
- int top = _rectangles.Min (r => r.Top);
|
|
|
- int right = _rectangles.Max (r => r.Right);
|
|
|
- int bottom = _rectangles.Max (r => r.Bottom);
|
|
|
+ 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);
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Determines whether the region is empty.
|
|
|
+ /// Returns a hash code for this region.
|
|
|
/// </summary>
|
|
|
- /// <returns><c>true</c> if the region is empty; otherwise, <c>false</c>.</returns>
|
|
|
- public bool IsEmpty () { return !_rectangles.Any (); }
|
|
|
+ /// <returns>A hash code for this region.</returns>
|
|
|
+ public override int GetHashCode ()
|
|
|
+ {
|
|
|
+ var hash = new HashCode ();
|
|
|
+
|
|
|
+ foreach (Rectangle rect in _rectangles)
|
|
|
+ {
|
|
|
+ hash.Add (rect);
|
|
|
+ }
|
|
|
+
|
|
|
+ return hash.ToHashCode ();
|
|
|
+ }
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Determines whether the specified point is contained within the region.
|
|
|
+ /// Returns an array of rectangles that represent the region.
|
|
|
/// </summary>
|
|
|
- /// <param name="x">The x-coordinate of the point.</param>
|
|
|
- /// <param name="y">The y-coordinate of the point.</param>
|
|
|
- /// <returns><c>true</c> if the point is contained within the region; otherwise, <c>false</c>.</returns>
|
|
|
- public bool Contains (int x, int y) { return _rectangles.Any (r => r.Contains (x, y)); }
|
|
|
+ /// <returns>An array of <see cref="Rectangle"/> objects that make up the region.</returns>
|
|
|
+ public Rectangle [] GetRectangles () { return _rectangles.ToArray (); }
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Determines whether the specified rectangle is contained within the region.
|
|
|
+ /// Updates the region to be the intersection of itself with the specified rectangle.
|
|
|
/// </summary>
|
|
|
- /// <param name="rectangle">The rectangle to check for containment.</param>
|
|
|
- /// <returns><c>true</c> if the rectangle is contained within the region; otherwise, <c>false</c>.</returns>
|
|
|
- public bool Contains (Rectangle rectangle) { return _rectangles.Any (r => r.Contains (rectangle)); }
|
|
|
+ /// <remarks>
|
|
|
+ /// <para>
|
|
|
+ /// This is a helper method that is equivalent to calling <see cref="Combine(Rectangle,RegionOp)"/> with
|
|
|
+ /// <see cref="RegionOp.Intersect"/>.
|
|
|
+ /// </para>
|
|
|
+ /// </remarks>
|
|
|
+ /// <param name="rectangle">The rectangle to intersect with the region.</param>
|
|
|
+ public void Intersect (Rectangle rectangle) { Combine (rectangle, RegionOp.Intersect); }
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Returns an array of rectangles that represent the region.
|
|
|
+ /// Updates the region to be the intersection of itself with the specified region.
|
|
|
/// </summary>
|
|
|
- /// <returns>An array of <see cref="Rectangle"/> objects that make up the region.</returns>
|
|
|
- public Rectangle [] GetRegionScans () { return _rectangles.ToArray (); }
|
|
|
+ /// <remarks>
|
|
|
+ /// <para>
|
|
|
+ /// This is a helper method that is equivalent to calling <see cref="Combine(Region,RegionOp)"/> with
|
|
|
+ /// <see cref="RegionOp.Intersect"/>.
|
|
|
+ /// </para>
|
|
|
+ /// </remarks>
|
|
|
+ /// <param name="region">The region to intersect with this region.</param>
|
|
|
+ public void Intersect (Region? region) { Combine (region, RegionOp.Intersect); }
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Offsets all rectangles in the region by the specified amounts.
|
|
|
+ /// Determines whether the region is empty.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns><c>true</c> if the region is empty; otherwise, <c>false</c>.</returns>
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Translates all rectangles in the region by the specified offsets.
|
|
|
/// </summary>
|
|
|
/// <param name="offsetX">The amount to offset along the x-axis.</param>
|
|
|
/// <param name="offsetY">The amount to offset along the y-axis.</param>
|
|
|
- public void Offset (int offsetX, int offsetY)
|
|
|
+ public void Translate (int offsetX, int offsetY)
|
|
|
{
|
|
|
- for (int i = 0; i < _rectangles.Count; i++)
|
|
|
+ if (offsetX == 0 && offsetY == 0)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (var i = 0; i < _rectangles.Count; i++)
|
|
|
{
|
|
|
- var rect = _rectangles [i];
|
|
|
- _rectangles [i] = new Rectangle (rect.Left + offsetX, rect.Top + offsetY, rect.Width, rect.Height);
|
|
|
+ Rectangle rect = _rectangles [i];
|
|
|
+ _rectangles [i] = rect with { X = rect.Left + offsetX, Y = rect.Top + offsetY };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Merges overlapping rectangles into a minimal set of non-overlapping rectangles.
|
|
|
+ /// Adds the specified rectangle to the region. Merges all rectangles into a minimal or granular bounding shape.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="rectangle">The rectangle to add to the region.</param>
|
|
|
+ public void Union (Rectangle rectangle) { Combine (rectangle, RegionOp.Union); }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Adds the specified region to this region. Merges all rectangles into a minimal or granular bounding shape.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="region">The region to add to this region.</param>
|
|
|
+ public void Union (Region? region) { Combine (region, RegionOp.Union); }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Adds the specified rectangle to the region. Merges all rectangles into the smallest possible bounding shape.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="rectangle">The rectangle to add to the region.</param>
|
|
|
+ public void MinimalUnion (Rectangle rectangle) { Combine (rectangle, RegionOp.MinimalUnion); }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Adds the specified region to this region. Merges all rectangles into the smallest possible bounding shape.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="region">The region to add to this region.</param>
|
|
|
+ public void MinimalUnion (Region? region) { Combine (region, RegionOp.MinimalUnion); }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Merges overlapping rectangles into a minimal or granular set of non-overlapping rectangles with a minimal bounding
|
|
|
+ /// shape.
|
|
|
/// </summary>
|
|
|
/// <param name="rectangles">The list of rectangles to merge.</param>
|
|
|
+ /// <param name="minimize">
|
|
|
+ /// If <c>true</c>, minimizes the set to the smallest possible number of rectangles; otherwise,
|
|
|
+ /// returns a granular set.
|
|
|
+ /// </param>
|
|
|
/// <returns>A list of merged rectangles.</returns>
|
|
|
- private List<Rectangle> MergeRectangles (List<Rectangle> rectangles)
|
|
|
+ internal static List<Rectangle> MergeRectangles (List<Rectangle> rectangles, bool minimize)
|
|
|
{
|
|
|
- // Simplified merging logic: this does not handle all edge cases for merging overlapping rectangles.
|
|
|
- // For a full implementation, a plane sweep algorithm or similar would be needed.
|
|
|
- List<Rectangle> merged = new List<Rectangle> (rectangles);
|
|
|
- bool mergedAny;
|
|
|
+ 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<Rectangle> 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Merges overlapping vertical intervals into a minimal set of non-overlapping rectangles.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="active">The set of active vertical intervals.</param>
|
|
|
+ /// <param name="startX">The starting x-coordinate for the rectangles.</param>
|
|
|
+ /// <param name="endX">The ending x-coordinate for the rectangles.</param>
|
|
|
+ /// <returns>A list of merged rectangles.</returns>
|
|
|
+ internal static List<Rectangle> MergeVerticalIntervals (SortedSet<(int yTop, int yBottom)> active, int startX, int endX)
|
|
|
+ {
|
|
|
+ if (active.Count == 0)
|
|
|
+ {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Rectangle> 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;
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Minimizes a list of rectangles into the smallest possible set of non-overlapping rectangles
|
|
|
+ /// by merging adjacent rectangles where possible.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="rectangles">The list of rectangles to minimize.</param>
|
|
|
+ /// <returns>A list of minimized rectangles.</returns>
|
|
|
+ internal static List<Rectangle> MinimizeRectangles (List<Rectangle> rectangles)
|
|
|
+ {
|
|
|
+ if (rectangles.Count <= 1)
|
|
|
+ {
|
|
|
+ return rectangles.ToList ();
|
|
|
+ }
|
|
|
+
|
|
|
+ List<Rectangle> minimized = new (rectangles.Count); // Pre-allocate
|
|
|
+ List<Rectangle> current = new (rectangles); // Work with a copy
|
|
|
+
|
|
|
+ bool changed;
|
|
|
|
|
|
do
|
|
|
{
|
|
|
- mergedAny = false;
|
|
|
+ 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;
|
|
|
|
|
|
- for (var i = 0; i < merged.Count; i++)
|
|
|
+ while (i < current.Count)
|
|
|
{
|
|
|
- for (int j = i + 1; j < merged.Count; j++)
|
|
|
+ Rectangle r = current [i];
|
|
|
+ int j = i + 1;
|
|
|
+
|
|
|
+ while (j < current.Count)
|
|
|
{
|
|
|
- if (merged [i].IntersectsWith (merged [j]))
|
|
|
+ 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)))
|
|
|
{
|
|
|
- merged [i] = Rectangle.Union (merged [i], merged [j]);
|
|
|
- merged.RemoveAt (j);
|
|
|
- mergedAny = true;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
|
|
|
- break;
|
|
|
+ // 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++;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- if (mergedAny)
|
|
|
- {
|
|
|
- break;
|
|
|
- }
|
|
|
+ minimized.Add (r);
|
|
|
+ i++;
|
|
|
}
|
|
|
+
|
|
|
+ current = minimized.ToList (); // Prepare for next iteration
|
|
|
}
|
|
|
- while (mergedAny);
|
|
|
+ while (changed);
|
|
|
|
|
|
- return merged;
|
|
|
+ return minimized;
|
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
@@ -230,8 +763,28 @@ public class Region : IDisposable
|
|
|
/// <param name="original">The original rectangle.</param>
|
|
|
/// <param name="subtract">The rectangle to subtract from the original.</param>
|
|
|
/// <returns>An enumerable collection of resulting rectangles after subtraction.</returns>
|
|
|
- private IEnumerable<Rectangle> SubtractRectangle (Rectangle original, Rectangle subtract)
|
|
|
+ internal static IEnumerable<Rectangle> 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;
|
|
@@ -239,19 +792,29 @@ public class Region : IDisposable
|
|
|
yield break;
|
|
|
}
|
|
|
|
|
|
- // Top segment
|
|
|
+ // 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);
|
|
|
+ yield return new (
|
|
|
+ original.Left,
|
|
|
+ original.Top,
|
|
|
+ original.Width,
|
|
|
+ subtract.Top - original.Top);
|
|
|
}
|
|
|
|
|
|
- // Bottom segment
|
|
|
+ // Bottom segment (below subtract)
|
|
|
if (original.Bottom > subtract.Bottom)
|
|
|
{
|
|
|
- yield return new (original.Left, subtract.Bottom, original.Width, original.Bottom - subtract.Bottom);
|
|
|
+ yield return new (
|
|
|
+ original.Left,
|
|
|
+ subtract.Bottom,
|
|
|
+ original.Width,
|
|
|
+ original.Bottom - subtract.Bottom);
|
|
|
}
|
|
|
|
|
|
- // Left segment
|
|
|
+ // Left segment (to the left of subtract)
|
|
|
if (original.Left < subtract.Left)
|
|
|
{
|
|
|
int top = Math.Max (original.Top, subtract.Top);
|
|
@@ -259,11 +822,15 @@ public class Region : IDisposable
|
|
|
|
|
|
if (bottom > top)
|
|
|
{
|
|
|
- yield return new (original.Left, top, subtract.Left - original.Left, bottom - top);
|
|
|
+ yield return new (
|
|
|
+ original.Left,
|
|
|
+ top,
|
|
|
+ subtract.Left - original.Left,
|
|
|
+ bottom - top);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- // Right segment
|
|
|
+ // Right segment (to the right of subtract)
|
|
|
if (original.Right > subtract.Right)
|
|
|
{
|
|
|
int top = Math.Max (original.Top, subtract.Top);
|
|
@@ -271,13 +838,302 @@ public class Region : IDisposable
|
|
|
|
|
|
if (bottom > top)
|
|
|
{
|
|
|
- yield return new (subtract.Right, top, original.Right - subtract.Right, bottom - top);
|
|
|
+ yield return new (
|
|
|
+ subtract.Right,
|
|
|
+ top,
|
|
|
+ original.Right - subtract.Right,
|
|
|
+ bottom - top);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Fills the interior of all rectangles in the region with the specified attribute and fill rune.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="attribute">The attribute (color/style) to use.</param>
|
|
|
+ /// <param name="fillRune">
|
|
|
+ /// The rune to fill the interior of the rectangles with. If <cref langword="null"/> space will be
|
|
|
+ /// used.
|
|
|
+ /// </param>
|
|
|
+ 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)' ');
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+
|
|
|
/// <summary>
|
|
|
- /// Releases all resources used by the <see cref="Region"/>.
|
|
|
+ /// Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big
|
|
|
+ /// enough.
|
|
|
/// </summary>
|
|
|
- public void Dispose () { _rectangles.Clear (); }
|
|
|
+ /// <param name="canvas">The canvas to draw on.</param>
|
|
|
+ /// <param name="style">The line style to use for drawing.</param>
|
|
|
+ /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
|
|
|
+ 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 └──┘
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Draws the outer perimeter of the region to <paramref name="lineCanvas"/> using <paramref name="style"/> and
|
|
|
+ /// <paramref name="attribute"/>.
|
|
|
+ /// The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing
|
|
|
+ /// boundaries and excluding internal lines.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="lineCanvas">The LineCanvas to draw on.</param>
|
|
|
+ /// <param name="style">The line style to use for drawing.</param>
|
|
|
+ /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
|
|
|
+ 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
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|