Region.cs 40 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Represents a region composed of one or more rectangles, providing methods for geometric set operations such as
  5. /// union,
  6. /// intersection, exclusion, and complement. This class is designed for use in graphical or terminal-based user
  7. /// interfaces
  8. /// where regions need to be manipulated to manage screen areas, clipping, or drawing boundaries.
  9. /// </summary>
  10. /// <remarks>
  11. /// <para>
  12. /// This class is thread-safe. All operations are synchronized to ensure consistent state when accessed concurrently.
  13. /// </para>
  14. /// <para>
  15. /// The <see cref="Region"/> class adopts a philosophy of efficiency and flexibility, balancing performance with
  16. /// usability for GUI applications. It maintains a list of <see cref="Rectangle"/> objects, representing disjoint
  17. /// (non-overlapping) rectangular areas, and supports operations inspired by set theory. These operations allow
  18. /// combining regions in various ways, such as merging areas (<see cref="RegionOp.Union"/> or
  19. /// <see cref="RegionOp.MinimalUnion"/>),
  20. /// finding common areas (<see cref="RegionOp.Intersect"/>), or removing portions (
  21. /// <see cref="RegionOp.Difference"/> or
  22. /// <see cref="Exclude(Rectangle)"/>).
  23. /// </para>
  24. /// <para>
  25. /// To achieve high performance, the class employs a sweep-line algorithm for merging rectangles, which efficiently
  26. /// processes large sets of rectangles in O(n log n) time by scanning along the x-axis and tracking active vertical
  27. /// intervals. This approach ensures scalability for typical GUI scenarios with moderate numbers of rectangles. For
  28. /// operations like <see cref="RegionOp.Union"/> and <see cref="RegionOp.MinimalUnion"/>, an optional minimization
  29. /// step (
  30. /// <see
  31. /// cref="MinimizeRectangles"/>
  32. /// ) is used to reduce the number of rectangles to a minimal set, producing the smallest
  33. /// possible collection of non-overlapping rectangles that cover the same area. This minimization, while O(n²) in
  34. /// worst-case complexity, is optimized for small-to-medium collections and provides a compact representation ideal
  35. /// for drawing or logical operations.
  36. /// </para>
  37. /// <para>
  38. /// The class is immutable in its operations (returning new regions or modifying in-place via methods like
  39. /// <see cref="Combine(Rectangle,RegionOp)"/>), supports nullability for robustness, and implements
  40. /// <see cref="IDisposable"/> to manage
  41. /// resources by clearing internal state. Developers can choose between granular (detailed) or minimal (compact)
  42. /// outputs for union operations via <see cref="RegionOp.Union"/> and <see cref="RegionOp.MinimalUnion"/>, catering
  43. /// to diverse use cases such as rendering optimization, event handling, or visualization.
  44. /// </para>
  45. /// </remarks>
  46. public class Region
  47. {
  48. private readonly List<Rectangle> _rectangles = [];
  49. // Add a single reusable list for temp operations
  50. private readonly List<Rectangle> _tempRectangles = new();
  51. // Object used for synchronization
  52. private readonly object _syncLock = new object();
  53. /// <summary>
  54. /// Initializes a new instance of the <see cref="Region"/> class.
  55. /// </summary>
  56. public Region () { }
  57. /// <summary>
  58. /// Initializes a new instance of the <see cref="Region"/> class with the specified rectangle.
  59. /// </summary>
  60. /// <param name="rectangle">The initial rectangle for the region.</param>
  61. public Region (Rectangle rectangle)
  62. {
  63. lock (_syncLock)
  64. {
  65. _rectangles.Add (rectangle);
  66. }
  67. }
  68. /// <summary>
  69. /// Creates an exact copy of the region.
  70. /// </summary>
  71. /// <returns>A new <see cref="Region"/> that is a copy of this instance.</returns>
  72. public Region Clone ()
  73. {
  74. lock (_syncLock)
  75. {
  76. var clone = new Region ();
  77. clone._rectangles.Capacity = _rectangles.Count; // Pre-allocate capacity
  78. clone._rectangles.AddRange (_rectangles);
  79. return clone;
  80. }
  81. }
  82. /// <summary>
  83. /// Combines <paramref name="rectangle"/> with the region using the specified operation.
  84. /// </summary>
  85. /// <param name="rectangle">The rectangle to combine.</param>
  86. /// <param name="operation">The operation to perform.</param>
  87. public void Combine (Rectangle rectangle, RegionOp operation)
  88. {
  89. lock (_syncLock)
  90. {
  91. if (rectangle.IsEmpty && operation != RegionOp.Replace)
  92. {
  93. if (operation == RegionOp.Intersect)
  94. {
  95. _rectangles.Clear ();
  96. }
  97. return;
  98. }
  99. Combine (new Region (rectangle), operation);
  100. }
  101. }
  102. /// <summary>
  103. /// Combines <paramref name="region"/> with the region using the specified operation.
  104. /// </summary>
  105. /// <param name="region">The region to combine.</param>
  106. /// <param name="operation">The operation to perform.</param>
  107. public void Combine (Region? region, RegionOp operation)
  108. {
  109. lock (_syncLock)
  110. {
  111. CombineInternal(region, operation);
  112. }
  113. }
  114. // Private method to implement the combine logic within a lock
  115. private void CombineInternal(Region? region, RegionOp operation)
  116. {
  117. if (region is null || region._rectangles.Count == 0)
  118. {
  119. if (operation is RegionOp.Intersect or RegionOp.Replace)
  120. {
  121. _rectangles.Clear ();
  122. }
  123. return;
  124. }
  125. switch (operation)
  126. {
  127. case RegionOp.Difference:
  128. // region is regionB
  129. // We'll chain the difference: (regionA - rect1) - rect2 - rect3 ...
  130. List<Rectangle> newRectangles = new (_rectangles);
  131. foreach (Rectangle rect in region._rectangles)
  132. {
  133. List<Rectangle> temp = new ();
  134. foreach (Rectangle r in newRectangles)
  135. {
  136. temp.AddRange (SubtractRectangle (r, rect));
  137. }
  138. newRectangles = temp;
  139. }
  140. _rectangles.Clear ();
  141. _rectangles.AddRange (newRectangles);
  142. break;
  143. case RegionOp.Intersect:
  144. List<Rectangle> intersections = new (_rectangles.Count); // Pre-allocate
  145. // Null is same as empty region
  146. region ??= new ();
  147. foreach (Rectangle rect1 in _rectangles)
  148. {
  149. foreach (Rectangle rect2 in region!._rectangles)
  150. {
  151. Rectangle intersected = Rectangle.Intersect (rect1, rect2);
  152. if (!intersected.IsEmpty)
  153. {
  154. intersections.Add (intersected);
  155. }
  156. }
  157. }
  158. _rectangles.Clear ();
  159. _rectangles.AddRange (intersections);
  160. break;
  161. case RegionOp.Union:
  162. // Avoid collection initialization with spread operator
  163. _tempRectangles.Clear();
  164. _tempRectangles.AddRange(_rectangles);
  165. if (region != null)
  166. {
  167. // Get the region's rectangles safely
  168. lock (region._syncLock)
  169. {
  170. _tempRectangles.AddRange(region._rectangles);
  171. }
  172. }
  173. List<Rectangle> mergedUnion = MergeRectangles(_tempRectangles, false);
  174. _rectangles.Clear();
  175. _rectangles.AddRange(mergedUnion);
  176. break;
  177. case RegionOp.MinimalUnion:
  178. // Avoid collection initialization with spread operator
  179. _tempRectangles.Clear();
  180. _tempRectangles.AddRange(_rectangles);
  181. if (region != null)
  182. {
  183. // Get the region's rectangles safely
  184. lock (region._syncLock)
  185. {
  186. _tempRectangles.AddRange(region._rectangles);
  187. }
  188. }
  189. List<Rectangle> mergedMinimalUnion = MergeRectangles(_tempRectangles, true);
  190. _rectangles.Clear();
  191. _rectangles.AddRange(mergedMinimalUnion);
  192. break;
  193. case RegionOp.XOR:
  194. Exclude (region);
  195. region.Combine (this, RegionOp.Difference);
  196. _rectangles.AddRange (region._rectangles);
  197. break;
  198. case RegionOp.ReverseDifference:
  199. region.Combine (this, RegionOp.Difference);
  200. _rectangles.Clear ();
  201. _rectangles.AddRange (region._rectangles);
  202. break;
  203. case RegionOp.Replace:
  204. _rectangles.Clear ();
  205. _rectangles.Capacity = region._rectangles.Count; // Pre-allocate
  206. _rectangles.AddRange (region._rectangles);
  207. break;
  208. }
  209. }
  210. /// <summary>
  211. /// Updates the region to be the complement of itself within the specified bounds.
  212. /// </summary>
  213. /// <param name="bounds">The bounding rectangle to use for complementing the region.</param>
  214. public void Complement (Rectangle bounds)
  215. {
  216. if (bounds.IsEmpty || _rectangles.Count == 0)
  217. {
  218. _rectangles.Clear ();
  219. return;
  220. }
  221. List<Rectangle> complementRectangles = new (4) { bounds }; // Typical max initial capacity
  222. foreach (Rectangle rect in _rectangles)
  223. {
  224. complementRectangles = complementRectangles.SelectMany (r => SubtractRectangle (r, rect)).ToList ();
  225. }
  226. _rectangles.Clear ();
  227. _rectangles.AddRange (complementRectangles);
  228. }
  229. /// <summary>
  230. /// Determines whether the specified point is contained within the region.
  231. /// </summary>
  232. /// <param name="x">The x-coordinate of the point.</param>
  233. /// <param name="y">The y-coordinate of the point.</param>
  234. /// <returns><c>true</c> if the point is contained within the region; otherwise, <c>false</c>.</returns>
  235. public bool Contains (int x, int y)
  236. {
  237. lock (_syncLock)
  238. {
  239. foreach (Rectangle r in _rectangles)
  240. {
  241. if (r.Contains (x, y))
  242. {
  243. return true;
  244. }
  245. }
  246. return false;
  247. }
  248. }
  249. /// <summary>
  250. /// Determines whether the specified rectangle is contained within the region.
  251. /// </summary>
  252. /// <param name="rectangle">The rectangle to check for containment.</param>
  253. /// <returns><c>true</c> if the rectangle is contained within the region; otherwise, <c>false</c>.</returns>
  254. public bool Contains (Rectangle rectangle)
  255. {
  256. lock (_syncLock)
  257. {
  258. foreach (Rectangle r in _rectangles)
  259. {
  260. if (r.Contains (rectangle))
  261. {
  262. return true;
  263. }
  264. }
  265. return false;
  266. }
  267. }
  268. /// <summary>
  269. /// Determines whether the specified object is equal to this region.
  270. /// </summary>
  271. /// <param name="obj">The object to compare with this region.</param>
  272. /// <returns><c>true</c> if the objects are equal; otherwise, <c>false</c>.</returns>
  273. public override bool Equals (object? obj) { return obj is Region other && Equals (other); }
  274. private static bool IsRegionEmpty (List<Rectangle> rectangles)
  275. {
  276. if (rectangles.Count == 0)
  277. {
  278. return true;
  279. }
  280. foreach (Rectangle r in rectangles)
  281. {
  282. if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
  283. {
  284. return false;
  285. }
  286. }
  287. return true;
  288. }
  289. /// <summary>
  290. /// Determines whether the specified region is equal to this region.
  291. /// </summary>
  292. /// <param name="other">The region to compare with this region.</param>
  293. /// <returns><c>true</c> if the regions are equal; otherwise, <c>false</c>.</returns>
  294. public bool Equals (Region? other)
  295. {
  296. if (other is null)
  297. {
  298. return false;
  299. }
  300. if (ReferenceEquals (this, other))
  301. {
  302. return true;
  303. }
  304. // Check if either region is empty
  305. bool thisEmpty = IsRegionEmpty (_rectangles);
  306. bool otherEmpty = IsRegionEmpty (other._rectangles);
  307. // If either is empty, they're equal only if both are empty
  308. if (thisEmpty || otherEmpty)
  309. {
  310. return thisEmpty == otherEmpty;
  311. }
  312. // For non-empty regions, compare rectangle counts
  313. if (_rectangles.Count != other._rectangles.Count)
  314. {
  315. return false;
  316. }
  317. // Compare all rectangles - order matters since we maintain canonical form
  318. for (var i = 0; i < _rectangles.Count; i++)
  319. {
  320. if (!_rectangles [i].Equals (other._rectangles [i]))
  321. {
  322. return false;
  323. }
  324. }
  325. return true;
  326. }
  327. /// <summary>
  328. /// Removes the specified rectangle from the region.
  329. /// </summary>
  330. /// <remarks>
  331. /// <para>
  332. /// This is a helper method that is equivalent to calling <see cref="Combine(Rectangle,RegionOp)"/> with
  333. /// <see cref="RegionOp.Difference"/>.
  334. /// </para>
  335. /// </remarks>
  336. /// <param name="rectangle">The rectangle to exclude from the region.</param>
  337. public void Exclude (Rectangle rectangle) { Combine (rectangle, RegionOp.Difference); }
  338. /// <summary>
  339. /// Removes the portion of the specified region from this region.
  340. /// </summary>
  341. /// <remarks>
  342. /// <para>
  343. /// This is a helper method that is equivalent to calling <see cref="Combine(Region,RegionOp)"/> with
  344. /// <see cref="RegionOp.Difference"/>.
  345. /// </para>
  346. /// </remarks>
  347. /// <param name="region">The region to exclude from this region.</param>
  348. public void Exclude (Region? region) { Combine (region, RegionOp.Difference); }
  349. /// <summary>
  350. /// Gets a bounding rectangle for the entire region.
  351. /// </summary>
  352. /// <returns>A <see cref="Rectangle"/> that bounds the region.</returns>
  353. public Rectangle GetBounds ()
  354. {
  355. if (_rectangles.Count == 0)
  356. {
  357. return Rectangle.Empty;
  358. }
  359. Rectangle first = _rectangles [0];
  360. int left = first.Left;
  361. int top = first.Top;
  362. int right = first.Right;
  363. int bottom = first.Bottom;
  364. for (var i = 1; i < _rectangles.Count; i++)
  365. {
  366. Rectangle r = _rectangles [i];
  367. left = Math.Min (left, r.Left);
  368. top = Math.Min (top, r.Top);
  369. right = Math.Max (right, r.Right);
  370. bottom = Math.Max (bottom, r.Bottom);
  371. }
  372. return new (left, top, right - left, bottom - top);
  373. }
  374. /// <summary>
  375. /// Returns a hash code for this region.
  376. /// </summary>
  377. /// <returns>A hash code for this region.</returns>
  378. public override int GetHashCode ()
  379. {
  380. var hash = new HashCode ();
  381. foreach (Rectangle rect in _rectangles)
  382. {
  383. hash.Add (rect);
  384. }
  385. return hash.ToHashCode ();
  386. }
  387. /// <summary>
  388. /// Returns an array of rectangles that represent the region.
  389. /// </summary>
  390. /// <returns>An array of <see cref="Rectangle"/> objects that make up the region.</returns>
  391. public Rectangle [] GetRectangles () { return _rectangles.ToArray (); }
  392. /// <summary>
  393. /// Updates the region to be the intersection of itself with the specified rectangle.
  394. /// </summary>
  395. /// <remarks>
  396. /// <para>
  397. /// This is a helper method that is equivalent to calling <see cref="Combine(Rectangle,RegionOp)"/> with
  398. /// <see cref="RegionOp.Intersect"/>.
  399. /// </para>
  400. /// </remarks>
  401. /// <param name="rectangle">The rectangle to intersect with the region.</param>
  402. public void Intersect (Rectangle rectangle) { Combine (rectangle, RegionOp.Intersect); }
  403. /// <summary>
  404. /// Updates the region to be the intersection of itself with the specified region.
  405. /// </summary>
  406. /// <remarks>
  407. /// <para>
  408. /// This is a helper method that is equivalent to calling <see cref="Combine(Region,RegionOp)"/> with
  409. /// <see cref="RegionOp.Intersect"/>.
  410. /// </para>
  411. /// </remarks>
  412. /// <param name="region">The region to intersect with this region.</param>
  413. public void Intersect (Region? region) { Combine (region, RegionOp.Intersect); }
  414. /// <summary>
  415. /// Determines whether the region is empty.
  416. /// </summary>
  417. /// <returns><c>true</c> if the region is empty; otherwise, <c>false</c>.</returns>
  418. public bool IsEmpty ()
  419. {
  420. if (_rectangles.Count == 0)
  421. {
  422. return true;
  423. }
  424. foreach (Rectangle r in _rectangles)
  425. {
  426. if (r is { IsEmpty: false, Width: > 0, Height: > 0 })
  427. {
  428. return false;
  429. }
  430. }
  431. return true;
  432. }
  433. /// <summary>
  434. /// Translates all rectangles in the region by the specified offsets.
  435. /// </summary>
  436. /// <param name="offsetX">The amount to offset along the x-axis.</param>
  437. /// <param name="offsetY">The amount to offset along the y-axis.</param>
  438. public void Translate (int offsetX, int offsetY)
  439. {
  440. if (offsetX == 0 && offsetY == 0)
  441. {
  442. return;
  443. }
  444. for (var i = 0; i < _rectangles.Count; i++)
  445. {
  446. Rectangle rect = _rectangles [i];
  447. _rectangles [i] = rect with { X = rect.Left + offsetX, Y = rect.Top + offsetY };
  448. }
  449. }
  450. /// <summary>
  451. /// Adds the specified rectangle to the region. Merges all rectangles into a minimal or granular bounding shape.
  452. /// </summary>
  453. /// <param name="rectangle">The rectangle to add to the region.</param>
  454. public void Union (Rectangle rectangle) { Combine (rectangle, RegionOp.Union); }
  455. /// <summary>
  456. /// Adds the specified region to this region. Merges all rectangles into a minimal or granular bounding shape.
  457. /// </summary>
  458. /// <param name="region">The region to add to this region.</param>
  459. public void Union (Region? region) { Combine (region, RegionOp.Union); }
  460. /// <summary>
  461. /// Adds the specified rectangle to the region. Merges all rectangles into the smallest possible bounding shape.
  462. /// </summary>
  463. /// <param name="rectangle">The rectangle to add to the region.</param>
  464. public void MinimalUnion (Rectangle rectangle) { Combine (rectangle, RegionOp.MinimalUnion); }
  465. /// <summary>
  466. /// Adds the specified region to this region. Merges all rectangles into the smallest possible bounding shape.
  467. /// </summary>
  468. /// <param name="region">The region to add to this region.</param>
  469. public void MinimalUnion (Region? region) { Combine (region, RegionOp.MinimalUnion); }
  470. /// <summary>
  471. /// Merges overlapping rectangles into a minimal or granular set of non-overlapping rectangles with a minimal bounding
  472. /// shape.
  473. /// </summary>
  474. /// <param name="rectangles">The list of rectangles to merge.</param>
  475. /// <param name="minimize">
  476. /// If <c>true</c>, minimizes the set to the smallest possible number of rectangles; otherwise,
  477. /// returns a granular set.
  478. /// </param>
  479. /// <returns>A list of merged rectangles.</returns>
  480. internal static List<Rectangle> MergeRectangles (List<Rectangle> rectangles, bool minimize)
  481. {
  482. if (rectangles.Count == 0)
  483. {
  484. return [];
  485. }
  486. // Sweep-line algorithm to merge rectangles
  487. List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2); // Pre-allocate
  488. foreach (Rectangle r in rectangles)
  489. {
  490. if (!r.IsEmpty)
  491. {
  492. events.Add ((r.Left, true, r.Top, r.Bottom)); // Start event
  493. events.Add ((r.Right, false, r.Top, r.Bottom)); // End event
  494. }
  495. }
  496. if (events.Count == 0)
  497. {
  498. return []; // Return empty list if no non-empty rectangles exist
  499. }
  500. events.Sort (
  501. (a, b) =>
  502. {
  503. int cmp = a.x.CompareTo (b.x);
  504. if (cmp != 0)
  505. {
  506. return cmp;
  507. }
  508. return a.isStart.CompareTo (b.isStart); // Start events before end events at same x
  509. });
  510. List<Rectangle> merged = [];
  511. SortedSet<(int yTop, int yBottom)> active = new (
  512. Comparer<(int yTop, int yBottom)>.Create (
  513. (a, b) =>
  514. {
  515. int cmp = a.yTop.CompareTo (b.yTop);
  516. return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
  517. }));
  518. int lastX = events [0].x;
  519. foreach ((int x, bool isStart, int yTop, int yBottom) evt in events)
  520. {
  521. // Output rectangles for the previous segment if there are active rectangles
  522. if (active.Count > 0 && evt.x > lastX)
  523. {
  524. merged.AddRange (MergeVerticalIntervals (active, lastX, evt.x));
  525. }
  526. // Process the event
  527. if (evt.isStart)
  528. {
  529. active.Add ((evt.yTop, evt.yBottom));
  530. }
  531. else
  532. {
  533. active.Remove ((evt.yTop, evt.yBottom));
  534. }
  535. lastX = evt.x;
  536. }
  537. return minimize ? MinimizeRectangles (merged) : merged;
  538. }
  539. /// <summary>
  540. /// Merges overlapping vertical intervals into a minimal set of non-overlapping rectangles.
  541. /// </summary>
  542. /// <param name="active">The set of active vertical intervals.</param>
  543. /// <param name="startX">The starting x-coordinate for the rectangles.</param>
  544. /// <param name="endX">The ending x-coordinate for the rectangles.</param>
  545. /// <returns>A list of merged rectangles.</returns>
  546. internal static List<Rectangle> MergeVerticalIntervals (SortedSet<(int yTop, int yBottom)> active, int startX, int endX)
  547. {
  548. if (active.Count == 0)
  549. {
  550. return [];
  551. }
  552. List<Rectangle> result = new (active.Count); // Pre-allocate
  553. int? currentTop = null;
  554. int? currentBottom = null;
  555. foreach ((int yTop, int yBottom) in active)
  556. {
  557. if (currentTop == null)
  558. {
  559. currentTop = yTop;
  560. currentBottom = yBottom;
  561. }
  562. else if (yTop <= currentBottom)
  563. {
  564. currentBottom = Math.Max (currentBottom.Value, yBottom);
  565. }
  566. else
  567. {
  568. result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value));
  569. currentTop = yTop;
  570. currentBottom = yBottom;
  571. }
  572. }
  573. if (currentTop != null)
  574. {
  575. result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value));
  576. }
  577. return result;
  578. }
  579. /// <summary>
  580. /// Minimizes a list of rectangles into the smallest possible set of non-overlapping rectangles
  581. /// by merging adjacent rectangles where possible.
  582. /// </summary>
  583. /// <param name="rectangles">The list of rectangles to minimize.</param>
  584. /// <returns>A list of minimized rectangles.</returns>
  585. internal static List<Rectangle> MinimizeRectangles (List<Rectangle> rectangles)
  586. {
  587. if (rectangles.Count <= 1)
  588. {
  589. return rectangles.ToList ();
  590. }
  591. List<Rectangle> minimized = new (rectangles.Count); // Pre-allocate
  592. List<Rectangle> current = new (rectangles); // Work with a copy
  593. bool changed;
  594. do
  595. {
  596. changed = false;
  597. minimized.Clear ();
  598. // Sort by Y then X for consistent processing
  599. current.Sort (
  600. (a, b) =>
  601. {
  602. int cmp = a.Top.CompareTo (b.Top);
  603. return cmp != 0 ? cmp : a.Left.CompareTo (b.Left);
  604. });
  605. var i = 0;
  606. while (i < current.Count)
  607. {
  608. Rectangle r = current [i];
  609. int j = i + 1;
  610. while (j < current.Count)
  611. {
  612. Rectangle next = current [j];
  613. // Check if rectangles can be merged horizontally (same Y range, adjacent X)
  614. if (r.Top == next.Top && r.Bottom == next.Bottom && (r.Right == next.Left || next.Right == r.Left || r.IntersectsWith (next)))
  615. {
  616. r = new (
  617. Math.Min (r.Left, next.Left),
  618. r.Top,
  619. Math.Max (r.Right, next.Right) - Math.Min (r.Left, next.Left),
  620. r.Height
  621. );
  622. current.RemoveAt (j);
  623. changed = true;
  624. }
  625. // Check if rectangles can be merged vertically (same X range, adjacent Y)
  626. else if (r.Left == next.Left && r.Right == next.Right && (r.Bottom == next.Top || next.Bottom == r.Top || r.IntersectsWith (next)))
  627. {
  628. r = new (
  629. r.Left,
  630. Math.Min (r.Top, next.Top),
  631. r.Width,
  632. Math.Max (r.Bottom, next.Bottom) - Math.Min (r.Top, next.Top)
  633. );
  634. current.RemoveAt (j);
  635. changed = true;
  636. }
  637. else
  638. {
  639. j++;
  640. }
  641. }
  642. minimized.Add (r);
  643. i++;
  644. }
  645. current = minimized.ToList (); // Prepare for next iteration
  646. }
  647. while (changed);
  648. return minimized;
  649. }
  650. /// <summary>
  651. /// Subtracts the specified rectangle from the original rectangle, returning the resulting rectangles.
  652. /// </summary>
  653. /// <param name="original">The original rectangle.</param>
  654. /// <param name="subtract">The rectangle to subtract from the original.</param>
  655. /// <returns>An enumerable collection of resulting rectangles after subtraction.</returns>
  656. internal static IEnumerable<Rectangle> SubtractRectangle (Rectangle original, Rectangle subtract)
  657. {
  658. // Handle empty or invalid rectangles
  659. if (original.IsEmpty || original.Width <= 0 || original.Height <= 0)
  660. {
  661. yield break; // Return empty enumeration for empty or invalid original
  662. }
  663. if (subtract.IsEmpty || subtract.Width <= 0 || subtract.Height <= 0)
  664. {
  665. yield return original;
  666. yield break;
  667. }
  668. // Check for complete overlap (subtract fully contains or equals original)
  669. if (subtract.Left <= original.Left && subtract.Top <= original.Top && subtract.Right >= original.Right && subtract.Bottom >= original.Bottom)
  670. {
  671. yield break; // Return empty if subtract completely overlaps original
  672. }
  673. // Check for no overlap
  674. if (!original.IntersectsWith (subtract))
  675. {
  676. yield return original;
  677. yield break;
  678. }
  679. // Fragment the original rectangle into segments excluding the subtract rectangle
  680. // Top segment (above subtract)
  681. if (original.Top < subtract.Top)
  682. {
  683. yield return new (
  684. original.Left,
  685. original.Top,
  686. original.Width,
  687. subtract.Top - original.Top);
  688. }
  689. // Bottom segment (below subtract)
  690. if (original.Bottom > subtract.Bottom)
  691. {
  692. yield return new (
  693. original.Left,
  694. subtract.Bottom,
  695. original.Width,
  696. original.Bottom - subtract.Bottom);
  697. }
  698. // Left segment (to the left of subtract)
  699. if (original.Left < subtract.Left)
  700. {
  701. int top = Math.Max (original.Top, subtract.Top);
  702. int bottom = Math.Min (original.Bottom, subtract.Bottom);
  703. if (bottom > top)
  704. {
  705. yield return new (
  706. original.Left,
  707. top,
  708. subtract.Left - original.Left,
  709. bottom - top);
  710. }
  711. }
  712. // Right segment (to the right of subtract)
  713. if (original.Right > subtract.Right)
  714. {
  715. int top = Math.Max (original.Top, subtract.Top);
  716. int bottom = Math.Min (original.Bottom, subtract.Bottom);
  717. if (bottom > top)
  718. {
  719. yield return new (
  720. subtract.Right,
  721. top,
  722. original.Right - subtract.Right,
  723. bottom - top);
  724. }
  725. }
  726. }
  727. /// <summary>
  728. /// Fills the interior of all rectangles in the region with the specified attribute and fill rune.
  729. /// </summary>
  730. /// <param name="attribute">The attribute (color/style) to use.</param>
  731. /// <param name="fillRune">
  732. /// The rune to fill the interior of the rectangles with. If <cref langword="null"/> space will be
  733. /// used.
  734. /// </param>
  735. public void FillRectangles (Attribute attribute, Rune? fillRune = null)
  736. {
  737. if (_rectangles.Count == 0)
  738. {
  739. return;
  740. }
  741. foreach (Rectangle rect in _rectangles)
  742. {
  743. if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
  744. {
  745. continue;
  746. }
  747. Application.Driver?.SetAttribute (attribute);
  748. for (int y = rect.Top; y < rect.Bottom; y++)
  749. {
  750. for (int x = rect.Left; x < rect.Right; x++)
  751. {
  752. Application.Driver?.Move (x, y);
  753. Application.Driver?.AddRune (fillRune ?? (Rune)' ');
  754. }
  755. }
  756. }
  757. }
  758. /// <summary>
  759. /// Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big
  760. /// enough.
  761. /// </summary>
  762. /// <param name="canvas">The canvas to draw on.</param>
  763. /// <param name="style">The line style to use for drawing.</param>
  764. /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
  765. public void DrawBoundaries (LineCanvas canvas, LineStyle style, Attribute? attribute = null)
  766. {
  767. if (_rectangles.Count == 0)
  768. {
  769. return;
  770. }
  771. foreach (Rectangle rect in _rectangles)
  772. {
  773. if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
  774. {
  775. continue;
  776. }
  777. // Only draw boundaries if the rectangle is "big enough" (e.g., width and height > 1)
  778. //if (rect.Width > 2 && rect.Height > 2)
  779. {
  780. if (rect.Width > 1)
  781. {
  782. // Add horizontal lines
  783. canvas.AddLine (new (rect.Left, rect.Top), rect.Width, Orientation.Horizontal, style, attribute);
  784. canvas.AddLine (new (rect.Left, rect.Bottom - 1), rect.Width, Orientation.Horizontal, style, attribute);
  785. }
  786. if (rect.Height > 1)
  787. {
  788. // Add vertical lines
  789. canvas.AddLine (new (rect.Left, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
  790. canvas.AddLine (new (rect.Right - 1, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
  791. }
  792. }
  793. }
  794. }
  795. // 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.
  796. //
  797. // Example: There are 3 regions here. the first is a rect (0,0,1,4). Second is (10, 0, 2, 4).
  798. // This is how they should draw:
  799. //
  800. // |123456789|123456789|123456789
  801. // 1 │ ┌┐ ┌─┐
  802. // 2 │ ││ │ │
  803. // 3 │ ││ │ │
  804. // 4 │ └┘ └─┘
  805. //
  806. // But this is what it draws:
  807. // |123456789|123456789|123456789
  808. // 1┌┐ ┌─┐ ┌──┐
  809. // 2││ │ │ │ │
  810. // 3││ │ │ │ │
  811. // 4││ │ │ │ │
  812. // 5└┘ └─┘ └──┘
  813. //
  814. // Example: There are two rectangles in this region. (0,0,3,3) and (3, 3, 3, 3).
  815. // This is fill - correct:
  816. // |123456789
  817. // 1░░░
  818. // 2░░░
  819. // 3░░░░░
  820. // 4 ░░░
  821. // 5 ░░░
  822. // 6
  823. //
  824. // This is what DrawOuterBoundary should draw
  825. // |123456789|123456789
  826. // 1┌─┐
  827. // 2│ │
  828. // 3└─┼─┐
  829. // 4 │ │
  830. // 5 └─┘
  831. // 6
  832. //
  833. // This is what DrawOuterBoundary actually draws
  834. // |123456789|123456789
  835. // 1┌──┐
  836. // 2│ │
  837. // 3│ └─┐
  838. // 4└─┐ │
  839. // 5 │ │
  840. // 6 └──┘
  841. /// <summary>
  842. /// Draws the outer perimeter of the region to <paramref name="lineCanvas"/> using <paramref name="style"/> and
  843. /// <paramref name="attribute"/>.
  844. /// The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing
  845. /// boundaries and excluding internal lines.
  846. /// </summary>
  847. /// <param name="lineCanvas">The LineCanvas to draw on.</param>
  848. /// <param name="style">The line style to use for drawing.</param>
  849. /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
  850. public void DrawOuterBoundary (LineCanvas lineCanvas, LineStyle style, Attribute? attribute = null)
  851. {
  852. if (_rectangles.Count == 0)
  853. {
  854. return;
  855. }
  856. // Get the bounds of the region
  857. Rectangle bounds = GetBounds ();
  858. // Add protection against extremely large allocations
  859. if (bounds.Width > 1000 || bounds.Height > 1000)
  860. {
  861. // Fall back to drawing each rectangle's boundary
  862. DrawBoundaries(lineCanvas, style, attribute);
  863. return;
  864. }
  865. // Create a grid to track which cells are inside the region
  866. var insideRegion = new bool [bounds.Width + 1, bounds.Height + 1];
  867. // Fill the grid based on rectangles
  868. foreach (Rectangle rect in _rectangles)
  869. {
  870. if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
  871. {
  872. continue;
  873. }
  874. for (int x = rect.Left; x < rect.Right; x++)
  875. {
  876. for (int y = rect.Top; y < rect.Bottom; y++)
  877. {
  878. // Adjust coordinates to grid space
  879. int gridX = x - bounds.Left;
  880. int gridY = y - bounds.Top;
  881. if (gridX >= 0 && gridX < bounds.Width && gridY >= 0 && gridY < bounds.Height)
  882. {
  883. insideRegion [gridX, gridY] = true;
  884. }
  885. }
  886. }
  887. }
  888. // Find horizontal boundary lines
  889. for (var y = 0; y <= bounds.Height; y++)
  890. {
  891. int startX = -1;
  892. for (var x = 0; x <= bounds.Width; x++)
  893. {
  894. bool above = y > 0 && insideRegion [x, y - 1];
  895. bool below = y < bounds.Height && insideRegion [x, y];
  896. // A boundary exists where one side is inside and the other is outside
  897. bool isBoundary = above != below;
  898. if (isBoundary)
  899. {
  900. // Start a new segment or continue the current one
  901. if (startX == -1)
  902. {
  903. startX = x;
  904. }
  905. }
  906. else
  907. {
  908. // End the current segment if one exists
  909. if (startX != -1)
  910. {
  911. int length = x - startX + 1; // Add 1 to make sure lines connect
  912. lineCanvas.AddLine (
  913. new (startX + bounds.Left, y + bounds.Top),
  914. length,
  915. Orientation.Horizontal,
  916. style,
  917. attribute
  918. );
  919. startX = -1;
  920. }
  921. }
  922. }
  923. // End any segment that reaches the right edge
  924. if (startX != -1)
  925. {
  926. int length = bounds.Width + 1 - startX + 1; // Add 1 to make sure lines connect
  927. lineCanvas.AddLine (
  928. new (startX + bounds.Left, y + bounds.Top),
  929. length,
  930. Orientation.Horizontal,
  931. style,
  932. attribute
  933. );
  934. }
  935. }
  936. // Find vertical boundary lines
  937. for (var x = 0; x <= bounds.Width; x++)
  938. {
  939. int startY = -1;
  940. for (var y = 0; y <= bounds.Height; y++)
  941. {
  942. bool left = x > 0 && insideRegion [x - 1, y];
  943. bool right = x < bounds.Width && insideRegion [x, y];
  944. // A boundary exists where one side is inside and the other is outside
  945. bool isBoundary = left != right;
  946. if (isBoundary)
  947. {
  948. // Start a new segment or continue the current one
  949. if (startY == -1)
  950. {
  951. startY = y;
  952. }
  953. }
  954. else
  955. {
  956. // End the current segment if one exists
  957. if (startY != -1)
  958. {
  959. int length = y - startY + 1; // Add 1 to make sure lines connect
  960. lineCanvas.AddLine (
  961. new (x + bounds.Left, startY + bounds.Top),
  962. length,
  963. Orientation.Vertical,
  964. style,
  965. attribute
  966. );
  967. startY = -1;
  968. }
  969. }
  970. }
  971. // End any segment that reaches the bottom edge
  972. if (startY != -1)
  973. {
  974. int length = bounds.Height + 1 - startY + 1; // Add 1 to make sure lines connect
  975. lineCanvas.AddLine (
  976. new (x + bounds.Left, startY + bounds.Top),
  977. length,
  978. Orientation.Vertical,
  979. style,
  980. attribute
  981. );
  982. }
  983. }
  984. }
  985. }