Region.cs 43 KB

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