Region.cs 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190
  1. #nullable enable
  2. namespace Terminal.Gui.Drawing;
  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 <= 1)
  483. {
  484. return rectangles.ToList ();
  485. }
  486. // Generate events
  487. List<(int x, bool isStart, int yTop, int yBottom)> events = new (rectangles.Count * 2);
  488. foreach (Rectangle r in rectangles)
  489. {
  490. if (!r.IsEmpty)
  491. {
  492. events.Add ((r.Left, true, r.Top, r.Bottom));
  493. events.Add ((r.Right, false, r.Top, r.Bottom));
  494. }
  495. }
  496. if (events.Count == 0)
  497. {
  498. return [];
  499. }
  500. // Sort events:
  501. // 1. Primarily by x-coordinate.
  502. // 2. Secondary: End events before Start events at the same x.
  503. // 3. Tertiary: By yTop coordinate as a tie-breaker.
  504. // 4. Quaternary: By yBottom coordinate as a final tie-breaker.
  505. events.Sort (
  506. (a, b) =>
  507. {
  508. // 1. Sort by X
  509. int cmp = a.x.CompareTo (b.x);
  510. if (cmp != 0) return cmp;
  511. // 2. Sort End events before Start events
  512. bool aIsEnd = !a.isStart;
  513. bool bIsEnd = !b.isStart;
  514. cmp = aIsEnd.CompareTo (bIsEnd); // True (End) comes after False (Start)
  515. if (cmp != 0) return -cmp; // Reverse: End (true) should come before Start (false)
  516. // 3. Tie-breaker: Sort by yTop
  517. cmp = a.yTop.CompareTo (b.yTop);
  518. if (cmp != 0) return cmp;
  519. // 4. Final Tie-breaker: Sort by yBottom
  520. return a.yBottom.CompareTo (b.yBottom);
  521. });
  522. List<Rectangle> merged = [];
  523. // Use a dictionary to track active intervals and their overlap counts
  524. Dictionary<(int yTop, int yBottom), int> activeCounts = new ();
  525. // Comparer for sorting intervals when needed
  526. var intervalComparer = Comparer<(int yTop, int yBottom)>.Create (
  527. (a, b) =>
  528. {
  529. int cmp = a.yTop.CompareTo (b.yTop);
  530. return cmp != 0 ? cmp : a.yBottom.CompareTo (b.yBottom);
  531. });
  532. // Helper to get the current active intervals (where count > 0) as a SortedSet
  533. SortedSet<(int yTop, int yBottom)> GetActiveIntervals ()
  534. {
  535. var set = new SortedSet<(int yTop, int yBottom)> (intervalComparer);
  536. foreach (var kvp in activeCounts)
  537. {
  538. if (kvp.Value > 0)
  539. {
  540. set.Add (kvp.Key);
  541. }
  542. }
  543. return set;
  544. }
  545. // Group events by x-coordinate to process all events at a given x together
  546. var groupedEvents = events.GroupBy (e => e.x).OrderBy (g => g.Key);
  547. int lastX = groupedEvents.First ().Key; // Initialize with the first event's x
  548. foreach (var group in groupedEvents)
  549. {
  550. int currentX = group.Key;
  551. // Get active intervals based on state *before* processing events at currentX
  552. var currentActiveIntervals = GetActiveIntervals ();
  553. // 1. Output rectangles for the segment ending *before* this x coordinate
  554. if (currentX > lastX && currentActiveIntervals.Count > 0)
  555. {
  556. merged.AddRange (MergeVerticalIntervals (currentActiveIntervals, lastX, currentX));
  557. }
  558. // 2. Process all events *at* this x coordinate to update counts
  559. foreach (var evt in group)
  560. {
  561. var interval = (evt.yTop, evt.yBottom);
  562. if (evt.isStart)
  563. {
  564. activeCounts.TryGetValue (interval, out int count);
  565. activeCounts [interval] = count + 1;
  566. }
  567. else
  568. {
  569. // Only decrement/remove if the interval exists
  570. if (activeCounts.TryGetValue (interval, out int count))
  571. {
  572. if (count - 1 <= 0)
  573. {
  574. activeCounts.Remove (interval);
  575. }
  576. else
  577. {
  578. activeCounts [interval] = count - 1;
  579. }
  580. }
  581. }
  582. }
  583. // 3. Update lastX for the next segment
  584. lastX = currentX;
  585. }
  586. return minimize ? MinimizeRectangles (merged) : merged;
  587. }
  588. /// <summary>
  589. /// Merges overlapping vertical intervals into a minimal set of non-overlapping rectangles.
  590. /// </summary>
  591. /// <param name="active">The set of active vertical intervals.</param>
  592. /// <param name="startX">The starting x-coordinate for the rectangles.</param>
  593. /// <param name="endX">The ending x-coordinate for the rectangles.</param>
  594. /// <returns>A list of merged rectangles.</returns>
  595. internal static List<Rectangle> MergeVerticalIntervals (SortedSet<(int yTop, int yBottom)> active, int startX, int endX)
  596. {
  597. if (active.Count == 0)
  598. {
  599. return [];
  600. }
  601. List<Rectangle> result = new (active.Count); // Pre-allocate
  602. int? currentTop = null;
  603. int? currentBottom = null;
  604. foreach ((int yTop, int yBottom) in active)
  605. {
  606. if (currentTop == null)
  607. {
  608. currentTop = yTop;
  609. currentBottom = yBottom;
  610. }
  611. else if (yTop <= currentBottom)
  612. {
  613. currentBottom = Math.Max (currentBottom.Value, yBottom);
  614. }
  615. else
  616. {
  617. result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value));
  618. currentTop = yTop;
  619. currentBottom = yBottom;
  620. }
  621. }
  622. if (currentTop != null)
  623. {
  624. result.Add (new (startX, currentTop.Value, endX - startX, currentBottom!.Value - currentTop.Value));
  625. }
  626. return result;
  627. }
  628. /// <summary>
  629. /// Minimizes a list of rectangles into the smallest possible set of non-overlapping rectangles
  630. /// by merging adjacent rectangles where possible.
  631. /// </summary>
  632. /// <param name="rectangles">The list of rectangles to minimize.</param>
  633. /// <returns>A list of minimized rectangles.</returns>
  634. internal static List<Rectangle> MinimizeRectangles (List<Rectangle> rectangles)
  635. {
  636. if (rectangles.Count <= 1)
  637. {
  638. return rectangles.ToList ();
  639. }
  640. List<Rectangle> minimized = new (rectangles.Count); // Pre-allocate
  641. List<Rectangle> current = new (rectangles); // Work with a copy
  642. bool changed;
  643. do
  644. {
  645. changed = false;
  646. minimized.Clear ();
  647. // Sort by Y then X for consistent processing
  648. current.Sort (
  649. (a, b) =>
  650. {
  651. int cmp = a.Top.CompareTo (b.Top);
  652. return cmp != 0 ? cmp : a.Left.CompareTo (b.Left);
  653. });
  654. var i = 0;
  655. while (i < current.Count)
  656. {
  657. Rectangle r = current [i];
  658. int j = i + 1;
  659. while (j < current.Count)
  660. {
  661. Rectangle next = current [j];
  662. // Check if rectangles can be merged horizontally (same Y range, adjacent X)
  663. if (r.Top == next.Top && r.Bottom == next.Bottom && (r.Right == next.Left || next.Right == r.Left || r.IntersectsWith (next)))
  664. {
  665. r = new (
  666. Math.Min (r.Left, next.Left),
  667. r.Top,
  668. Math.Max (r.Right, next.Right) - Math.Min (r.Left, next.Left),
  669. r.Height
  670. );
  671. current.RemoveAt (j);
  672. changed = true;
  673. }
  674. // Check if rectangles can be merged vertically (same X range, adjacent Y)
  675. else if (r.Left == next.Left && r.Right == next.Right && (r.Bottom == next.Top || next.Bottom == r.Top || r.IntersectsWith (next)))
  676. {
  677. r = new (
  678. r.Left,
  679. Math.Min (r.Top, next.Top),
  680. r.Width,
  681. Math.Max (r.Bottom, next.Bottom) - Math.Min (r.Top, next.Top)
  682. );
  683. current.RemoveAt (j);
  684. changed = true;
  685. }
  686. else
  687. {
  688. j++;
  689. }
  690. }
  691. minimized.Add (r);
  692. i++;
  693. }
  694. current = minimized.ToList (); // Prepare for next iteration
  695. }
  696. while (changed);
  697. return minimized;
  698. }
  699. /// <summary>
  700. /// Subtracts the specified rectangle from the original rectangle, returning the resulting rectangles.
  701. /// </summary>
  702. /// <param name="original">The original rectangle.</param>
  703. /// <param name="subtract">The rectangle to subtract from the original.</param>
  704. /// <returns>An enumerable collection of resulting rectangles after subtraction.</returns>
  705. internal static IEnumerable<Rectangle> SubtractRectangle (Rectangle original, Rectangle subtract)
  706. {
  707. // Handle empty or invalid rectangles
  708. if (original.IsEmpty || original.Width <= 0 || original.Height <= 0)
  709. {
  710. yield break; // Return empty enumeration for empty or invalid original
  711. }
  712. if (subtract.IsEmpty || subtract.Width <= 0 || subtract.Height <= 0)
  713. {
  714. yield return original;
  715. yield break;
  716. }
  717. // Check for complete overlap (subtract fully contains or equals original)
  718. if (subtract.Left <= original.Left && subtract.Top <= original.Top && subtract.Right >= original.Right && subtract.Bottom >= original.Bottom)
  719. {
  720. yield break; // Return empty if subtract completely overlaps original
  721. }
  722. // Check for no overlap
  723. if (!original.IntersectsWith (subtract))
  724. {
  725. yield return original;
  726. yield break;
  727. }
  728. // Fragment the original rectangle into segments excluding the subtract rectangle
  729. // Top segment (above subtract)
  730. if (original.Top < subtract.Top)
  731. {
  732. yield return new (
  733. original.Left,
  734. original.Top,
  735. original.Width,
  736. subtract.Top - original.Top);
  737. }
  738. // Bottom segment (below subtract)
  739. if (original.Bottom > subtract.Bottom)
  740. {
  741. yield return new (
  742. original.Left,
  743. subtract.Bottom,
  744. original.Width,
  745. original.Bottom - subtract.Bottom);
  746. }
  747. // Left segment (to the left of subtract)
  748. if (original.Left < subtract.Left)
  749. {
  750. int top = Math.Max (original.Top, subtract.Top);
  751. int bottom = Math.Min (original.Bottom, subtract.Bottom);
  752. if (bottom > top)
  753. {
  754. yield return new (
  755. original.Left,
  756. top,
  757. subtract.Left - original.Left,
  758. bottom - top);
  759. }
  760. }
  761. // Right segment (to the right of subtract)
  762. if (original.Right > subtract.Right)
  763. {
  764. int top = Math.Max (original.Top, subtract.Top);
  765. int bottom = Math.Min (original.Bottom, subtract.Bottom);
  766. if (bottom > top)
  767. {
  768. yield return new (
  769. subtract.Right,
  770. top,
  771. original.Right - subtract.Right,
  772. bottom - top);
  773. }
  774. }
  775. }
  776. /// <summary>
  777. /// Fills the interior of all rectangles in the region with the specified attribute and fill rune.
  778. /// </summary>
  779. /// <param name="attribute">The attribute (color/style) to use.</param>
  780. /// <param name="fillRune">
  781. /// The rune to fill the interior of the rectangles with. If <cref langword="null"/> space will be
  782. /// used.
  783. /// </param>
  784. public void FillRectangles (Attribute attribute, Rune? fillRune = null)
  785. {
  786. if (_rectangles.Count == 0)
  787. {
  788. return;
  789. }
  790. foreach (Rectangle rect in _rectangles)
  791. {
  792. if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
  793. {
  794. continue;
  795. }
  796. Application.Driver?.SetAttribute (attribute);
  797. for (int y = rect.Top; y < rect.Bottom; y++)
  798. {
  799. for (int x = rect.Left; x < rect.Right; x++)
  800. {
  801. Application.Driver?.Move (x, y);
  802. Application.Driver?.AddRune (fillRune ?? (Rune)' ');
  803. }
  804. }
  805. }
  806. }
  807. /// <summary>
  808. /// Draws the boundaries of all rectangles in the region using the specified attributes, only if the rectangle is big
  809. /// enough.
  810. /// </summary>
  811. /// <param name="canvas">The canvas to draw on.</param>
  812. /// <param name="style">The line style to use for drawing.</param>
  813. /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
  814. public void DrawBoundaries (LineCanvas canvas, LineStyle style, Attribute? attribute = null)
  815. {
  816. if (_rectangles.Count == 0)
  817. {
  818. return;
  819. }
  820. foreach (Rectangle rect in _rectangles)
  821. {
  822. if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
  823. {
  824. continue;
  825. }
  826. // Only draw boundaries if the rectangle is "big enough" (e.g., width and height > 1)
  827. //if (rect.Width > 2 && rect.Height > 2)
  828. {
  829. if (rect.Width > 1)
  830. {
  831. // Add horizontal lines
  832. canvas.AddLine (new (rect.Left, rect.Top), rect.Width, Orientation.Horizontal, style, attribute);
  833. canvas.AddLine (new (rect.Left, rect.Bottom - 1), rect.Width, Orientation.Horizontal, style, attribute);
  834. }
  835. if (rect.Height > 1)
  836. {
  837. // Add vertical lines
  838. canvas.AddLine (new (rect.Left, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
  839. canvas.AddLine (new (rect.Right - 1, rect.Top), rect.Height, Orientation.Vertical, style, attribute);
  840. }
  841. }
  842. }
  843. }
  844. // 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.
  845. //
  846. // Example: There are 3 regions here. the first is a rect (0,0,1,4). Second is (10, 0, 2, 4).
  847. // This is how they should draw:
  848. //
  849. // |123456789|123456789|123456789
  850. // 1 │ ┌┐ ┌─┐
  851. // 2 │ ││ │ │
  852. // 3 │ ││ │ │
  853. // 4 │ └┘ └─┘
  854. //
  855. // But this is what it draws:
  856. // |123456789|123456789|123456789
  857. // 1┌┐ ┌─┐ ┌──┐
  858. // 2││ │ │ │ │
  859. // 3││ │ │ │ │
  860. // 4││ │ │ │ │
  861. // 5└┘ └─┘ └──┘
  862. //
  863. // Example: There are two rectangles in this region. (0,0,3,3) and (3, 3, 3, 3).
  864. // This is fill - correct:
  865. // |123456789
  866. // 1░░░
  867. // 2░░░
  868. // 3░░░░░
  869. // 4 ░░░
  870. // 5 ░░░
  871. // 6
  872. //
  873. // This is what DrawOuterBoundary should draw
  874. // |123456789|123456789
  875. // 1┌─┐
  876. // 2│ │
  877. // 3└─┼─┐
  878. // 4 │ │
  879. // 5 └─┘
  880. // 6
  881. //
  882. // This is what DrawOuterBoundary actually draws
  883. // |123456789|123456789
  884. // 1┌──┐
  885. // 2│ │
  886. // 3│ └─┐
  887. // 4└─┐ │
  888. // 5 │ │
  889. // 6 └──┘
  890. /// <summary>
  891. /// Draws the outer perimeter of the region to <paramref name="lineCanvas"/> using <paramref name="style"/> and
  892. /// <paramref name="attribute"/>.
  893. /// The outer perimeter follows the shape of the rectangles in the region, even if non-rectangular, by drawing
  894. /// boundaries and excluding internal lines.
  895. /// </summary>
  896. /// <param name="lineCanvas">The LineCanvas to draw on.</param>
  897. /// <param name="style">The line style to use for drawing.</param>
  898. /// <param name="attribute">The attribute (color/style) to use for the lines. If <c>null</c>.</param>
  899. public void DrawOuterBoundary (LineCanvas lineCanvas, LineStyle style, Attribute? attribute = null)
  900. {
  901. if (_rectangles.Count == 0)
  902. {
  903. return;
  904. }
  905. // Get the bounds of the region
  906. Rectangle bounds = GetBounds ();
  907. // Add protection against extremely large allocations
  908. if (bounds.Width > 1000 || bounds.Height > 1000)
  909. {
  910. // Fall back to drawing each rectangle's boundary
  911. DrawBoundaries(lineCanvas, style, attribute);
  912. return;
  913. }
  914. // Create a grid to track which cells are inside the region
  915. var insideRegion = new bool [bounds.Width + 1, bounds.Height + 1];
  916. // Fill the grid based on rectangles
  917. foreach (Rectangle rect in _rectangles)
  918. {
  919. if (rect.IsEmpty || rect.Width <= 0 || rect.Height <= 0)
  920. {
  921. continue;
  922. }
  923. for (int x = rect.Left; x < rect.Right; x++)
  924. {
  925. for (int y = rect.Top; y < rect.Bottom; y++)
  926. {
  927. // Adjust coordinates to grid space
  928. int gridX = x - bounds.Left;
  929. int gridY = y - bounds.Top;
  930. if (gridX >= 0 && gridX < bounds.Width && gridY >= 0 && gridY < bounds.Height)
  931. {
  932. insideRegion [gridX, gridY] = true;
  933. }
  934. }
  935. }
  936. }
  937. // Find horizontal boundary lines
  938. for (var y = 0; y <= bounds.Height; y++)
  939. {
  940. int startX = -1;
  941. for (var x = 0; x <= bounds.Width; x++)
  942. {
  943. bool above = y > 0 && insideRegion [x, y - 1];
  944. bool below = y < bounds.Height && insideRegion [x, y];
  945. // A boundary exists where one side is inside and the other is outside
  946. bool isBoundary = above != below;
  947. if (isBoundary)
  948. {
  949. // Start a new segment or continue the current one
  950. if (startX == -1)
  951. {
  952. startX = x;
  953. }
  954. }
  955. else
  956. {
  957. // End the current segment if one exists
  958. if (startX != -1)
  959. {
  960. int length = x - startX + 1; // Add 1 to make sure lines connect
  961. lineCanvas.AddLine (
  962. new (startX + bounds.Left, y + bounds.Top),
  963. length,
  964. Orientation.Horizontal,
  965. style,
  966. attribute
  967. );
  968. startX = -1;
  969. }
  970. }
  971. }
  972. // End any segment that reaches the right edge
  973. if (startX != -1)
  974. {
  975. int length = bounds.Width + 1 - startX + 1; // Add 1 to make sure lines connect
  976. lineCanvas.AddLine (
  977. new (startX + bounds.Left, y + bounds.Top),
  978. length,
  979. Orientation.Horizontal,
  980. style,
  981. attribute
  982. );
  983. }
  984. }
  985. // Find vertical boundary lines
  986. for (var x = 0; x <= bounds.Width; x++)
  987. {
  988. int startY = -1;
  989. for (var y = 0; y <= bounds.Height; y++)
  990. {
  991. bool left = x > 0 && insideRegion [x - 1, y];
  992. bool right = x < bounds.Width && insideRegion [x, y];
  993. // A boundary exists where one side is inside and the other is outside
  994. bool isBoundary = left != right;
  995. if (isBoundary)
  996. {
  997. // Start a new segment or continue the current one
  998. if (startY == -1)
  999. {
  1000. startY = y;
  1001. }
  1002. }
  1003. else
  1004. {
  1005. // End the current segment if one exists
  1006. if (startY != -1)
  1007. {
  1008. int length = y - startY + 1; // Add 1 to make sure lines connect
  1009. lineCanvas.AddLine (
  1010. new (x + bounds.Left, startY + bounds.Top),
  1011. length,
  1012. Orientation.Vertical,
  1013. style,
  1014. attribute
  1015. );
  1016. startY = -1;
  1017. }
  1018. }
  1019. }
  1020. // End any segment that reaches the bottom edge
  1021. if (startY != -1)
  1022. {
  1023. int length = bounds.Height + 1 - startY + 1; // Add 1 to make sure lines connect
  1024. lineCanvas.AddLine (
  1025. new (x + bounds.Left, startY + bounds.Top),
  1026. length,
  1027. Orientation.Vertical,
  1028. style,
  1029. attribute
  1030. );
  1031. }
  1032. }
  1033. }
  1034. }