LineCanvas.cs 23 KB

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