| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201 |
-
- namespace Terminal.Gui.Drawing;
- /// <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>
- /// <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 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 () { }
- /// <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)
- {
- lock (_syncLock)
- {
- _rectangles.Add (rectangle);
- }
- }
- /// <summary>
- /// Creates an exact copy of the region.
- /// </summary>
- /// <returns>A new <see cref="Region"/> that is a copy of this instance.</returns>
- public Region Clone ()
- {
- lock (_syncLock)
- {
- var clone = new Region ();
- clone._rectangles.Capacity = _rectangles.Count; // Pre-allocate capacity
- clone._rectangles.AddRange (_rectangles);
- return clone;
- }
- }
- /// <summary>
- /// Combines <paramref name="rectangle"/> with the region using the specified operation.
- /// </summary>
- /// <param name="rectangle">The rectangle to combine.</param>
- /// <param name="operation">The operation to perform.</param>
- 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);
- }
- }
- /// <summary>
- /// Combines <paramref name="region"/> with the region using the specified operation.
- /// </summary>
- /// <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);
- }
- }
- // 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<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;
- }
- }
- /// <summary>
- /// Updates the region to be the complement of itself within the specified bounds.
- /// </summary>
- /// <param name="bounds">The bounding rectangle to use for complementing the region.</param>
- public void Complement (Rectangle bounds)
- {
- if (bounds.IsEmpty || _rectangles.Count == 0)
- {
- _rectangles.Clear ();
- return;
- }
- List<Rectangle> 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);
- }
- /// <summary>
- /// Determines whether the specified point is contained within 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)
- {
- 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)
- {
- 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>
- /// 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>
- /// <returns>A <see cref="Rectangle"/> that bounds the region.</returns>
- 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);
- }
- /// <summary>
- /// Returns a hash code for this region.
- /// </summary>
- /// <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>
- /// Returns an array of rectangles that represent the region.
- /// </summary>
- /// <returns>An array of <see cref="Rectangle"/> objects that make up the region.</returns>
- public Rectangle [] GetRectangles () { return _rectangles.ToArray (); }
- /// <summary>
- /// Updates the region to be the intersection of itself with the specified rectangle.
- /// </summary>
- /// <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>
- /// Updates the region to be the intersection of itself with the specified region.
- /// </summary>
- /// <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>
- /// 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 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 };
- }
- }
- /// <summary>
- /// 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>
- internal static List<Rectangle> MergeRectangles (List<Rectangle> rectangles, bool minimize)
- {
- if (rectangles.Count <= 1)
- {
- return rectangles.ToList ();
- }
- // Generate events
- List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2);
- foreach (Rectangle r in rectangles)
- {
- if (!r.IsEmpty)
- {
- events.Add ((r.Left, true, r.Top, r.Bottom));
- events.Add ((r.Right, false, r.Top, r.Bottom));
- }
- }
- if (events.Count == 0)
- {
- return [];
- }
- // Sort events:
- // 1. Primarily by x-coordinate.
- // 2. Secondary: End events before Start events at the same x.
- // 3. Tertiary: By yTop coordinate as a tie-breaker.
- // 4. Quaternary: By yBottom coordinate as a final tie-breaker.
- events.Sort (
- (a, b) =>
- {
- // 1. Sort by X
- int cmp = a.x.CompareTo (b.x);
- if (cmp != 0)
- {
- return cmp;
- }
- // 2. Sort End events before Start events
- bool aIsEnd = !a.isStart;
- bool bIsEnd = !b.isStart;
- cmp = aIsEnd.CompareTo (bIsEnd); // True (End) comes after False (Start)
- if (cmp != 0)
- {
- return -cmp; // Reverse: End (true) should come before Start (false)
- }
- // 3. Tie-breaker: Sort by yTop
- cmp = a.yTop.CompareTo (b.yTop);
- if (cmp != 0)
- {
- return cmp;
- }
- // 4. Final Tie-breaker: Sort by yBottom
- return a.yBottom.CompareTo (b.yBottom);
- });
- List<Rectangle> merged = [];
- // Use a dictionary to track active intervals and their overlap counts
- Dictionary<(int yTop, int yBottom), int> activeCounts = new ();
- // Comparer for sorting intervals when needed
- var intervalComparer = Comparer<(int yTop, int yBottom)>.Create (
- (a, b) =>
- {
- int cmp = a.yTop.CompareTo (b.yTop);
- return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
- });
- // Helper to get the current active intervals (where count > 0) as a SortedSet
- SortedSet<(int yTop, int yBottom)> GetActiveIntervals ()
- {
- var set = new SortedSet<(int yTop, int yBottom)> (intervalComparer);
- foreach (var kvp in activeCounts)
- {
- if (kvp.Value > 0)
- {
- set.Add (kvp.Key);
- }
- }
- return set;
- }
- // Group events by x-coordinate to process all events at a given x together
- var groupedEvents = events.GroupBy (e => e.x).OrderBy (g => g.Key);
- int lastX = groupedEvents.First ().Key; // Initialize with the first event's x
- foreach (var group in groupedEvents)
- {
- int currentX = group.Key;
- // Get active intervals based on state *before* processing events at currentX
- var currentActiveIntervals = GetActiveIntervals ();
- // 1. Output rectangles for the segment ending *before* this x coordinate
- if (currentX > lastX && currentActiveIntervals.Count > 0)
- {
- merged.AddRange (MergeVerticalIntervals (currentActiveIntervals, lastX, currentX));
- }
- // 2. Process all events *at* this x coordinate to update counts
- foreach (var evt in group)
- {
- var interval = (evt.yTop, evt.yBottom);
- if (evt.isStart)
- {
- activeCounts.TryGetValue (interval, out int count);
- activeCounts [interval] = count + 1;
- }
- else
- {
- // Only decrement/remove if the interval exists
- if (activeCounts.TryGetValue (interval, out int count))
- {
- if (count - 1 <= 0)
- {
- activeCounts.Remove (interval);
- }
- else
- {
- activeCounts [interval] = count - 1;
- }
- }
- }
- }
- // 3. Update lastX for the next segment
- lastX = currentX;
- }
- 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
- {
- 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;
- }
- /// <summary>
- /// Subtracts the specified rectangle from the original rectangle, returning the resulting rectangles.
- /// </summary>
- /// <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>
- 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;
- 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);
- }
- }
- }
- /// <summary>
- /// Fills the interior of all rectangles in the region with the specified attribute and fill rune.
- /// </summary>
- /// <param name="driver"></param>
- /// <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 (IDriver? driver, Attribute? attribute, Rune? fillRune = null)
- {
- ArgumentNullException.ThrowIfNull (driver);
- if (_rectangles.Count == 0)
- {
- return;
- }
- foreach (Rectangle rect in _rectangles)
- {
- if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
- {
- continue;
- }
- driver?.SetAttribute (attribute!.Value);
- for (int y = rect.Top; y < rect.Bottom; y++)
- {
- for (int x = rect.Left; x < rect.Right; x++)
- {
- driver?.Move (x, y);
- driver?.AddRune (fillRune ?? (Rune)' ');
- }
- }
- }
- }
- /// <summary>
- /// Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big
- /// enough.
- /// </summary>
- /// <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
- );
- }
- }
- }
- }
|