LineCanvas.cs 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109
  1. using System.Runtime.InteropServices;
  2. namespace Terminal.Gui.Drawing;
  3. /// <summary>Facilitates box drawing and line intersection detection and rendering. Does not support diagonal lines.</summary>
  4. public class LineCanvas : IDisposable
  5. {
  6. /// <summary>Creates a new instance.</summary>
  7. public LineCanvas ()
  8. {
  9. // TODO: Refactor ConfigurationManager to not use an event handler for this.
  10. // Instead, have it call a method on any class appropriately attributed
  11. // to update the cached values. See Issue #2871
  12. ConfigurationManager.Applied += ConfigurationManager_Applied;
  13. }
  14. private readonly List<StraightLine> _lines = [];
  15. /// <summary>Creates a new instance with the given <paramref name="lines"/>.</summary>
  16. /// <param name="lines">Initial lines for the canvas.</param>
  17. public LineCanvas (IEnumerable<StraightLine> lines) : this () { _lines = lines.ToList (); }
  18. /// <summary>
  19. /// Optional <see cref="FillPair"/> which when present overrides the <see cref="StraightLine.Attribute"/>
  20. /// (colors) of lines in the canvas. This can be used e.g. to apply a global <see cref="GradientFill"/>
  21. /// across all lines.
  22. /// </summary>
  23. public FillPair? Fill { get; set; }
  24. private Rectangle _cachedBounds;
  25. /// <summary>
  26. /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the line that is
  27. /// the furthest left/top and Size is defined by the line that extends the furthest right/bottom.
  28. /// </summary>
  29. public Rectangle Bounds
  30. {
  31. get
  32. {
  33. if (_cachedBounds.IsEmpty)
  34. {
  35. if (_lines.Count == 0)
  36. {
  37. return _cachedBounds;
  38. }
  39. Rectangle bounds = _lines [0].Bounds;
  40. for (var i = 1; i < _lines.Count; i++)
  41. {
  42. bounds = Rectangle.Union (bounds, _lines [i].Bounds);
  43. }
  44. if (bounds is { Width: 0 } or { Height: 0 })
  45. {
  46. bounds = bounds with
  47. {
  48. Width = Math.Clamp (bounds.Width, 1, short.MaxValue),
  49. Height = Math.Clamp (bounds.Height, 1, short.MaxValue)
  50. };
  51. }
  52. _cachedBounds = bounds;
  53. }
  54. return _cachedBounds;
  55. }
  56. }
  57. /// <summary>Gets the lines in the canvas.</summary>
  58. public IReadOnlyCollection<StraightLine> Lines => _lines.AsReadOnly ();
  59. /// <summary>
  60. /// <para>Adds a new <paramref name="length"/> long line to the canvas starting at <paramref name="start"/>.</para>
  61. /// <para>
  62. /// Use positive <paramref name="length"/> for the line to extend Right and negative for Left when
  63. /// <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
  64. /// </para>
  65. /// <para>
  66. /// Use positive <paramref name="length"/> for the line to extend Down and negative for Up when
  67. /// <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
  68. /// </para>
  69. /// </summary>
  70. /// <param name="start">Starting point.</param>
  71. /// <param name="length">
  72. /// The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for
  73. /// Up/Left.
  74. /// </param>
  75. /// <param name="orientation">The direction of the line.</param>
  76. /// <param name="style">The style of line to use</param>
  77. /// <param name="attribute"></param>
  78. public void AddLine (
  79. Point start,
  80. int length,
  81. Orientation orientation,
  82. LineStyle style,
  83. Attribute? attribute = null
  84. )
  85. {
  86. _cachedBounds = Rectangle.Empty;
  87. _lines.Add (new (start, length, orientation, style, attribute));
  88. }
  89. /// <summary>Adds a new line to the canvas</summary>
  90. /// <param name="line"></param>
  91. public void AddLine (StraightLine line)
  92. {
  93. _cachedBounds = Rectangle.Empty;
  94. _lines.Add (line);
  95. }
  96. private Region? _exclusionRegion;
  97. /// <summary>
  98. /// Causes the provided region to be excluded from <see cref="GetCellMap"/> and <see cref="GetMap()"/>.
  99. /// </summary>
  100. /// <remarks>
  101. /// <para>
  102. /// Each call to this method will add to the exclusion region. To clear the exclusion region, call
  103. /// <see cref="ClearCache"/>.
  104. /// </para>
  105. /// </remarks>
  106. public void Exclude (Region region)
  107. {
  108. _exclusionRegion ??= new ();
  109. _exclusionRegion.Union (region);
  110. }
  111. /// <summary>
  112. /// Clears the exclusion region. After calling this method, <see cref="GetCellMap"/> and <see cref="GetMap()"/> will
  113. /// return all points in the canvas.
  114. /// </summary>
  115. public void ClearExclusions () { _exclusionRegion = null; }
  116. /// <summary>Clears all lines from the LineCanvas.</summary>
  117. public void Clear ()
  118. {
  119. _cachedBounds = Rectangle.Empty;
  120. _lines.Clear ();
  121. ClearExclusions ();
  122. }
  123. /// <summary>
  124. /// Clears any cached states from the canvas. Call this method if you make changes to lines that have already been
  125. /// added.
  126. /// </summary>
  127. public void ClearCache () { _cachedBounds = Rectangle.Empty; }
  128. /// <summary>
  129. /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
  130. /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
  131. /// intersection symbols.
  132. /// </summary>
  133. /// <remarks>
  134. /// <para>
  135. /// Only the points within the <see cref="Bounds"/> of the canvas that are not in the exclusion region will be
  136. /// returned. To exclude points from the map, use <see cref="Exclude"/>.
  137. /// </para>
  138. /// </remarks>
  139. /// <returns>A map of all the points within the canvas.</returns>
  140. public Dictionary<Point, Cell?> GetCellMap ()
  141. {
  142. Dictionary<Point, Cell?> map = new ();
  143. List<IntersectionDefinition> intersectionsBufferList = [];
  144. // walk through each pixel of the bitmap
  145. for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++)
  146. {
  147. for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++)
  148. {
  149. intersectionsBufferList.Clear ();
  150. foreach (var line in _lines)
  151. {
  152. if (line.Intersects (x, y) is { } intersect)
  153. {
  154. intersectionsBufferList.Add (intersect);
  155. }
  156. }
  157. // Safe as long as the list is not modified while the span is in use.
  158. ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan (intersectionsBufferList);
  159. Cell? cell = GetCellForIntersects (intersects);
  160. // TODO: Can we skip the whole nested looping if _exclusionRegion is null?
  161. if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false)
  162. {
  163. map.Add (new (x, y), cell);
  164. }
  165. }
  166. }
  167. return map;
  168. }
  169. /// <summary>
  170. /// Evaluates the lines and returns both the cell map and a Region encompassing the drawn cells.
  171. /// This is more efficient than calling <see cref="GetCellMap"/> and <see cref="GetRegion"/> separately
  172. /// as it builds both in a single pass through the canvas bounds.
  173. /// </summary>
  174. /// <returns>A tuple containing the cell map and the Region of drawn cells</returns>
  175. public (Dictionary<Point, Cell?> CellMap, Region Region) GetCellMapWithRegion ()
  176. {
  177. Dictionary<Point, Cell?> map = new ();
  178. Region region = new ();
  179. List<IntersectionDefinition> intersectionsBufferList = [];
  180. List<int> rowXValues = [];
  181. // walk through each pixel of the bitmap, row by row
  182. for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++)
  183. {
  184. rowXValues.Clear ();
  185. for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++)
  186. {
  187. intersectionsBufferList.Clear ();
  188. foreach (StraightLine line in _lines)
  189. {
  190. if (line.Intersects (x, y) is { } intersect)
  191. {
  192. intersectionsBufferList.Add (intersect);
  193. }
  194. }
  195. // Safe as long as the list is not modified while the span is in use.
  196. ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan (intersectionsBufferList);
  197. Cell? cell = GetCellForIntersects (intersects);
  198. if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false)
  199. {
  200. map.Add (new (x, y), cell);
  201. rowXValues.Add (x);
  202. }
  203. }
  204. // Build Region spans for this completed row
  205. if (rowXValues.Count <= 0)
  206. {
  207. continue;
  208. }
  209. // X values are already sorted (inner loop iterates x in order)
  210. int spanStart = rowXValues [0];
  211. int spanEnd = rowXValues [0];
  212. for (int i = 1; i < rowXValues.Count; i++)
  213. {
  214. if (rowXValues [i] == spanEnd + 1)
  215. {
  216. // Continue the span
  217. spanEnd = rowXValues [i];
  218. }
  219. else
  220. {
  221. // End the current span and add it to the region
  222. region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union);
  223. spanStart = rowXValues [i];
  224. spanEnd = rowXValues [i];
  225. }
  226. }
  227. // Add the final span for this row
  228. region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union);
  229. }
  230. return (map, region);
  231. }
  232. /// <summary>
  233. /// Efficiently builds a <see cref="Region"/> from line cells by grouping contiguous horizontal spans.
  234. /// This avoids the performance overhead of adding each cell individually while accurately
  235. /// representing the non-rectangular shape of the lines.
  236. /// </summary>
  237. /// <param name="cellMap">Dictionary of points where line cells are drawn. If empty, returns an empty Region.</param>
  238. /// <returns>A Region encompassing all the line cells, or an empty Region if cellMap is empty</returns>
  239. public static Region GetRegion (Dictionary<Point, Cell?> cellMap)
  240. {
  241. // Group cells by row for efficient horizontal span detection
  242. // Sort by Y then X so that within each row group, X values are in order
  243. IEnumerable<IGrouping<int, Point>> rowGroups = cellMap.Keys
  244. .OrderBy (p => p.Y)
  245. .ThenBy (p => p.X)
  246. .GroupBy (p => p.Y);
  247. Region region = new ();
  248. foreach (IGrouping<int, Point> row in rowGroups)
  249. {
  250. int y = row.Key;
  251. // X values are sorted due to ThenBy above
  252. List<int> xValues = row.Select (p => p.X).ToList ();
  253. // Note: GroupBy on non-empty Keys guarantees non-empty groups, but check anyway for safety
  254. if (xValues.Count == 0)
  255. {
  256. continue;
  257. }
  258. // Merge contiguous x values into horizontal spans
  259. int spanStart = xValues [0];
  260. int spanEnd = xValues [0];
  261. for (int i = 1; i < xValues.Count; i++)
  262. {
  263. if (xValues [i] == spanEnd + 1)
  264. {
  265. // Continue the span
  266. spanEnd = xValues [i];
  267. }
  268. else
  269. {
  270. // End the current span and add it to the region
  271. region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union);
  272. spanStart = xValues [i];
  273. spanEnd = xValues [i];
  274. }
  275. }
  276. // Add the final span for this row
  277. region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union);
  278. }
  279. return region;
  280. }
  281. // TODO: Unless there's an obvious use case for this API we should delete it in favor of the
  282. // simpler version that doesn't take an area.
  283. /// <summary>
  284. /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
  285. /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
  286. /// intersection symbols.
  287. /// </summary>
  288. /// <remarks>
  289. /// <para>
  290. /// Only the points within the <paramref name="inArea"/> of the canvas that are not in the exclusion region will be
  291. /// returned. To exclude points from the map, use <see cref="Exclude"/>.
  292. /// </para>
  293. /// </remarks>
  294. /// <param name="inArea">A rectangle to constrain the search by.</param>
  295. /// <returns>A map of the points within the canvas that intersect with <paramref name="inArea"/>.</returns>
  296. public Dictionary<Point, Rune> GetMap (Rectangle inArea)
  297. {
  298. Dictionary<Point, Rune> map = new ();
  299. List<IntersectionDefinition> intersectionsBufferList = [];
  300. // walk through each pixel of the bitmap
  301. for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++)
  302. {
  303. for (int x = inArea.X; x < inArea.X + inArea.Width; x++)
  304. {
  305. intersectionsBufferList.Clear ();
  306. foreach (var line in _lines)
  307. {
  308. if (line.Intersects (x, y) is { } intersect)
  309. {
  310. intersectionsBufferList.Add (intersect);
  311. }
  312. }
  313. // Safe as long as the list is not modified while the span is in use.
  314. ReadOnlySpan<IntersectionDefinition> intersects = CollectionsMarshal.AsSpan (intersectionsBufferList);
  315. Rune? rune = GetRuneForIntersects (intersects);
  316. if (rune is { } && _exclusionRegion?.Contains (x, y) is null or false)
  317. {
  318. map.Add (new (x, y), rune.Value);
  319. }
  320. }
  321. }
  322. return map;
  323. }
  324. /// <summary>
  325. /// Evaluates the lines that have been added to the canvas and returns a map containing the glyphs and their
  326. /// locations. The glyphs are the characters that should be rendered so that all lines connect up with the appropriate
  327. /// intersection symbols.
  328. /// </summary>
  329. /// <remarks>
  330. /// <para>
  331. /// Only the points within the <see cref="Bounds"/> of the canvas that are not in the exclusion region will be
  332. /// returned. To exclude points from the map, use <see cref="Exclude"/>.
  333. /// </para>
  334. /// </remarks>
  335. /// <returns>A map of all the points within the canvas.</returns>
  336. public Dictionary<Point, Rune> GetMap () { return GetMap (Bounds); }
  337. /// <summary>Merges one line canvas into this one.</summary>
  338. /// <param name="lineCanvas"></param>
  339. public void Merge (LineCanvas lineCanvas)
  340. {
  341. foreach (StraightLine line in lineCanvas._lines)
  342. {
  343. AddLine (line);
  344. }
  345. if (lineCanvas._exclusionRegion is { })
  346. {
  347. _exclusionRegion ??= new ();
  348. _exclusionRegion.Union (lineCanvas._exclusionRegion);
  349. }
  350. }
  351. /// <summary>Removes the last line added to the canvas</summary>
  352. /// <returns></returns>
  353. public StraightLine RemoveLastLine ()
  354. {
  355. StraightLine? l = _lines.LastOrDefault ();
  356. if (l is { })
  357. {
  358. _lines.Remove (l);
  359. }
  360. return l!;
  361. }
  362. /// <summary>
  363. /// Returns the contents of the line canvas rendered to a string. The string will include all columns and rows,
  364. /// even if <see cref="Bounds"/> has negative coordinates. For example, if the canvas contains a single line that
  365. /// starts at (-1,-1) with a length of 2, the rendered string will have a length of 2.
  366. /// </summary>
  367. /// <returns>The canvas rendered to a string.</returns>
  368. public override string ToString ()
  369. {
  370. if (Bounds.IsEmpty)
  371. {
  372. return string.Empty;
  373. }
  374. // Generate the rune map for the entire canvas
  375. Dictionary<Point, Rune> runeMap = GetMap ();
  376. // Create the rune canvas
  377. Rune [,] canvas = new Rune [Bounds.Height, Bounds.Width];
  378. // Copy the rune map to the canvas, adjusting for any negative coordinates
  379. foreach (KeyValuePair<Point, Rune> kvp in runeMap)
  380. {
  381. int x = kvp.Key.X - Bounds.X;
  382. int y = kvp.Key.Y - Bounds.Y;
  383. canvas [y, x] = kvp.Value;
  384. }
  385. // Convert the canvas to a string
  386. var sb = new StringBuilder ();
  387. for (var y = 0; y < canvas.GetLength (0); y++)
  388. {
  389. for (var x = 0; x < canvas.GetLength (1); x++)
  390. {
  391. Rune r = canvas [y, x];
  392. sb.Append (r.Value == 0 ? ' ' : r.ToString ());
  393. }
  394. if (y < canvas.GetLength (0) - 1)
  395. {
  396. sb.AppendLine ();
  397. }
  398. }
  399. return sb.ToString ();
  400. }
  401. private static bool All (ReadOnlySpan<IntersectionDefinition> intersects, Orientation orientation)
  402. {
  403. foreach (var intersect in intersects)
  404. {
  405. if (intersect.Line.Orientation != orientation)
  406. {
  407. return false;
  408. }
  409. }
  410. return true;
  411. }
  412. private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
  413. {
  414. foreach (KeyValuePair<IntersectionRuneType, IntersectionRuneResolver> irr in _runeResolvers)
  415. {
  416. irr.Value.SetGlyphs ();
  417. }
  418. }
  419. /// <summary>
  420. /// Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/> and there are
  421. /// no additional <see cref="IntersectionRuneType"/>
  422. /// </summary>
  423. /// <param name="intersects"></param>
  424. /// <param name="types"></param>
  425. /// <returns></returns>
  426. private static bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types) { return intersects.SetEquals (types); }
  427. private Attribute? GetAttributeForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
  428. {
  429. return Fill?.GetAttribute (intersects [0].Point) ?? intersects [0].Line.Attribute;
  430. }
  431. private readonly Dictionary<IntersectionRuneType, IntersectionRuneResolver> _runeResolvers = new ()
  432. {
  433. {
  434. IntersectionRuneType.ULCorner,
  435. new ULIntersectionRuneResolver ()
  436. },
  437. {
  438. IntersectionRuneType.URCorner,
  439. new URIntersectionRuneResolver ()
  440. },
  441. {
  442. IntersectionRuneType.LLCorner,
  443. new LLIntersectionRuneResolver ()
  444. },
  445. {
  446. IntersectionRuneType.LRCorner,
  447. new LRIntersectionRuneResolver ()
  448. },
  449. {
  450. IntersectionRuneType.TopTee,
  451. new TopTeeIntersectionRuneResolver ()
  452. },
  453. {
  454. IntersectionRuneType.LeftTee,
  455. new LeftTeeIntersectionRuneResolver ()
  456. },
  457. {
  458. IntersectionRuneType.RightTee,
  459. new RightTeeIntersectionRuneResolver ()
  460. },
  461. {
  462. IntersectionRuneType.BottomTee,
  463. new BottomTeeIntersectionRuneResolver ()
  464. },
  465. {
  466. IntersectionRuneType.Cross,
  467. new CrossIntersectionRuneResolver ()
  468. }
  469. // TODO: Add other resolvers
  470. };
  471. private Cell? GetCellForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
  472. {
  473. if (intersects.IsEmpty)
  474. {
  475. return null;
  476. }
  477. var cell = new Cell ();
  478. Rune? rune = GetRuneForIntersects (intersects);
  479. if (rune.HasValue)
  480. {
  481. cell.Grapheme = rune.ToString ()!;
  482. }
  483. cell.Attribute = GetAttributeForIntersects (intersects);
  484. return cell;
  485. }
  486. private Rune? GetRuneForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
  487. {
  488. if (intersects.IsEmpty)
  489. {
  490. return null;
  491. }
  492. IntersectionRuneType runeType = GetRuneTypeForIntersects (intersects);
  493. if (_runeResolvers.TryGetValue (runeType, out IntersectionRuneResolver? resolver))
  494. {
  495. return resolver.GetRuneForIntersects (intersects);
  496. }
  497. // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
  498. bool useDouble = AnyLineStyles (intersects, [LineStyle.Double]);
  499. bool useDashed = AnyLineStyles (intersects, [LineStyle.Dashed, LineStyle.RoundedDashed]);
  500. bool useDotted = AnyLineStyles (intersects, [LineStyle.Dotted, LineStyle.RoundedDotted]);
  501. // horiz and vert lines same as Single for Rounded
  502. bool useThick = AnyLineStyles (intersects, [LineStyle.Heavy]);
  503. bool useThickDashed = AnyLineStyles (intersects, [LineStyle.HeavyDashed]);
  504. bool useThickDotted = AnyLineStyles (intersects, [LineStyle.HeavyDotted]);
  505. // TODO: Support ruler
  506. //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
  507. // TODO: maybe make these resolvers too for simplicity?
  508. switch (runeType)
  509. {
  510. case IntersectionRuneType.None:
  511. return null;
  512. case IntersectionRuneType.Dot:
  513. return Glyphs.Dot;
  514. case IntersectionRuneType.HLine:
  515. if (useDouble)
  516. {
  517. return Glyphs.HLineDbl;
  518. }
  519. if (useDashed)
  520. {
  521. return Glyphs.HLineDa2;
  522. }
  523. if (useDotted)
  524. {
  525. return Glyphs.HLineDa3;
  526. }
  527. return useThick ? Glyphs.HLineHv :
  528. useThickDashed ? Glyphs.HLineHvDa2 :
  529. useThickDotted ? Glyphs.HLineHvDa3 : Glyphs.HLine;
  530. case IntersectionRuneType.VLine:
  531. if (useDouble)
  532. {
  533. return Glyphs.VLineDbl;
  534. }
  535. if (useDashed)
  536. {
  537. return Glyphs.VLineDa3;
  538. }
  539. if (useDotted)
  540. {
  541. return Glyphs.VLineDa4;
  542. }
  543. return useThick ? Glyphs.VLineHv :
  544. useThickDashed ? Glyphs.VLineHvDa3 :
  545. useThickDotted ? Glyphs.VLineHvDa4 : Glyphs.VLine;
  546. default:
  547. throw new (
  548. "Could not find resolver or switch case for "
  549. + nameof (runeType)
  550. + ":"
  551. + runeType
  552. );
  553. }
  554. static bool AnyLineStyles (ReadOnlySpan<IntersectionDefinition> intersects, ReadOnlySpan<LineStyle> lineStyles)
  555. {
  556. foreach (IntersectionDefinition intersect in intersects)
  557. {
  558. foreach (LineStyle style in lineStyles)
  559. {
  560. if (intersect.Line.Style == style)
  561. {
  562. return true;
  563. }
  564. }
  565. }
  566. return false;
  567. }
  568. }
  569. private IntersectionRuneType GetRuneTypeForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
  570. {
  571. HashSet<IntersectionType> set = new (capacity: intersects.Length);
  572. foreach (var intersect in intersects)
  573. {
  574. set.Add (intersect.Type);
  575. }
  576. #region Cross Conditions
  577. if (Has (
  578. set,
  579. [IntersectionType.PassOverHorizontal,
  580. IntersectionType.PassOverVertical]
  581. ))
  582. {
  583. return IntersectionRuneType.Cross;
  584. }
  585. if (Has (
  586. set,
  587. [IntersectionType.PassOverVertical,
  588. IntersectionType.StartLeft,
  589. IntersectionType.StartRight]
  590. ))
  591. {
  592. return IntersectionRuneType.Cross;
  593. }
  594. if (Has (
  595. set,
  596. [IntersectionType.PassOverHorizontal,
  597. IntersectionType.StartUp,
  598. IntersectionType.StartDown]
  599. ))
  600. {
  601. return IntersectionRuneType.Cross;
  602. }
  603. if (Has (
  604. set,
  605. [IntersectionType.StartLeft,
  606. IntersectionType.StartRight,
  607. IntersectionType.StartUp,
  608. IntersectionType.StartDown]
  609. ))
  610. {
  611. return IntersectionRuneType.Cross;
  612. }
  613. #endregion
  614. #region Corner Conditions
  615. if (Exactly (set, CornerIntersections.UpperLeft))
  616. {
  617. return IntersectionRuneType.ULCorner;
  618. }
  619. if (Exactly (set, CornerIntersections.UpperRight))
  620. {
  621. return IntersectionRuneType.URCorner;
  622. }
  623. if (Exactly (set, CornerIntersections.LowerRight))
  624. {
  625. return IntersectionRuneType.LRCorner;
  626. }
  627. if (Exactly (set, CornerIntersections.LowerLeft))
  628. {
  629. return IntersectionRuneType.LLCorner;
  630. }
  631. #endregion Corner Conditions
  632. #region T Conditions
  633. if (Has (
  634. set,
  635. [IntersectionType.PassOverHorizontal,
  636. IntersectionType.StartDown]
  637. ))
  638. {
  639. return IntersectionRuneType.TopTee;
  640. }
  641. if (Has (
  642. set,
  643. [IntersectionType.StartRight,
  644. IntersectionType.StartLeft,
  645. IntersectionType.StartDown]
  646. ))
  647. {
  648. return IntersectionRuneType.TopTee;
  649. }
  650. if (Has (
  651. set,
  652. [IntersectionType.PassOverHorizontal,
  653. IntersectionType.StartUp]
  654. ))
  655. {
  656. return IntersectionRuneType.BottomTee;
  657. }
  658. if (Has (
  659. set,
  660. [IntersectionType.StartRight,
  661. IntersectionType.StartLeft,
  662. IntersectionType.StartUp]
  663. ))
  664. {
  665. return IntersectionRuneType.BottomTee;
  666. }
  667. if (Has (
  668. set,
  669. [IntersectionType.PassOverVertical,
  670. IntersectionType.StartRight]
  671. ))
  672. {
  673. return IntersectionRuneType.LeftTee;
  674. }
  675. if (Has (
  676. set,
  677. [IntersectionType.StartRight,
  678. IntersectionType.StartDown,
  679. IntersectionType.StartUp]
  680. ))
  681. {
  682. return IntersectionRuneType.LeftTee;
  683. }
  684. if (Has (
  685. set,
  686. [IntersectionType.PassOverVertical,
  687. IntersectionType.StartLeft]
  688. ))
  689. {
  690. return IntersectionRuneType.RightTee;
  691. }
  692. if (Has (
  693. set,
  694. [IntersectionType.StartLeft,
  695. IntersectionType.StartDown,
  696. IntersectionType.StartUp]
  697. ))
  698. {
  699. return IntersectionRuneType.RightTee;
  700. }
  701. #endregion
  702. if (All (intersects, Orientation.Horizontal))
  703. {
  704. return IntersectionRuneType.HLine;
  705. }
  706. if (All (intersects, Orientation.Vertical))
  707. {
  708. return IntersectionRuneType.VLine;
  709. }
  710. return IntersectionRuneType.Dot;
  711. }
  712. /// <summary>
  713. /// Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/> specified
  714. /// (i.e. AND).
  715. /// </summary>
  716. /// <param name="intersects"></param>
  717. /// <param name="types"></param>
  718. /// <returns></returns>
  719. private bool Has (HashSet<IntersectionType> intersects, ReadOnlySpan<IntersectionType> types)
  720. {
  721. foreach (var type in types)
  722. {
  723. if (!intersects.Contains (type))
  724. {
  725. return false;
  726. }
  727. }
  728. return true;
  729. }
  730. /// <summary>
  731. /// Preallocated arrays for <see cref="GetRuneTypeForIntersects"/> calls to <see cref="Exactly"/>.
  732. /// </summary>
  733. /// <remarks>
  734. /// Optimization to avoid array allocation for each call from array params. Please do not edit the arrays at runtime. :)
  735. ///
  736. /// More ideal solution would be to change <see cref="Exactly"/> to take ReadOnlySpan instead of an array
  737. /// but that would require replacing the HashSet.SetEquals call.
  738. /// </remarks>
  739. private static class CornerIntersections
  740. {
  741. // Names matching #region "Corner Conditions" IntersectionRuneType
  742. internal static readonly IntersectionType [] UpperLeft = [IntersectionType.StartRight, IntersectionType.StartDown];
  743. internal static readonly IntersectionType [] UpperRight = [IntersectionType.StartLeft, IntersectionType.StartDown];
  744. internal static readonly IntersectionType [] LowerRight = [IntersectionType.StartUp, IntersectionType.StartLeft];
  745. internal static readonly IntersectionType [] LowerLeft = [IntersectionType.StartUp, IntersectionType.StartRight];
  746. }
  747. private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver
  748. {
  749. public override void SetGlyphs ()
  750. {
  751. _round = Glyphs.BottomTee;
  752. _doubleH = Glyphs.BottomTeeDblH;
  753. _doubleV = Glyphs.BottomTeeDblV;
  754. _doubleBoth = Glyphs.BottomTeeDbl;
  755. _thickH = Glyphs.BottomTeeHvH;
  756. _thickV = Glyphs.BottomTeeHvV;
  757. _thickBoth = Glyphs.BottomTeeHvDblH;
  758. _normal = Glyphs.BottomTee;
  759. }
  760. }
  761. private class CrossIntersectionRuneResolver : IntersectionRuneResolver
  762. {
  763. public override void SetGlyphs ()
  764. {
  765. _round = Glyphs.Cross;
  766. _doubleH = Glyphs.CrossDblH;
  767. _doubleV = Glyphs.CrossDblV;
  768. _doubleBoth = Glyphs.CrossDbl;
  769. _thickH = Glyphs.CrossHvH;
  770. _thickV = Glyphs.CrossHvV;
  771. _thickBoth = Glyphs.CrossHv;
  772. _normal = Glyphs.Cross;
  773. }
  774. }
  775. private abstract class IntersectionRuneResolver
  776. {
  777. internal Rune _doubleBoth;
  778. internal Rune _doubleH;
  779. internal Rune _doubleV;
  780. internal Rune _normal;
  781. internal Rune _round;
  782. internal Rune _thickBoth;
  783. internal Rune _thickH;
  784. internal Rune _thickV;
  785. protected IntersectionRuneResolver ()
  786. {
  787. SetGlyphs ();
  788. }
  789. public Rune? GetRuneForIntersects (ReadOnlySpan<IntersectionDefinition> intersects)
  790. {
  791. // Note that there aren't any glyphs for intersections of double lines with heavy lines
  792. bool doubleHorizontal = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Horizontal, [LineStyle.Double]);
  793. bool doubleVertical = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Vertical, [LineStyle.Double]);
  794. if (doubleHorizontal)
  795. {
  796. return doubleVertical ? _doubleBoth : _doubleH;
  797. }
  798. if (doubleVertical)
  799. {
  800. return _doubleV;
  801. }
  802. bool thickHorizontal = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Horizontal,
  803. [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]);
  804. bool thickVertical = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Vertical,
  805. [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]);
  806. if (thickHorizontal)
  807. {
  808. return thickVertical ? _thickBoth : _thickH;
  809. }
  810. if (thickVertical)
  811. {
  812. return _thickV;
  813. }
  814. return UseRounded (intersects) ? _round : _normal;
  815. static bool UseRounded (ReadOnlySpan<IntersectionDefinition> intersects)
  816. {
  817. foreach (var intersect in intersects)
  818. {
  819. if (intersect.Line.Length == 0)
  820. {
  821. continue;
  822. }
  823. if (intersect.Line.Style is
  824. LineStyle.Rounded or
  825. LineStyle.RoundedDashed or
  826. LineStyle.RoundedDotted)
  827. {
  828. return true;
  829. }
  830. }
  831. return false;
  832. }
  833. static bool AnyWithOrientationAndAnyLineStyle (
  834. ReadOnlySpan<IntersectionDefinition> intersects,
  835. Orientation orientation,
  836. ReadOnlySpan<LineStyle> lineStyles)
  837. {
  838. foreach (var i in intersects)
  839. {
  840. if (i.Line.Orientation != orientation)
  841. {
  842. continue;
  843. }
  844. // Any line style
  845. foreach (var style in lineStyles)
  846. {
  847. if (i.Line.Style == style)
  848. {
  849. return true;
  850. }
  851. }
  852. }
  853. return false;
  854. }
  855. }
  856. /// <summary>
  857. /// Sets the glyphs used. Call this method after construction and any time ConfigurationManager has updated the
  858. /// settings.
  859. /// </summary>
  860. public abstract void SetGlyphs ();
  861. }
  862. private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver
  863. {
  864. public override void SetGlyphs ()
  865. {
  866. _round = Glyphs.LeftTee;
  867. _doubleH = Glyphs.LeftTeeDblH;
  868. _doubleV = Glyphs.LeftTeeDblV;
  869. _doubleBoth = Glyphs.LeftTeeDbl;
  870. _thickH = Glyphs.LeftTeeHvH;
  871. _thickV = Glyphs.LeftTeeHvV;
  872. _thickBoth = Glyphs.LeftTeeHvDblH;
  873. _normal = Glyphs.LeftTee;
  874. }
  875. }
  876. private class LLIntersectionRuneResolver : IntersectionRuneResolver
  877. {
  878. public override void SetGlyphs ()
  879. {
  880. _round = Glyphs.LLCornerR;
  881. _doubleH = Glyphs.LLCornerSingleDbl;
  882. _doubleV = Glyphs.LLCornerDblSingle;
  883. _doubleBoth = Glyphs.LLCornerDbl;
  884. _thickH = Glyphs.LLCornerLtHv;
  885. _thickV = Glyphs.LLCornerHvLt;
  886. _thickBoth = Glyphs.LLCornerHv;
  887. _normal = Glyphs.LLCorner;
  888. }
  889. }
  890. private class LRIntersectionRuneResolver : IntersectionRuneResolver
  891. {
  892. public override void SetGlyphs ()
  893. {
  894. _round = Glyphs.LRCornerR;
  895. _doubleH = Glyphs.LRCornerSingleDbl;
  896. _doubleV = Glyphs.LRCornerDblSingle;
  897. _doubleBoth = Glyphs.LRCornerDbl;
  898. _thickH = Glyphs.LRCornerLtHv;
  899. _thickV = Glyphs.LRCornerHvLt;
  900. _thickBoth = Glyphs.LRCornerHv;
  901. _normal = Glyphs.LRCorner;
  902. }
  903. }
  904. private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver
  905. {
  906. public override void SetGlyphs ()
  907. {
  908. _round = Glyphs.RightTee;
  909. _doubleH = Glyphs.RightTeeDblH;
  910. _doubleV = Glyphs.RightTeeDblV;
  911. _doubleBoth = Glyphs.RightTeeDbl;
  912. _thickH = Glyphs.RightTeeHvH;
  913. _thickV = Glyphs.RightTeeHvV;
  914. _thickBoth = Glyphs.RightTeeHvDblH;
  915. _normal = Glyphs.RightTee;
  916. }
  917. }
  918. private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver
  919. {
  920. public override void SetGlyphs ()
  921. {
  922. _round = Glyphs.TopTee;
  923. _doubleH = Glyphs.TopTeeDblH;
  924. _doubleV = Glyphs.TopTeeDblV;
  925. _doubleBoth = Glyphs.TopTeeDbl;
  926. _thickH = Glyphs.TopTeeHvH;
  927. _thickV = Glyphs.TopTeeHvV;
  928. _thickBoth = Glyphs.TopTeeHvDblH;
  929. _normal = Glyphs.TopTee;
  930. }
  931. }
  932. private class ULIntersectionRuneResolver : IntersectionRuneResolver
  933. {
  934. public override void SetGlyphs ()
  935. {
  936. _round = Glyphs.ULCornerR;
  937. _doubleH = Glyphs.ULCornerSingleDbl;
  938. _doubleV = Glyphs.ULCornerDblSingle;
  939. _doubleBoth = Glyphs.ULCornerDbl;
  940. _thickH = Glyphs.ULCornerLtHv;
  941. _thickV = Glyphs.ULCornerHvLt;
  942. _thickBoth = Glyphs.ULCornerHv;
  943. _normal = Glyphs.ULCorner;
  944. }
  945. }
  946. private class URIntersectionRuneResolver : IntersectionRuneResolver
  947. {
  948. public override void SetGlyphs ()
  949. {
  950. _round = Glyphs.URCornerR;
  951. _doubleH = Glyphs.URCornerSingleDbl;
  952. _doubleV = Glyphs.URCornerDblSingle;
  953. _doubleBoth = Glyphs.URCornerDbl;
  954. _thickH = Glyphs.URCornerHvLt;
  955. _thickV = Glyphs.URCornerLtHv;
  956. _thickBoth = Glyphs.URCornerHv;
  957. _normal = Glyphs.URCorner;
  958. }
  959. }
  960. /// <inheritdoc/>
  961. public void Dispose ()
  962. {
  963. ConfigurationManager.Applied -= ConfigurationManager_Applied;
  964. GC.SuppressFinalize (this);
  965. }
  966. }