LineCanvas.cs 28 KB

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