LineCanvas.cs 24 KB

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