LineCanvas.cs 23 KB

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