LineCanvas.cs 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  1. #nullable enable
  2. using System;
  3. using System.Collections;
  4. using System.Collections.Generic;
  5. using System.Linq;
  6. using System.Text;
  7. namespace Terminal.Gui {
  8. /// <summary>
  9. /// Defines the style of lines for a <see cref="LineCanvas"/>.
  10. /// </summary>
  11. public enum LineStyle {
  12. /// <summary>
  13. /// No border is drawn.
  14. /// </summary>
  15. None,
  16. /// <summary>
  17. /// The border is drawn using thin line CM.Glyphs.
  18. /// </summary>
  19. Single,
  20. /// <summary>
  21. /// The border is drawn using thin line glyphs with dashed (double and triple) straight lines.
  22. /// </summary>
  23. Dashed,
  24. /// <summary>
  25. /// The border is drawn using thin line glyphs with short dashed (triple and quadruple) straight lines.
  26. /// </summary>
  27. Dotted,
  28. /// <summary>
  29. /// The border is drawn using thin double line CM.Glyphs.
  30. /// </summary>
  31. Double,
  32. /// <summary>
  33. /// The border is drawn using heavy line CM.Glyphs.
  34. /// </summary>
  35. Heavy,
  36. /// <summary>
  37. /// The border is drawn using heavy line glyphs with dashed (double and triple) straight lines.
  38. /// </summary>
  39. HeavyDashed,
  40. /// <summary>
  41. /// The border is drawn using heavy line glyphs with short dashed (triple and quadruple) straight lines.
  42. /// </summary>
  43. HeavyDotted,
  44. /// <summary>
  45. /// The border is drawn using thin line glyphs with rounded corners.
  46. /// </summary>
  47. Rounded,
  48. /// <summary>
  49. /// The border is drawn using thin line glyphs with rounded corners and dashed (double and triple) straight lines.
  50. /// </summary>
  51. RoundedDashed,
  52. /// <summary>
  53. /// The border is drawn using thin line glyphs with rounded corners and short dashed (triple and quadruple) straight lines.
  54. /// </summary>
  55. RoundedDotted,
  56. // TODO: Support Ruler
  57. ///// <summary>
  58. ///// The border is drawn as a diagnostic ruler ("|123456789...").
  59. ///// </summary>
  60. //Ruler
  61. }
  62. /// <summary>
  63. /// Facilitates box drawing and line intersection detection
  64. /// and rendering. Does not support diagonal lines.
  65. /// </summary>
  66. public class LineCanvas : IDisposable {
  67. /// <summary>
  68. /// Creates a new instance.
  69. /// </summary>
  70. public LineCanvas()
  71. {
  72. // TODO: Refactor ConfigurationManager to not use an event handler for this.
  73. // Instead, have it call a method on any class appropriately attributed
  74. // to update the cached values. See Issue #2871
  75. ConfigurationManager.Applied += ConfigurationManager_Applied;
  76. }
  77. private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
  78. {
  79. foreach (var irr in runeResolvers) {
  80. irr.Value.SetGlyphs ();
  81. }
  82. }
  83. private List<StraightLine> _lines = new List<StraightLine> ();
  84. Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
  85. {IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()},
  86. {IntersectionRuneType.URCorner,new URIntersectionRuneResolver()},
  87. {IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()},
  88. {IntersectionRuneType.LRCorner,new LRIntersectionRuneResolver()},
  89. {IntersectionRuneType.TopTee,new TopTeeIntersectionRuneResolver()},
  90. {IntersectionRuneType.LeftTee,new LeftTeeIntersectionRuneResolver()},
  91. {IntersectionRuneType.RightTee,new RightTeeIntersectionRuneResolver()},
  92. {IntersectionRuneType.BottomTee,new BottomTeeIntersectionRuneResolver()},
  93. {IntersectionRuneType.Cross,new CrossIntersectionRuneResolver()},
  94. // TODO: Add other resolvers
  95. };
  96. /// <summary>
  97. /// <para>
  98. /// Adds a new <paramref name="length"/> long line to the canvas starting at <paramref name="start"/>.
  99. /// </para>
  100. /// <para>
  101. /// Use positive <paramref name="length"/> for the line to extend Right and negative for Left
  102. /// when <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
  103. /// </para>
  104. /// <para>
  105. /// Use positive <paramref name="length"/> for the line to extend Down and negative for Up
  106. /// when <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
  107. /// </para>
  108. /// </summary>
  109. /// <param name="start">Starting point.</param>
  110. /// <param name="length">The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for Up/Left.</param>
  111. /// <param name="orientation">The direction of the line.</param>
  112. /// <param name="style">The style of line to use</param>
  113. /// <param name="attribute"></param>
  114. public void AddLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default)
  115. {
  116. _cachedBounds = Rect.Empty;
  117. _lines.Add (new StraightLine (start, length, orientation, style, attribute));
  118. }
  119. /// <summary>
  120. /// Adds a new line to the canvas
  121. /// </summary>
  122. /// <param name="line"></param>
  123. public void AddLine (StraightLine line)
  124. {
  125. _cachedBounds = Rect.Empty;
  126. _lines.Add (line);
  127. }
  128. /// <summary>
  129. /// Removes the last line added to the canvas
  130. /// </summary>
  131. /// <returns></returns>
  132. public StraightLine RemoveLastLine()
  133. {
  134. var l = _lines.LastOrDefault ();
  135. if(l != null) {
  136. _lines.Remove(l);
  137. }
  138. return l!;
  139. }
  140. /// <summary>
  141. /// Clears all lines from the LineCanvas.
  142. /// </summary>
  143. public void Clear ()
  144. {
  145. _cachedBounds = Rect.Empty;
  146. _lines.Clear ();
  147. }
  148. /// <summary>
  149. /// Clears any cached states from the canvas
  150. /// Call this method if you make changes to lines
  151. /// that have already been added.
  152. /// </summary>
  153. public void ClearCache ()
  154. {
  155. _cachedBounds = Rect.Empty;
  156. }
  157. private Rect _cachedBounds;
  158. /// <summary>
  159. /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the
  160. /// line that is furthest left/top and Size is defined by the line that extends the furthest
  161. /// right/bottom.
  162. /// </summary>
  163. public Rect Bounds {
  164. get {
  165. if (_cachedBounds.IsEmpty) {
  166. if (_lines.Count == 0) {
  167. return _cachedBounds;
  168. }
  169. Rect bounds = _lines [0].Bounds;
  170. for (var i = 1; i < _lines.Count; i++) {
  171. var line = _lines [i];
  172. var lineBounds = line.Bounds;
  173. bounds = Rect.Union (bounds, lineBounds);
  174. }
  175. if (bounds.Width == 0) {
  176. bounds.Width = 1;
  177. }
  178. if (bounds.Height == 0) {
  179. bounds.Height = 1;
  180. }
  181. _cachedBounds = new Rect (bounds.X, bounds.Y, bounds.Width, bounds.Height);
  182. }
  183. return _cachedBounds;
  184. }
  185. }
  186. // TODO: Unless there's an obvious use case for this API we should delete it in favor of the
  187. // simpler version that doensn't take an area.
  188. /// <summary>
  189. /// Evaluates the lines that have been added to the canvas and returns a map containing
  190. /// the glyphs and their locations. The glyphs are the characters that should be rendered
  191. /// so that all lines connect up with the appropriate intersection symbols.
  192. /// </summary>
  193. /// <param name="inArea">A rectangle to constrain the search by.</param>
  194. /// <returns>A map of the points within the canvas that intersect with <paramref name="inArea"/>.</returns>
  195. public Dictionary<Point, Rune> GetMap (Rect inArea)
  196. {
  197. var map = new Dictionary<Point, Rune> ();
  198. // walk through each pixel of the bitmap
  199. for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++) {
  200. for (int x = inArea.X; x < inArea.X + inArea.Width; x++) {
  201. var intersects = _lines
  202. .Select (l => l.Intersects (x, y))
  203. .Where (i => i != null)
  204. .ToArray ();
  205. var rune = GetRuneForIntersects (Application.Driver, intersects);
  206. if (rune != null) {
  207. map.Add (new Point (x, y), rune.Value);
  208. }
  209. }
  210. }
  211. return map;
  212. }
  213. /// <summary>
  214. /// Evaluates the lines that have been added to the canvas and returns a map containing
  215. /// the glyphs and their locations. The glyphs are the characters that should be rendered
  216. /// so that all lines connect up with the appropriate intersection symbols.
  217. /// </summary>
  218. /// <returns>A map of all the points within the canvas.</returns>
  219. public Dictionary<Point, Cell> GetCellMap ()
  220. {
  221. var map = new Dictionary<Point, Cell> ();
  222. // walk through each pixel of the bitmap
  223. for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++) {
  224. for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++) {
  225. var intersects = _lines
  226. .Select (l => l.Intersects (x, y))
  227. .Where (i => i != null)
  228. .ToArray ();
  229. var cell = GetCellForIntersects (Application.Driver, intersects);
  230. if (cell != null) {
  231. map.Add (new Point (x, y), cell);
  232. }
  233. }
  234. }
  235. return map;
  236. }
  237. /// <summary>
  238. /// Evaluates the lines that have been added to the canvas and returns a map containing
  239. /// the glyphs and their locations. The glyphs are the characters that should be rendered
  240. /// so that all lines connect up with the appropriate intersection symbols.
  241. /// </summary>
  242. /// <returns>A map of all the points within the canvas.</returns>
  243. public Dictionary<Point, Rune> GetMap () => GetMap (Bounds);
  244. /// <summary>
  245. /// Returns the contents of the line canvas rendered to a string. The string
  246. /// will include all columns and rows, even if <see cref="Bounds"/> has negative coordinates.
  247. /// For example, if the canvas contains a single line that starts at (-1,-1) with a length of 2, the
  248. /// rendered string will have a length of 2.
  249. /// </summary>
  250. /// <returns>The canvas rendered to a string.</returns>
  251. public override string ToString ()
  252. {
  253. if (Bounds.IsEmpty) {
  254. return string.Empty;
  255. }
  256. // Generate the rune map for the entire canvas
  257. var runeMap = GetMap ();
  258. // Create the rune canvas
  259. Rune [,] canvas = new Rune [Bounds.Height, Bounds.Width];
  260. // Copy the rune map to the canvas, adjusting for any negative coordinates
  261. foreach (var kvp in runeMap) {
  262. int x = kvp.Key.X - Bounds.X;
  263. int y = kvp.Key.Y - Bounds.Y;
  264. canvas [y, x] = kvp.Value;
  265. }
  266. // Convert the canvas to a string
  267. StringBuilder sb = new StringBuilder ();
  268. for (int y = 0; y < canvas.GetLength (0); y++) {
  269. for (int x = 0; x < canvas.GetLength (1); x++) {
  270. Rune r = canvas [y, x];
  271. sb.Append (r.Value == 0 ? ' ' : r.ToString ());
  272. }
  273. if (y < canvas.GetLength (0) - 1) {
  274. sb.AppendLine ();
  275. }
  276. }
  277. return sb.ToString ();
  278. }
  279. private abstract class IntersectionRuneResolver {
  280. internal Rune _round;
  281. internal Rune _doubleH;
  282. internal Rune _doubleV;
  283. internal Rune _doubleBoth;
  284. internal Rune _thickH;
  285. internal Rune _thickV;
  286. internal Rune _thickBoth;
  287. internal Rune _normal;
  288. public IntersectionRuneResolver()
  289. {
  290. SetGlyphs ();
  291. }
  292. /// <summary>
  293. /// Sets the glyphs used. Call this method after construction and any time
  294. /// ConfigurationManager has updated the settings.
  295. /// </summary>
  296. public abstract void SetGlyphs ();
  297. public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
  298. {
  299. var useRounded = intersects.Any (i => i?.Line.Length != 0 && (
  300. i?.Line.Style == LineStyle.Rounded || i?.Line.Style == LineStyle.RoundedDashed || i?.Line.Style == LineStyle.RoundedDotted));
  301. // Note that there aren't any glyphs for intersections of double lines with heavy lines
  302. bool doubleHorizontal = intersects.Any (l => l?.Line.Orientation == Orientation.Horizontal && l.Line.Style == LineStyle.Double);
  303. bool doubleVertical = intersects.Any (l => l?.Line.Orientation == Orientation.Vertical && l.Line.Style == LineStyle.Double);
  304. bool thickHorizontal = intersects.Any (l => l?.Line.Orientation == Orientation.Horizontal && (
  305. l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
  306. bool thickVertical = intersects.Any (l => l?.Line.Orientation == Orientation.Vertical && (
  307. l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
  308. if (doubleHorizontal) {
  309. return doubleVertical ? _doubleBoth : _doubleH;
  310. }
  311. if (doubleVertical) {
  312. return _doubleV;
  313. }
  314. if (thickHorizontal) {
  315. return thickVertical ? _thickBoth : _thickH;
  316. }
  317. if (thickVertical) {
  318. return _thickV;
  319. }
  320. return useRounded ? _round : _normal;
  321. }
  322. }
  323. private class ULIntersectionRuneResolver : IntersectionRuneResolver {
  324. public override void SetGlyphs ()
  325. {
  326. _round = CM.Glyphs.ULCornerR;
  327. _doubleH = CM.Glyphs.ULCornerSingleDbl;
  328. _doubleV = CM.Glyphs.ULCornerDblSingle;
  329. _doubleBoth = CM.Glyphs.ULCornerDbl;
  330. _thickH = CM.Glyphs.ULCornerLtHv;
  331. _thickV = CM.Glyphs.ULCornerHvLt;
  332. _thickBoth = CM.Glyphs.ULCornerHv;
  333. _normal = CM.Glyphs.ULCorner;
  334. }
  335. }
  336. private class URIntersectionRuneResolver : IntersectionRuneResolver {
  337. public override void SetGlyphs ()
  338. {
  339. _round = CM.Glyphs.URCornerR;
  340. _doubleH = CM.Glyphs.URCornerSingleDbl;
  341. _doubleV = CM.Glyphs.URCornerDblSingle;
  342. _doubleBoth = CM.Glyphs.URCornerDbl;
  343. _thickH = CM.Glyphs.URCornerHvLt;
  344. _thickV = CM.Glyphs.URCornerLtHv;
  345. _thickBoth = CM.Glyphs.URCornerHv;
  346. _normal = CM.Glyphs.URCorner;
  347. }
  348. }
  349. private class LLIntersectionRuneResolver : IntersectionRuneResolver {
  350. public override void SetGlyphs ()
  351. {
  352. _round = CM.Glyphs.LLCornerR;
  353. _doubleH = CM.Glyphs.LLCornerSingleDbl;
  354. _doubleV = CM.Glyphs.LLCornerDblSingle;
  355. _doubleBoth = CM.Glyphs.LLCornerDbl;
  356. _thickH = CM.Glyphs.LLCornerLtHv;
  357. _thickV = CM.Glyphs.LLCornerHvLt;
  358. _thickBoth = CM.Glyphs.LLCornerHv;
  359. _normal = CM.Glyphs.LLCorner;
  360. }
  361. }
  362. private class LRIntersectionRuneResolver : IntersectionRuneResolver {
  363. public override void SetGlyphs ()
  364. {
  365. _round = CM.Glyphs.LRCornerR;
  366. _doubleH = CM.Glyphs.LRCornerSingleDbl;
  367. _doubleV = CM.Glyphs.LRCornerDblSingle;
  368. _doubleBoth = CM.Glyphs.LRCornerDbl;
  369. _thickH = CM.Glyphs.LRCornerLtHv;
  370. _thickV = CM.Glyphs.LRCornerHvLt;
  371. _thickBoth = CM.Glyphs.LRCornerHv;
  372. _normal = CM.Glyphs.LRCorner;
  373. }
  374. }
  375. private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver {
  376. public override void SetGlyphs ()
  377. {
  378. _round = CM.Glyphs.TopTee;
  379. _doubleH = CM.Glyphs.TopTeeDblH;
  380. _doubleV = CM.Glyphs.TopTeeDblV;
  381. _doubleBoth = CM.Glyphs.TopTeeDbl;
  382. _thickH = CM.Glyphs.TopTeeHvH;
  383. _thickV = CM.Glyphs.TopTeeHvV;
  384. _thickBoth = CM.Glyphs.TopTeeHvDblH;
  385. _normal = CM.Glyphs.TopTee;
  386. }
  387. }
  388. private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver {
  389. public override void SetGlyphs ()
  390. {
  391. _round = CM.Glyphs.LeftTee;
  392. _doubleH = CM.Glyphs.LeftTeeDblH;
  393. _doubleV = CM.Glyphs.LeftTeeDblV;
  394. _doubleBoth = CM.Glyphs.LeftTeeDbl;
  395. _thickH = CM.Glyphs.LeftTeeHvH;
  396. _thickV = CM.Glyphs.LeftTeeHvV;
  397. _thickBoth = CM.Glyphs.LeftTeeHvDblH;
  398. _normal = CM.Glyphs.LeftTee;
  399. }
  400. }
  401. private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver {
  402. public override void SetGlyphs ()
  403. {
  404. _round = CM.Glyphs.RightTee;
  405. _doubleH = CM.Glyphs.RightTeeDblH;
  406. _doubleV = CM.Glyphs.RightTeeDblV;
  407. _doubleBoth = CM.Glyphs.RightTeeDbl;
  408. _thickH = CM.Glyphs.RightTeeHvH;
  409. _thickV = CM.Glyphs.RightTeeHvV;
  410. _thickBoth = CM.Glyphs.RightTeeHvDblH;
  411. _normal = CM.Glyphs.RightTee;
  412. }
  413. }
  414. private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver {
  415. public override void SetGlyphs ()
  416. {
  417. _round = CM.Glyphs.BottomTee;
  418. _doubleH = CM.Glyphs.BottomTeeDblH;
  419. _doubleV = CM.Glyphs.BottomTeeDblV;
  420. _doubleBoth = CM.Glyphs.BottomTeeDbl;
  421. _thickH = CM.Glyphs.BottomTeeHvH;
  422. _thickV = CM.Glyphs.BottomTeeHvV;
  423. _thickBoth = CM.Glyphs.BottomTeeHvDblH;
  424. _normal = CM.Glyphs.BottomTee;
  425. }
  426. }
  427. private class CrossIntersectionRuneResolver : IntersectionRuneResolver {
  428. public override void SetGlyphs ()
  429. {
  430. _round = CM.Glyphs.Cross;
  431. _doubleH = CM.Glyphs.CrossDblH;
  432. _doubleV = CM.Glyphs.CrossDblV;
  433. _doubleBoth = CM.Glyphs.CrossDbl;
  434. _thickH = CM.Glyphs.CrossHvH;
  435. _thickV = CM.Glyphs.CrossHvV;
  436. _thickBoth = CM.Glyphs.CrossHv;
  437. _normal = CM.Glyphs.Cross;
  438. }
  439. }
  440. private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
  441. {
  442. if (!intersects.Any ()) {
  443. return null;
  444. }
  445. var runeType = GetRuneTypeForIntersects (intersects);
  446. if (runeResolvers.TryGetValue (runeType, out var resolver)) {
  447. return resolver.GetRuneForIntersects (driver, intersects);
  448. }
  449. // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
  450. var useDouble = intersects.Any (i => i?.Line.Style == LineStyle.Double);
  451. var useDashed = intersects.Any (i => i?.Line.Style == LineStyle.Dashed || i?.Line.Style == LineStyle.RoundedDashed);
  452. var useDotted = intersects.Any (i => i?.Line.Style == LineStyle.Dotted || i?.Line.Style == LineStyle.RoundedDotted);
  453. // horiz and vert lines same as Single for Rounded
  454. var useThick = intersects.Any (i => i?.Line.Style == LineStyle.Heavy);
  455. var useThickDashed = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDashed);
  456. var useThickDotted = intersects.Any (i => i?.Line.Style == LineStyle.HeavyDotted);
  457. // TODO: Support ruler
  458. //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
  459. // TODO: maybe make these resolvers too for simplicity?
  460. switch (runeType) {
  461. case IntersectionRuneType.None:
  462. return null;
  463. case IntersectionRuneType.Dot:
  464. return (Rune)CM.Glyphs.Dot;
  465. case IntersectionRuneType.HLine:
  466. if (useDouble) {
  467. return CM.Glyphs.HLineDbl;
  468. }
  469. if (useDashed) {
  470. return CM.Glyphs.HLineDa2;
  471. }
  472. if (useDotted) {
  473. return CM.Glyphs.HLineDa3;
  474. }
  475. return useThick ? CM.Glyphs.HLineHv : (useThickDashed ? CM.Glyphs.HLineHvDa2 : (useThickDotted ? CM.Glyphs.HLineHvDa3 : CM.Glyphs.HLine));
  476. case IntersectionRuneType.VLine:
  477. if (useDouble) {
  478. return CM.Glyphs.VLineDbl;
  479. }
  480. if (useDashed) {
  481. return CM.Glyphs.VLineDa3;
  482. }
  483. if (useDotted) {
  484. return CM.Glyphs.VLineDa4;
  485. }
  486. return useThick ? CM.Glyphs.VLineHv : (useThickDashed ? CM.Glyphs.VLineHvDa3 : (useThickDotted ? CM.Glyphs.VLineHvDa4 : CM.Glyphs.VLine));
  487. default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType);
  488. }
  489. }
  490. private Attribute? GetAttributeForIntersects (IntersectionDefinition? [] intersects)
  491. {
  492. var set = new List<IntersectionDefinition?> (intersects.Where (i => i!.Line.Attribute?.HasValidColors ?? false));
  493. if (set.Count == 0) {
  494. return null;
  495. }
  496. return set [0]!.Line.Attribute;
  497. }
  498. private Cell? GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition? [] intersects)
  499. {
  500. if (!intersects.Any ()) {
  501. return null;
  502. }
  503. var cell = new Cell ();
  504. var rune = GetRuneForIntersects (driver, intersects);
  505. if (rune.HasValue) {
  506. cell.Runes.Add (rune.Value);
  507. }
  508. cell.Attribute = GetAttributeForIntersects (intersects);
  509. return cell;
  510. }
  511. private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition? [] intersects)
  512. {
  513. var set = new HashSet<IntersectionType> (intersects.Select (i => i!.Type));
  514. #region Cross Conditions
  515. if (Has (set,
  516. IntersectionType.PassOverHorizontal,
  517. IntersectionType.PassOverVertical
  518. )) {
  519. return IntersectionRuneType.Cross;
  520. }
  521. if (Has (set,
  522. IntersectionType.PassOverVertical,
  523. IntersectionType.StartLeft,
  524. IntersectionType.StartRight
  525. )) {
  526. return IntersectionRuneType.Cross;
  527. }
  528. if (Has (set,
  529. IntersectionType.PassOverHorizontal,
  530. IntersectionType.StartUp,
  531. IntersectionType.StartDown
  532. )) {
  533. return IntersectionRuneType.Cross;
  534. }
  535. if (Has (set,
  536. IntersectionType.StartLeft,
  537. IntersectionType.StartRight,
  538. IntersectionType.StartUp,
  539. IntersectionType.StartDown)) {
  540. return IntersectionRuneType.Cross;
  541. }
  542. #endregion
  543. #region Corner Conditions
  544. if (Exactly (set,
  545. IntersectionType.StartRight,
  546. IntersectionType.StartDown)) {
  547. return IntersectionRuneType.ULCorner;
  548. }
  549. if (Exactly (set,
  550. IntersectionType.StartLeft,
  551. IntersectionType.StartDown)) {
  552. return IntersectionRuneType.URCorner;
  553. }
  554. if (Exactly (set,
  555. IntersectionType.StartUp,
  556. IntersectionType.StartLeft)) {
  557. return IntersectionRuneType.LRCorner;
  558. }
  559. if (Exactly (set,
  560. IntersectionType.StartUp,
  561. IntersectionType.StartRight)) {
  562. return IntersectionRuneType.LLCorner;
  563. }
  564. #endregion Corner Conditions
  565. #region T Conditions
  566. if (Has (set,
  567. IntersectionType.PassOverHorizontal,
  568. IntersectionType.StartDown)) {
  569. return IntersectionRuneType.TopTee;
  570. }
  571. if (Has (set,
  572. IntersectionType.StartRight,
  573. IntersectionType.StartLeft,
  574. IntersectionType.StartDown)) {
  575. return IntersectionRuneType.TopTee;
  576. }
  577. if (Has (set,
  578. IntersectionType.PassOverHorizontal,
  579. IntersectionType.StartUp)) {
  580. return IntersectionRuneType.BottomTee;
  581. }
  582. if (Has (set,
  583. IntersectionType.StartRight,
  584. IntersectionType.StartLeft,
  585. IntersectionType.StartUp)) {
  586. return IntersectionRuneType.BottomTee;
  587. }
  588. if (Has (set,
  589. IntersectionType.PassOverVertical,
  590. IntersectionType.StartRight)) {
  591. return IntersectionRuneType.LeftTee;
  592. }
  593. if (Has (set,
  594. IntersectionType.StartRight,
  595. IntersectionType.StartDown,
  596. IntersectionType.StartUp)) {
  597. return IntersectionRuneType.LeftTee;
  598. }
  599. if (Has (set,
  600. IntersectionType.PassOverVertical,
  601. IntersectionType.StartLeft)) {
  602. return IntersectionRuneType.RightTee;
  603. }
  604. if (Has (set,
  605. IntersectionType.StartLeft,
  606. IntersectionType.StartDown,
  607. IntersectionType.StartUp)) {
  608. return IntersectionRuneType.RightTee;
  609. }
  610. #endregion
  611. if (All (intersects, Orientation.Horizontal)) {
  612. return IntersectionRuneType.HLine;
  613. }
  614. if (All (intersects, Orientation.Vertical)) {
  615. return IntersectionRuneType.VLine;
  616. }
  617. return IntersectionRuneType.Dot;
  618. }
  619. private bool All (IntersectionDefinition? [] intersects, Orientation orientation)
  620. {
  621. return intersects.All (i => i!.Line.Orientation == orientation);
  622. }
  623. /// <summary>
  624. /// Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/>
  625. /// specified (i.e. AND).
  626. /// </summary>
  627. /// <param name="intersects"></param>
  628. /// <param name="types"></param>
  629. /// <returns></returns>
  630. private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types)
  631. {
  632. return types.All (t => intersects.Contains (t));
  633. }
  634. /// <summary>
  635. /// Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/>
  636. /// and there are no additional <see cref="IntersectionRuneType"/>
  637. /// </summary>
  638. /// <param name="intersects"></param>
  639. /// <param name="types"></param>
  640. /// <returns></returns>
  641. private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types)
  642. {
  643. return intersects.SetEquals (types);
  644. }
  645. /// <summary>
  646. /// Merges one line canvas into this one.
  647. /// </summary>
  648. /// <param name="lineCanvas"></param>
  649. public void Merge (LineCanvas lineCanvas)
  650. {
  651. foreach (var line in lineCanvas._lines) {
  652. AddLine (line);
  653. }
  654. }
  655. /// <inheritdoc />
  656. public void Dispose ()
  657. {
  658. ConfigurationManager.Applied -= ConfigurationManager_Applied;
  659. }
  660. }
  661. internal class IntersectionDefinition {
  662. /// <summary>
  663. /// The point at which the intersection happens
  664. /// </summary>
  665. internal Point Point { get; }
  666. /// <summary>
  667. /// Defines how <see cref="Line"/> position relates
  668. /// to <see cref="Point"/>.
  669. /// </summary>
  670. internal IntersectionType Type { get; }
  671. /// <summary>
  672. /// The line that intersects <see cref="Point"/>
  673. /// </summary>
  674. internal StraightLine Line { get; }
  675. internal IntersectionDefinition (Point point, IntersectionType type, StraightLine line)
  676. {
  677. Point = point;
  678. Type = type;
  679. Line = line;
  680. }
  681. }
  682. /// <summary>
  683. /// The type of Rune that we will use before considering
  684. /// double width, curved borders etc
  685. /// </summary>
  686. internal enum IntersectionRuneType {
  687. None,
  688. Dot,
  689. ULCorner,
  690. URCorner,
  691. LLCorner,
  692. LRCorner,
  693. TopTee,
  694. BottomTee,
  695. RightTee,
  696. LeftTee,
  697. Cross,
  698. HLine,
  699. VLine,
  700. }
  701. internal enum IntersectionType {
  702. /// <summary>
  703. /// There is no intersection
  704. /// </summary>
  705. None,
  706. /// <summary>
  707. /// A line passes directly over this point traveling along
  708. /// the horizontal axis
  709. /// </summary>
  710. PassOverHorizontal,
  711. /// <summary>
  712. /// A line passes directly over this point traveling along
  713. /// the vertical axis
  714. /// </summary>
  715. PassOverVertical,
  716. /// <summary>
  717. /// A line starts at this point and is traveling up
  718. /// </summary>
  719. StartUp,
  720. /// <summary>
  721. /// A line starts at this point and is traveling right
  722. /// </summary>
  723. StartRight,
  724. /// <summary>
  725. /// A line starts at this point and is traveling down
  726. /// </summary>
  727. StartDown,
  728. /// <summary>
  729. /// A line starts at this point and is traveling left
  730. /// </summary>
  731. StartLeft,
  732. /// <summary>
  733. /// A line exists at this point who has 0 length
  734. /// </summary>
  735. Dot
  736. }
  737. }