LineCanvas.cs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Text;
  6. using Rune = System.Rune;
  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 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 glyphs.
  30. /// </summary>
  31. Double,
  32. /// <summary>
  33. /// The border is drawn using heavy line 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. private List<StraightLine> _lines = new List<StraightLine> ();
  68. Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
  69. {IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()},
  70. {IntersectionRuneType.URCorner,new URIntersectionRuneResolver()},
  71. {IntersectionRuneType.LLCorner,new LLIntersectionRuneResolver()},
  72. {IntersectionRuneType.LRCorner,new LRIntersectionRuneResolver()},
  73. {IntersectionRuneType.TopTee,new TopTeeIntersectionRuneResolver()},
  74. {IntersectionRuneType.LeftTee,new LeftTeeIntersectionRuneResolver()},
  75. {IntersectionRuneType.RightTee,new RightTeeIntersectionRuneResolver()},
  76. {IntersectionRuneType.BottomTee,new BottomTeeIntersectionRuneResolver()},
  77. {IntersectionRuneType.Crosshair,new CrosshairIntersectionRuneResolver()},
  78. // TODO: Add other resolvers
  79. };
  80. /// <summary>
  81. /// <para>
  82. /// Adds a new <paramref name="length"/> long line to the canvas starting at <paramref name="start"/>.
  83. /// </para>
  84. /// <para>
  85. /// Use positive <paramref name="length"/> for the line to extend Right and negative for Left
  86. /// when <see cref="Orientation"/> is <see cref="Orientation.Horizontal"/>.
  87. /// </para>
  88. /// <para>
  89. /// Use positive <paramref name="length"/> for the line to extend Down and negative for Up
  90. /// when <see cref="Orientation"/> is <see cref="Orientation.Vertical"/>.
  91. /// </para>
  92. /// </summary>
  93. /// <param name="start">Starting point.</param>
  94. /// <param name="length">The length of line. 0 for an intersection (cross or T). Positive for Down/Right. Negative for Up/Left.</param>
  95. /// <param name="orientation">The direction of the line.</param>
  96. /// <param name="style">The style of line to use</param>
  97. /// <param name="attribute"></param>
  98. public void AddLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default)
  99. {
  100. _cachedBounds = Rect.Empty;
  101. _lines.Add (new StraightLine (start, length, orientation, style, attribute));
  102. }
  103. private void AddLine (StraightLine line)
  104. {
  105. _cachedBounds = Rect.Empty;
  106. _lines.Add (line);
  107. }
  108. /// <summary>
  109. /// Clears all lines from the LineCanvas.
  110. /// </summary>
  111. public void Clear ()
  112. {
  113. _cachedBounds = Rect.Empty;
  114. _lines.Clear ();
  115. }
  116. private Rect _cachedBounds;
  117. /// <summary>
  118. /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the
  119. /// line that is furthest left/top and Size is defined by the line that extends the furthest
  120. /// right/bottom.
  121. /// </summary>
  122. public Rect Bounds {
  123. get {
  124. if (_cachedBounds.IsEmpty) {
  125. if (_lines.Count == 0) {
  126. return _cachedBounds;
  127. }
  128. Rect bounds = _lines [0].Bounds;
  129. for (var i = 1; i < _lines.Count; i++) {
  130. var line = _lines [i];
  131. var lineBounds = line.Bounds;
  132. bounds = Rect.Union (bounds, lineBounds);
  133. }
  134. if (bounds.Width == 0) {
  135. bounds.Width = 1;
  136. }
  137. if (bounds.Height == 0) {
  138. bounds.Height = 1;
  139. }
  140. _cachedBounds = new Rect (bounds.X, bounds.Y, bounds.Width, bounds.Height);
  141. }
  142. return _cachedBounds;
  143. }
  144. }
  145. // TODO: Unless there's an obvious use case for this API we should delete it in favor of the
  146. // simpler version that doensn't take an area.
  147. /// <summary>
  148. /// Evaluates the lines that have been added to the canvas and returns a map containing
  149. /// the glyphs and their locations. The glyphs are the characters that should be rendered
  150. /// so that all lines connect up with the appropriate intersection symbols.
  151. /// </summary>
  152. /// <param name="inArea">A rectangle to constrain the search by.</param>
  153. /// <returns>A map of the points within the canvas that intersect with <paramref name="inArea"/>.</returns>
  154. public Dictionary<Point, Rune> GetMap (Rect inArea)
  155. {
  156. var map = new Dictionary<Point, Rune> ();
  157. // walk through each pixel of the bitmap
  158. for (int y = inArea.Y; y < inArea.Y + inArea.Height; y++) {
  159. for (int x = inArea.X; x < inArea.X + inArea.Width; x++) {
  160. var intersects = _lines
  161. .Select (l => l.Intersects (x, y))
  162. .Where (i => i != null)
  163. .ToArray ();
  164. var rune = GetRuneForIntersects (Application.Driver, intersects);
  165. if (rune != null) {
  166. map.Add (new Point (x, y), rune.Value);
  167. }
  168. }
  169. }
  170. return map;
  171. }
  172. /// <summary>
  173. /// Evaluates the lines that have been added to the canvas and returns a map containing
  174. /// the glyphs and their locations. The glyphs are the characters that should be rendered
  175. /// so that all lines connect up with the appropriate intersection symbols.
  176. /// </summary>
  177. /// <returns>A map of all the points within the canvas.</returns>
  178. public Dictionary<Point, Cell> GetCellMap ()
  179. {
  180. var map = new Dictionary<Point, Cell> ();
  181. // walk through each pixel of the bitmap
  182. for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++) {
  183. for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++) {
  184. var intersects = _lines
  185. .Select (l => l.Intersects (x, y))
  186. .Where (i => i != null)
  187. .ToArray ();
  188. var cell = GetCellForIntersects (Application.Driver, intersects);
  189. if (cell != null) {
  190. map.Add (new Point (x, y), cell);
  191. }
  192. }
  193. }
  194. return map;
  195. }
  196. /// <summary>
  197. /// Evaluates the lines that have been added to the canvas and returns a map containing
  198. /// the glyphs and their locations. The glyphs are the characters that should be rendered
  199. /// so that all lines connect up with the appropriate intersection symbols.
  200. /// </summary>
  201. /// <returns>A map of all the points within the canvas.</returns>
  202. public Dictionary<Point, Rune> GetMap () => GetMap (Bounds);
  203. /// <summary>
  204. /// Returns the contents of the line canvas rendered to a string. The string
  205. /// will include all columns and rows, even if <see cref="Bounds"/> has negative coordinates.
  206. /// For example, if the canvas contains a single line that starts at (-1,-1) with a length of 2, the
  207. /// rendered string will have a length of 2.
  208. /// </summary>
  209. /// <returns>The canvas rendered to a string.</returns>
  210. public override string ToString ()
  211. {
  212. if (Bounds.IsEmpty) {
  213. return string.Empty;
  214. }
  215. // Generate the rune map for the entire canvas
  216. var runeMap = GetMap ();
  217. // Create the rune canvas
  218. Rune [,] canvas = new Rune [Bounds.Height, Bounds.Width];
  219. // Copy the rune map to the canvas, adjusting for any negative coordinates
  220. foreach (var kvp in runeMap) {
  221. int x = kvp.Key.X - Bounds.X;
  222. int y = kvp.Key.Y - Bounds.Y;
  223. canvas [y, x] = kvp.Value;
  224. }
  225. // Convert the canvas to a string
  226. StringBuilder sb = new StringBuilder ();
  227. for (int y = 0; y < canvas.GetLength (0); y++) {
  228. for (int x = 0; x < canvas.GetLength (1); x++) {
  229. Rune r = canvas [y, x];
  230. sb.Append (r.Value == 0 ? ' ' : r.ToString ());
  231. }
  232. if (y < canvas.GetLength (0) - 1) {
  233. sb.AppendLine ();
  234. }
  235. }
  236. return sb.ToString ();
  237. }
  238. private abstract class IntersectionRuneResolver {
  239. readonly Rune round;
  240. readonly Rune doubleH;
  241. readonly Rune doubleV;
  242. readonly Rune doubleBoth;
  243. readonly Rune thickH;
  244. readonly Rune thickV;
  245. readonly Rune thickBoth;
  246. readonly Rune normal;
  247. public IntersectionRuneResolver (Rune round, Rune doubleH, Rune doubleV, Rune doubleBoth, Rune thickH, Rune thickV, Rune thickBoth, Rune normal)
  248. {
  249. this.round = round;
  250. this.doubleH = doubleH;
  251. this.doubleV = doubleV;
  252. this.doubleBoth = doubleBoth;
  253. this.thickH = thickH;
  254. this.thickV = thickV;
  255. this.thickBoth = thickBoth;
  256. this.normal = normal;
  257. }
  258. public Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
  259. {
  260. var useRounded = intersects.Any (i => i.Line.Length != 0 && (
  261. i.Line.Style == LineStyle.Rounded || i.Line.Style == LineStyle.RoundedDashed || i.Line.Style == LineStyle.RoundedDotted));
  262. // Note that there aren't any glyphs for intersections of double lines with heavy lines
  263. bool doubleHorizontal = intersects.Any (l => l.Line.Orientation == Orientation.Horizontal && l.Line.Style == LineStyle.Double);
  264. bool doubleVertical = intersects.Any (l => l.Line.Orientation == Orientation.Vertical && l.Line.Style == LineStyle.Double);
  265. bool thickHorizontal = intersects.Any (l => l.Line.Orientation == Orientation.Horizontal && (
  266. l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
  267. bool thickVertical = intersects.Any (l => l.Line.Orientation == Orientation.Vertical && (
  268. l.Line.Style == LineStyle.Heavy || l.Line.Style == LineStyle.HeavyDashed || l.Line.Style == LineStyle.HeavyDotted));
  269. if (doubleHorizontal) {
  270. return doubleVertical ? doubleBoth : doubleH;
  271. }
  272. if (doubleVertical) {
  273. return doubleV;
  274. }
  275. if (thickHorizontal) {
  276. return thickVertical ? thickBoth : thickH;
  277. }
  278. if (thickVertical) {
  279. return thickV;
  280. }
  281. return useRounded ? round : normal;
  282. }
  283. }
  284. private class ULIntersectionRuneResolver : IntersectionRuneResolver {
  285. public ULIntersectionRuneResolver () :
  286. base ('╭', '╒', '╓', '╔', '┍', '┎', '┏', '┌')
  287. {
  288. }
  289. }
  290. private class URIntersectionRuneResolver : IntersectionRuneResolver {
  291. public URIntersectionRuneResolver () :
  292. base ('╮', '╕', '╖', '╗', '┑', '┒', '┓', '┐')
  293. {
  294. }
  295. }
  296. private class LLIntersectionRuneResolver : IntersectionRuneResolver {
  297. public LLIntersectionRuneResolver () :
  298. base ('╰', '╘', '╙', '╚', '┕', '┖', '┗', '└')
  299. {
  300. }
  301. }
  302. private class LRIntersectionRuneResolver : IntersectionRuneResolver {
  303. public LRIntersectionRuneResolver () :
  304. base ('╯', '╛', '╜', '╝', '┙', '┚', '┛', '┘')
  305. {
  306. }
  307. }
  308. private class TopTeeIntersectionRuneResolver : IntersectionRuneResolver {
  309. public TopTeeIntersectionRuneResolver () :
  310. base ('┬', '╤', '╥', '╦', '┯', '┰', '┳', '┬')
  311. {
  312. }
  313. }
  314. private class LeftTeeIntersectionRuneResolver : IntersectionRuneResolver {
  315. public LeftTeeIntersectionRuneResolver () :
  316. base ('├', '╞', '╟', '╠', '┝', '┠', '┣', '├')
  317. {
  318. }
  319. }
  320. private class RightTeeIntersectionRuneResolver : IntersectionRuneResolver {
  321. public RightTeeIntersectionRuneResolver () :
  322. base ('┤', '╡', '╢', '╣', '┥', '┨', '┫', '┤')
  323. {
  324. }
  325. }
  326. private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver {
  327. public BottomTeeIntersectionRuneResolver () :
  328. base ('┴', '╧', '╨', '╩', '┷', '┸', '┻', '┴')
  329. {
  330. }
  331. }
  332. private class CrosshairIntersectionRuneResolver : IntersectionRuneResolver {
  333. public CrosshairIntersectionRuneResolver () :
  334. base ('┼', '╪', '╫', '╬', '┿', '╂', '╋', '┼')
  335. {
  336. }
  337. }
  338. private Rune? GetRuneForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
  339. {
  340. if (!intersects.Any ()) {
  341. return null;
  342. }
  343. var runeType = GetRuneTypeForIntersects (intersects);
  344. if (runeResolvers.ContainsKey (runeType)) {
  345. return runeResolvers [runeType].GetRuneForIntersects (driver, intersects);
  346. }
  347. // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers
  348. var useDouble = intersects.Any (i => i.Line.Style == LineStyle.Double);
  349. var useDashed = intersects.Any (i => i.Line.Style == LineStyle.Dashed || i.Line.Style == LineStyle.RoundedDashed);
  350. var useDotted = intersects.Any (i => i.Line.Style == LineStyle.Dotted || i.Line.Style == LineStyle.RoundedDotted);
  351. // horiz and vert lines same as Single for Rounded
  352. var useThick = intersects.Any (i => i.Line.Style == LineStyle.Heavy);
  353. var useThickDashed = intersects.Any (i => i.Line.Style == LineStyle.HeavyDashed);
  354. var useThickDotted = intersects.Any (i => i.Line.Style == LineStyle.HeavyDotted);
  355. // TODO: Support ruler
  356. //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0);
  357. // TODO: maybe make these resolvers too for simplicity?
  358. switch (runeType) {
  359. case IntersectionRuneType.None:
  360. return null;
  361. case IntersectionRuneType.Dot:
  362. return (Rune)'.';
  363. case IntersectionRuneType.HLine:
  364. if (useDouble) {
  365. return driver.HDbLine;
  366. }
  367. if (useDashed) {
  368. return driver.HDsLine;
  369. }
  370. if (useDotted) {
  371. return driver.HDtLine;
  372. }
  373. return useThick ? driver.HThLine : (useThickDashed ? driver.HThDsLine : (useThickDotted ? driver.HThDtLine : driver.HLine));
  374. case IntersectionRuneType.VLine:
  375. if (useDouble) {
  376. return driver.VDbLine;
  377. }
  378. if (useDashed) {
  379. return driver.VDsLine;
  380. }
  381. if (useDotted) {
  382. return driver.VDtLine;
  383. }
  384. return useThick ? driver.VThLine : (useThickDashed ? driver.VThDsLine : (useThickDotted ? driver.VThDtLine : driver.VLine));
  385. default: throw new Exception ("Could not find resolver or switch case for " + nameof (runeType) + ":" + runeType);
  386. }
  387. }
  388. private Attribute? GetAttributeForIntersects (IntersectionDefinition [] intersects)
  389. {
  390. var set = new List<IntersectionDefinition> (intersects.Where (i => i.Line.Attribute?.HasValidColors ?? false));
  391. if (set.Count == 0) {
  392. return null;
  393. }
  394. return set [0].Line.Attribute;
  395. }
  396. /// <summary>
  397. /// Represents a single row/column within the <see cref="LineCanvas"/>. Includes the glyph and the foreground/background colors.
  398. /// </summary>
  399. public class Cell
  400. {
  401. /// <summary>
  402. /// The glyph to draw.
  403. /// </summary>
  404. public Rune? Rune { get; set; }
  405. /// <summary>
  406. /// The foreground color to draw the glyph with.
  407. /// </summary>
  408. public Attribute? Attribute { get; set; }
  409. }
  410. private Cell GetCellForIntersects (ConsoleDriver driver, IntersectionDefinition [] intersects)
  411. {
  412. if (!intersects.Any ()) {
  413. return null;
  414. }
  415. var cell = new Cell ();
  416. cell.Rune = GetRuneForIntersects (driver, intersects);
  417. cell.Attribute = GetAttributeForIntersects (intersects);
  418. return cell;
  419. }
  420. private IntersectionRuneType GetRuneTypeForIntersects (IntersectionDefinition [] intersects)
  421. {
  422. var set = new HashSet<IntersectionType> (intersects.Select (i => i.Type));
  423. #region Crosshair Conditions
  424. if (Has (set,
  425. IntersectionType.PassOverHorizontal,
  426. IntersectionType.PassOverVertical
  427. )) {
  428. return IntersectionRuneType.Crosshair;
  429. }
  430. if (Has (set,
  431. IntersectionType.PassOverVertical,
  432. IntersectionType.StartLeft,
  433. IntersectionType.StartRight
  434. )) {
  435. return IntersectionRuneType.Crosshair;
  436. }
  437. if (Has (set,
  438. IntersectionType.PassOverHorizontal,
  439. IntersectionType.StartUp,
  440. IntersectionType.StartDown
  441. )) {
  442. return IntersectionRuneType.Crosshair;
  443. }
  444. if (Has (set,
  445. IntersectionType.StartLeft,
  446. IntersectionType.StartRight,
  447. IntersectionType.StartUp,
  448. IntersectionType.StartDown)) {
  449. return IntersectionRuneType.Crosshair;
  450. }
  451. #endregion
  452. #region Corner Conditions
  453. if (Exactly (set,
  454. IntersectionType.StartRight,
  455. IntersectionType.StartDown)) {
  456. return IntersectionRuneType.ULCorner;
  457. }
  458. if (Exactly (set,
  459. IntersectionType.StartLeft,
  460. IntersectionType.StartDown)) {
  461. return IntersectionRuneType.URCorner;
  462. }
  463. if (Exactly (set,
  464. IntersectionType.StartUp,
  465. IntersectionType.StartLeft)) {
  466. return IntersectionRuneType.LRCorner;
  467. }
  468. if (Exactly (set,
  469. IntersectionType.StartUp,
  470. IntersectionType.StartRight)) {
  471. return IntersectionRuneType.LLCorner;
  472. }
  473. #endregion Corner Conditions
  474. #region T Conditions
  475. if (Has (set,
  476. IntersectionType.PassOverHorizontal,
  477. IntersectionType.StartDown)) {
  478. return IntersectionRuneType.TopTee;
  479. }
  480. if (Has (set,
  481. IntersectionType.StartRight,
  482. IntersectionType.StartLeft,
  483. IntersectionType.StartDown)) {
  484. return IntersectionRuneType.TopTee;
  485. }
  486. if (Has (set,
  487. IntersectionType.PassOverHorizontal,
  488. IntersectionType.StartUp)) {
  489. return IntersectionRuneType.BottomTee;
  490. }
  491. if (Has (set,
  492. IntersectionType.StartRight,
  493. IntersectionType.StartLeft,
  494. IntersectionType.StartUp)) {
  495. return IntersectionRuneType.BottomTee;
  496. }
  497. if (Has (set,
  498. IntersectionType.PassOverVertical,
  499. IntersectionType.StartRight)) {
  500. return IntersectionRuneType.LeftTee;
  501. }
  502. if (Has (set,
  503. IntersectionType.StartRight,
  504. IntersectionType.StartDown,
  505. IntersectionType.StartUp)) {
  506. return IntersectionRuneType.LeftTee;
  507. }
  508. if (Has (set,
  509. IntersectionType.PassOverVertical,
  510. IntersectionType.StartLeft)) {
  511. return IntersectionRuneType.RightTee;
  512. }
  513. if (Has (set,
  514. IntersectionType.StartLeft,
  515. IntersectionType.StartDown,
  516. IntersectionType.StartUp)) {
  517. return IntersectionRuneType.RightTee;
  518. }
  519. #endregion
  520. if (All (intersects, Orientation.Horizontal)) {
  521. return IntersectionRuneType.HLine;
  522. }
  523. if (All (intersects, Orientation.Vertical)) {
  524. return IntersectionRuneType.VLine;
  525. }
  526. return IntersectionRuneType.Dot;
  527. }
  528. private bool All (IntersectionDefinition [] intersects, Orientation orientation)
  529. {
  530. return intersects.All (i => i.Line.Orientation == orientation);
  531. }
  532. /// <summary>
  533. /// Returns true if the <paramref name="intersects"/> collection has all the <paramref name="types"/>
  534. /// specified (i.e. AND).
  535. /// </summary>
  536. /// <param name="intersects"></param>
  537. /// <param name="types"></param>
  538. /// <returns></returns>
  539. private bool Has (HashSet<IntersectionType> intersects, params IntersectionType [] types)
  540. {
  541. return types.All (t => intersects.Contains (t));
  542. }
  543. /// <summary>
  544. /// Returns true if all requested <paramref name="types"/> appear in <paramref name="intersects"/>
  545. /// and there are no additional <see cref="IntersectionRuneType"/>
  546. /// </summary>
  547. /// <param name="intersects"></param>
  548. /// <param name="types"></param>
  549. /// <returns></returns>
  550. private bool Exactly (HashSet<IntersectionType> intersects, params IntersectionType [] types)
  551. {
  552. return intersects.SetEquals (types);
  553. }
  554. /// <summary>
  555. /// Merges one line canvas into this one.
  556. /// </summary>
  557. /// <param name="lineCanvas"></param>
  558. public void Merge (LineCanvas lineCanvas)
  559. {
  560. foreach (var line in lineCanvas._lines) {
  561. AddLine (line);
  562. }
  563. }
  564. internal class IntersectionDefinition {
  565. /// <summary>
  566. /// The point at which the intersection happens
  567. /// </summary>
  568. internal Point Point { get; }
  569. /// <summary>
  570. /// Defines how <see cref="Line"/> position relates
  571. /// to <see cref="Point"/>.
  572. /// </summary>
  573. internal IntersectionType Type { get; }
  574. /// <summary>
  575. /// The line that intersects <see cref="Point"/>
  576. /// </summary>
  577. internal StraightLine Line { get; }
  578. internal IntersectionDefinition (Point point, IntersectionType type, StraightLine line)
  579. {
  580. Point = point;
  581. Type = type;
  582. Line = line;
  583. }
  584. }
  585. /// <summary>
  586. /// The type of Rune that we will use before considering
  587. /// double width, curved borders etc
  588. /// </summary>
  589. internal enum IntersectionRuneType {
  590. None,
  591. Dot,
  592. ULCorner,
  593. URCorner,
  594. LLCorner,
  595. LRCorner,
  596. TopTee,
  597. BottomTee,
  598. RightTee,
  599. LeftTee,
  600. Crosshair,
  601. HLine,
  602. VLine,
  603. }
  604. internal enum IntersectionType {
  605. /// <summary>
  606. /// There is no intersection
  607. /// </summary>
  608. None,
  609. /// <summary>
  610. /// A line passes directly over this point traveling along
  611. /// the horizontal axis
  612. /// </summary>
  613. PassOverHorizontal,
  614. /// <summary>
  615. /// A line passes directly over this point traveling along
  616. /// the vertical axis
  617. /// </summary>
  618. PassOverVertical,
  619. /// <summary>
  620. /// A line starts at this point and is traveling up
  621. /// </summary>
  622. StartUp,
  623. /// <summary>
  624. /// A line starts at this point and is traveling right
  625. /// </summary>
  626. StartRight,
  627. /// <summary>
  628. /// A line starts at this point and is traveling down
  629. /// </summary>
  630. StartDown,
  631. /// <summary>
  632. /// A line starts at this point and is traveling left
  633. /// </summary>
  634. StartLeft,
  635. /// <summary>
  636. /// A line exists at this point who has 0 length
  637. /// </summary>
  638. Dot
  639. }
  640. // TODO: Add events that notify when StraightLine changes to enable dynamic layout
  641. internal class StraightLine {
  642. public Point Start { get; }
  643. public int Length { get; }
  644. public Orientation Orientation { get; }
  645. public LineStyle Style { get; }
  646. public Attribute? Attribute { get; set; }
  647. internal StraightLine (Point start, int length, Orientation orientation, LineStyle style, Attribute? attribute = default)
  648. {
  649. this.Start = start;
  650. this.Length = length;
  651. this.Orientation = orientation;
  652. this.Style = style;
  653. this.Attribute = attribute;
  654. }
  655. internal IntersectionDefinition Intersects (int x, int y)
  656. {
  657. switch (Orientation) {
  658. case Orientation.Horizontal: return IntersectsHorizontally (x, y);
  659. case Orientation.Vertical: return IntersectsVertically (x, y);
  660. default: throw new ArgumentOutOfRangeException (nameof (Orientation));
  661. }
  662. }
  663. private IntersectionDefinition IntersectsHorizontally (int x, int y)
  664. {
  665. if (Start.Y != y) {
  666. return null;
  667. } else {
  668. if (StartsAt (x, y)) {
  669. return new IntersectionDefinition (
  670. Start,
  671. GetTypeByLength (IntersectionType.StartLeft, IntersectionType.PassOverHorizontal, IntersectionType.StartRight),
  672. this
  673. );
  674. }
  675. if (EndsAt (x, y)) {
  676. return new IntersectionDefinition (
  677. Start,
  678. Length < 0 ? IntersectionType.StartRight : IntersectionType.StartLeft,
  679. this
  680. );
  681. } else {
  682. var xmin = Math.Min (Start.X, Start.X + Length);
  683. var xmax = Math.Max (Start.X, Start.X + Length);
  684. if (xmin < x && xmax > x) {
  685. return new IntersectionDefinition (
  686. new Point (x, y),
  687. IntersectionType.PassOverHorizontal,
  688. this
  689. );
  690. }
  691. }
  692. return null;
  693. }
  694. }
  695. private IntersectionDefinition IntersectsVertically (int x, int y)
  696. {
  697. if (Start.X != x) {
  698. return null;
  699. } else {
  700. if (StartsAt (x, y)) {
  701. return new IntersectionDefinition (
  702. Start,
  703. GetTypeByLength (IntersectionType.StartUp, IntersectionType.PassOverVertical, IntersectionType.StartDown),
  704. this
  705. );
  706. }
  707. if (EndsAt (x, y)) {
  708. return new IntersectionDefinition (
  709. Start,
  710. Length < 0 ? IntersectionType.StartDown : IntersectionType.StartUp,
  711. this
  712. );
  713. } else {
  714. var ymin = Math.Min (Start.Y, Start.Y + Length);
  715. var ymax = Math.Max (Start.Y, Start.Y + Length);
  716. if (ymin < y && ymax > y) {
  717. return new IntersectionDefinition (
  718. new Point (x, y),
  719. IntersectionType.PassOverVertical,
  720. this
  721. );
  722. }
  723. }
  724. return null;
  725. }
  726. }
  727. private IntersectionType GetTypeByLength (IntersectionType typeWhenNegative, IntersectionType typeWhenZero, IntersectionType typeWhenPositive)
  728. {
  729. if (Length == 0) {
  730. return typeWhenZero;
  731. }
  732. return Length < 0 ? typeWhenNegative : typeWhenPositive;
  733. }
  734. private bool EndsAt (int x, int y)
  735. {
  736. var sub = (Length == 0) ? 0 : (Length > 0) ? 1 : -1;
  737. if (Orientation == Orientation.Horizontal) {
  738. return Start.X + Length - sub == x && Start.Y == y;
  739. }
  740. return Start.X == x && Start.Y + Length - sub == y;
  741. }
  742. private bool StartsAt (int x, int y)
  743. {
  744. return Start.X == x && Start.Y == y;
  745. }
  746. /// <summary>
  747. /// Gets the rectangle that describes the bounds of the canvas. Location is the coordinates of the
  748. /// line that is furthest left/top and Size is defined by the line that extends the furthest
  749. /// right/bottom.
  750. /// </summary>
  751. internal Rect Bounds {
  752. get {
  753. // 0 and 1/-1 Length means a size (width or height) of 1
  754. var size = Math.Max (1, Math.Abs (Length));
  755. // How much to offset x or y to get the start of the line
  756. var offset = Math.Abs (Length < 0 ? Length + 1 : 0);
  757. var x = Start.X - (Orientation == Orientation.Horizontal ? offset : 0);
  758. var y = Start.Y - (Orientation == Orientation.Vertical ? offset : 0);
  759. var width = Orientation == Orientation.Horizontal ? size : 1;
  760. var height = Orientation == Orientation.Vertical ? size : 1;
  761. return new Rect (x, y, width, height);
  762. }
  763. }
  764. /// <summary>
  765. /// Formats the Line as a string in (Start.X,Start.Y,Length,Orientation) notation.
  766. /// </summary>
  767. public override string ToString ()
  768. {
  769. return $"({Start.X},{Start.Y},{Length},{Orientation})";
  770. }
  771. }
  772. }
  773. }