#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
);
}
}
}
}