Răsfoiți Sursa

Line canvas exclude method (#3004)

Thomas Nind 1 an în urmă
părinte
comite
8ed1b16ee1

+ 14 - 0
Terminal.Gui/Drawing/LineCanvas.cs

@@ -77,6 +77,15 @@ namespace Terminal.Gui {
 			ConfigurationManager.Applied += ConfigurationManager_Applied;
 		}
 
+		/// <summary>
+		/// Creates a new instance with the given <paramref name="lines"/>.
+		/// </summary>
+		/// <param name="lines">Initial lines for the canvas.</param>
+		public LineCanvas (IEnumerable<StraightLine> lines) : this()
+		{
+			_lines = lines.ToList();
+		}
+
 		private void ConfigurationManager_Applied (object? sender, ConfigurationManagerEventArgs e)
 		{
 			foreach (var irr in runeResolvers) {
@@ -86,6 +95,11 @@ namespace Terminal.Gui {
 
 		private List<StraightLine> _lines = new List<StraightLine> ();
 
+		/// <summary>
+		/// Gets the lines in the canvas.
+		/// </summary>
+		public IReadOnlyCollection<StraightLine> Lines { get { return _lines.AsReadOnly(); } }
+
 		Dictionary<IntersectionRuneType, IntersectionRuneResolver> runeResolvers = new Dictionary<IntersectionRuneType, IntersectionRuneResolver> {
 			{IntersectionRuneType.ULCorner,new ULIntersectionRuneResolver()},
 			{IntersectionRuneType.URCorner,new URIntersectionRuneResolver()},

+ 1 - 0
Terminal.Gui/Drawing/StraightLine.cs

@@ -1,4 +1,5 @@
 using System;
+
 namespace Terminal.Gui {
 #nullable enable
 	// TODO: Add events that notify when StraightLine changes to enable dynamic layout

+ 213 - 0
Terminal.Gui/Drawing/StraightLineExtensions.cs

@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Generic;
+
+namespace Terminal.Gui {
+
+	/// <summary>
+	/// Extension methods for <see cref="StraightLine"/> (including collections).
+	/// </summary>
+	public static class StraightLineExtensions {
+		/// <summary>
+		/// Splits or removes all lines in the <paramref name="collection"/> such that none cover the given
+		/// exclusion area.
+		/// </summary>
+		/// <param name="collection">Lines to adjust</param>
+		/// <param name="start">First point to remove from collection</param>
+		/// <param name="length">The number of sequential points to exclude</param>
+		/// <param name="orientation">Orientation of the exclusion line</param>
+		/// <returns></returns>
+		public static IEnumerable<StraightLine> Exclude (this IEnumerable<StraightLine> collection, Point start, int length, Orientation orientation)
+		{
+			var toReturn = new List<StraightLine> ();
+			if (length == 0) {
+				return collection;
+			}
+
+			foreach (var l in collection) {
+
+				if(l.Length == 0) {
+					toReturn.Add (l);
+					continue;
+				}
+
+				// lines are parallel.  For any straight line one axis (x or y) is constant
+				// e.g. Horizontal lines have constant y
+				int econstPoint = orientation == Orientation.Horizontal ? start.Y : start.X;
+				int lconstPoint = l.Orientation == Orientation.Horizontal ? l.Start.Y : l.Start.X;
+
+				// For the varying axis what is the max/mins
+				// i.e. points on horizontal lines vary by x, vertical lines vary by y
+				int eDiffMin = GetLineStartOnDiffAxis (start, length, orientation);
+				int eDiffMax = GetLineEndOnDiffAxis (start, length, orientation);
+				int lDiffMin = GetLineStartOnDiffAxis (l.Start, l.Length, l.Orientation);
+				int lDiffMax = GetLineEndOnDiffAxis (l.Start, l.Length, l.Orientation);
+
+				// line is parallel to exclusion
+				if (l.Orientation == orientation) {
+
+					// Do the parallel lines share constant plane
+					if (econstPoint != lconstPoint) {
+
+						// No, so no way they overlap
+						toReturn.Add (l);
+					} else {
+
+						
+
+						if (lDiffMax < eDiffMin) {
+							// Line ends before exclusion starts
+							toReturn.Add (l);
+						} else if (lDiffMin > eDiffMax) {
+							// Line starts after exclusion ends
+							toReturn.Add (l);
+						} else {
+							//lines overlap!
+
+							// Is there a bit we can keep on the left?
+							if (lDiffMin < eDiffMin) {
+								// Create line up to exclusion point
+								int from = lDiffMin;
+								int len = eDiffMin - lDiffMin;
+
+								if (len > 0) {
+									toReturn.Add (CreateLineFromDiff (l, from, len));
+								}
+							}
+
+							// Is there a bit we can keep on the right?
+							if (lDiffMax > eDiffMax) {
+								// Create line up to exclusion point
+								int from = eDiffMax + 1;
+								int len = lDiffMax - eDiffMax;
+
+								if (len > 0) {
+
+									// A single line with length 1 and -1 are the same (fills only the single cell)
+									// They differ only in how they join to other lines (i.e. to create corners)
+									// Using negative for the later half of the line ensures line joins in a way
+									// consistent with its pre-snipped state.
+									if (len == 1) {
+										len = -1;
+									}
+										
+									toReturn.Add (CreateLineFromDiff (l, from, len));
+								}
+							}
+						}
+					}
+
+				} else {
+					// line is perpendicular to exclusion
+
+					// Does the constant plane of the exclusion appear within the differing plane of the line?
+					if(econstPoint >= lDiffMin && econstPoint <= lDiffMax) {
+						// Yes, e.g. Vertical exclusion's x is within xmin/xmax of the horizontal line
+
+						// Vice versa must also be true
+						// for example there is no intersection if the vertical exclusion line does not
+						// stretch down far enough to reach the line
+						if(lconstPoint >= eDiffMin && lconstPoint <= eDiffMax) {
+
+							// Perpendicular intersection occurs here
+							var intersection = l.Orientation == Orientation.Horizontal ?
+								new Point (econstPoint,lconstPoint) :
+								new Point (lconstPoint,econstPoint);
+
+							// To snip out this single point we will use a recursive call
+							// snipping 1 length along the orientation of l (i.e. parallel)
+							toReturn.AddRange (new [] { l }.Exclude (intersection, 1, l.Orientation));
+						}
+						else {
+							// No intersection
+							toReturn.Add (l);
+						}
+
+					}
+					else {
+						// Lines do not intersect
+						toReturn.Add (l);
+					}
+
+				}
+			}
+
+			return toReturn;
+		}
+
+		/// <summary>
+		/// <para>Calculates the single digit point where a line starts on the differing axis
+		/// i.e. the minimum (controlling for negative lengths).</para>
+		/// <para>
+		/// For lines with <see cref="Orientation.Horizontal"/> this is an x coordinate.
+		/// For lines that are <see cref="Orientation.Vertical"/> this is a y coordinate.
+		/// </para>
+		/// </summary>
+		/// <param name="start">Where the line starts</param>
+		/// <param name="length">Length of the line</param>
+		/// <param name="orientation">Orientation of the line</param>
+		/// <returns>The minimum x or y (whichever is differing) point on the line, controlling for negative lengths. </returns>
+		private static int GetLineStartOnDiffAxis (Point start, int length, Orientation orientation)
+		{
+			if(length == 0) {
+				throw new ArgumentException ("0 length lines are not supported", nameof (length));
+			}
+
+			var sub = length > 0 ? 1 : -1;
+
+			if (orientation == Orientation.Vertical) {
+				// Points on line differ by y
+				return Math.Min (start.Y + length - sub, start.Y);
+			}
+
+			// Points on line differ by x
+			return Math.Min (start.X + length - sub, start.X);
+		}
+
+		/// <summary>
+		/// <para>Calculates the single digit point where a line ends on the differing axis
+		/// i.e. the maximum (controlling for negative lengths).</para>
+		/// <para>
+		/// For lines with <see cref="Orientation.Horizontal"/> this is an x coordinate.
+		/// For lines that are <see cref="Orientation.Vertical"/> this is a y coordinate.
+		/// </para>
+		/// </summary>
+		/// <param name="start">Where the line starts</param>
+		/// <param name="length">Length of the line</param>
+		/// <param name="orientation">Orientation of the line</param>
+		/// <returns>The maximum x or y (whichever is differing) point on the line, controlling for negative lengths. </returns>
+		private static int GetLineEndOnDiffAxis (Point start, int length, Orientation orientation)
+		{
+			if (length == 0) {
+				throw new ArgumentException ("0 length lines are not supported", nameof (length));
+			}
+
+			var sub = length > 0 ? 1 : -1;
+
+			if (orientation == Orientation.Vertical) {
+				// Points on line differ by y
+				return Math.Max (start.Y + length - sub, start.Y);
+			}
+
+			// Points on line differ by x
+			return Math.Max (start.X + length - sub, start.X);
+		}
+
+		/// <summary>
+		/// Creates a new line which is part of <paramref name="l"/> from the point on the varying
+		/// axis <paramref name="from"/> to <paramref name="length"/>.  Horizontal lines have points that
+		/// vary by x while vertical lines have points that vary by y
+		/// </summary>
+		/// <param name="l">Line to create sub part from</param>
+		/// <param name="from">Point on varying axis to start at</param>
+		/// <param name="length">Length of line to return</param>
+		/// <returns>The new line</returns>
+		private static StraightLine CreateLineFromDiff (StraightLine l, int from, int length)
+		{
+			var start = new Point (
+				l.Orientation == Orientation.Horizontal ? from : l.Start.X,
+				l.Orientation == Orientation.Horizontal ? l.Start.Y : from);
+
+			return new StraightLine (start, length, l.Orientation, l.Style, l.Attribute);
+		}
+	}
+}

+ 2 - 1
Terminal.Gui/View/ViewDrawing.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 
@@ -211,7 +212,7 @@ namespace Terminal.Gui {
 			var h = regionScreen.Y + regionScreen.Height >= Driver.Rows ? Driver.Rows - regionScreen.Y : regionScreen.Height;
 
 			return new Rect (x, y, w, h);
-		}
+		}		
 
 		/// <summary>
 		/// Expands the <see cref="ConsoleDriver"/>'s clip region to include <see cref="Bounds"/>.

+ 25 - 2
UICatalog/Scenarios/LineDrawing.cs

@@ -2,6 +2,7 @@
 using System.Collections.Generic;
 using System.Linq;
 using Terminal.Gui;
+using static Terminal.Gui.SpinnerStyle;
 
 namespace UICatalog.Scenarios {
 
@@ -76,10 +77,10 @@ namespace UICatalog.Scenarios {
 					X = 0,
 					Y = Pos.Bottom (_colorPicker)
 				};
-
 				_stylePicker.SelectedItemChanged += (s, a) => {
 					SetStyle?.Invoke ((LineStyle)a.SelectedItem);
 				};
+				_stylePicker.SelectedItem = 1;
 
 				_addLayerBtn = new Button () {
 					Text = "New Layer",
@@ -152,12 +153,14 @@ namespace UICatalog.Scenarios {
 			{
 				if (mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) {
 					if (_currentLine == null) {
-
+						// Mouse pressed down
 						_currentLine = new StraightLine (
 							new Point (mouseEvent.X - GetBoundsOffset ().X, mouseEvent.Y - GetBoundsOffset ().X),
 							0, Orientation.Vertical, LineStyle, new Attribute (_currentColor, GetNormalColor ().Background));
+						
 						_currentLayer.AddLine (_currentLine);
 					} else {
+						// Mouse dragged
 						var start = _currentLine.Start;
 						var end = new Point (mouseEvent.X - GetBoundsOffset ().X, mouseEvent.Y - GetBoundsOffset ().Y);
 						var orientation = Orientation.Vertical;
@@ -180,7 +183,27 @@ namespace UICatalog.Scenarios {
 						SetNeedsDisplay ();
 					}
 				} else {
+
+					// Mouse released
 					if (_currentLine != null) {
+
+						if(_currentLine.Length == 0) {
+							_currentLine.Length = 1;
+						}
+
+						if(_currentLine.Style == LineStyle.None) {
+
+							// Treat none as eraser
+							var idx = _layers.IndexOf (_currentLayer);
+							_layers.Remove (_currentLayer);
+
+							_currentLayer = new LineCanvas(
+								_currentLayer.Lines.Exclude (_currentLine.Start, _currentLine.Length, _currentLine.Orientation)
+								);
+
+							_layers.Insert (idx, _currentLayer);
+						}
+
 						_currentLine = null;
 						undoHistory.Clear ();
 						SetNeedsDisplay ();

+ 415 - 0
UnitTests/Drawing/StraightLineExtensionsTests.cs

@@ -0,0 +1,415 @@
+using System;
+using System.Linq;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Terminal.Gui.DrawingTests {
+	public class StraightLineExtensionsTests
+	{
+		private ITestOutputHelper _output;
+
+
+		public StraightLineExtensionsTests(ITestOutputHelper output)
+		{
+			this._output = output;
+		}
+
+		#region Parallel Tests
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_HorizontalLines_LeftOnly ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=3 to x=103
+						.Exclude (new Point (3, 2), 100, Orientation.Horizontal)
+						.ToArray ();
+			// x=1 to x=2
+			var afterLine = Assert.Single (after);
+			Assert.Equal (l1.Start, afterLine.Start);
+			Assert.Equal (2, afterLine.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_HorizontalLines_RightOnly ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=0 to x=2
+						.Exclude (new Point (0, 2), 3, Orientation.Horizontal)
+						.ToArray ();
+			// x=3 to x=10
+			var afterLine = Assert.Single (after);
+			Assert.Equal (3, afterLine.Start.X);
+			Assert.Equal (2, afterLine.Start.Y);
+			Assert.Equal (8, afterLine.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_HorizontalLines_HorizontalSplit ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=4 to x=5
+						.Exclude (new Point (4, 2), 2, Orientation.Horizontal)
+						.ToArray ();
+			// x=1 to x=3,
+			// x=6 to x=10
+			Assert.Equal (2, after.Length);
+			var afterLeft = after [0];
+			var afterRight = after [1];
+
+			Assert.Equal (1, afterLeft.Start.X);
+			Assert.Equal (2, afterLeft.Start.Y);
+			Assert.Equal (3, afterLeft.Length);
+
+			Assert.Equal (6, afterRight.Start.X);
+			Assert.Equal (2, afterRight.Start.Y);
+			Assert.Equal (5, afterRight.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_HorizontalLines_CoverCompletely ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=4 to x=5
+						.Exclude (new Point (1, 2), 10, Orientation.Horizontal)
+						.ToArray ();
+			Assert.Empty (after);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_VerticalLines_TopOnly ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2, 1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=3 to y=103
+						.Exclude (new Point (2, 3), 100, Orientation.Vertical)
+						.ToArray ();
+			// y=1 to y=2
+			var afterLine = Assert.Single (after);
+			Assert.Equal (l1.Start, afterLine.Start);
+			Assert.Equal (2, afterLine.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_HorizontalLines_BottomOnly ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2, 1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=0 to y=2
+						.Exclude (new Point (2,0), 3, Orientation.Vertical)
+						.ToArray ();
+			// y=3 to y=10
+			var afterLine = Assert.Single (after);
+			Assert.Equal (3, afterLine.Start.Y);
+			Assert.Equal (2, afterLine.Start.X);
+			Assert.Equal (8, afterLine.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_VerticalLines_VerticalSplit ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2,1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=4 to y=5
+						.Exclude (new Point (2, 4), 2, Orientation.Vertical)
+						.ToArray ();
+			// y=1 to y=3,
+			// y=6 to y=10
+			Assert.Equal (2, after.Length);
+			var afterLeft = after [0];
+			var afterRight = after [1];
+
+			Assert.Equal (1, afterLeft.Start.Y);
+			Assert.Equal (2, afterLeft.Start.X);
+			Assert.Equal (3, afterLeft.Length);
+
+			Assert.Equal (6, afterRight.Start.Y);
+			Assert.Equal (2, afterRight.Start.X);
+			Assert.Equal (5, afterRight.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludeParallel_VerticalLines_CoverCompletely ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2,1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=4 to y=5
+						.Exclude (new Point (2,1), 10, Orientation.Vertical)
+						.ToArray ();
+			Assert.Empty (after);
+		}
+
+
+		#endregion
+
+		#region Perpendicular Intersection Tests
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_HorizontalLine_VerticalExclusion_Splits ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=3 y=0-10
+						.Exclude (new Point (3, 0), 10, Orientation.Vertical)
+						.ToArray ();
+			// x=1 to x=2,
+			// x=4 to x=10
+			Assert.Equal (2, after.Length);
+			var afterLeft = after [0];
+			var afterRight = after [1];
+
+			Assert.Equal (1, afterLeft.Start.X);
+			Assert.Equal (2, afterLeft.Start.Y);
+			Assert.Equal (2, afterLeft.Length);
+
+			Assert.Equal (4, afterRight.Start.X);
+			Assert.Equal (2, afterRight.Start.Y);
+			Assert.Equal (7, afterRight.Length);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_HorizontalLine_VerticalExclusion_ClipLeft ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=1 y=0-10
+						.Exclude (new Point (1, 0), 10, Orientation.Vertical)
+						.ToArray ();
+			// x=2 to x=10,
+			var lineAfter = Assert.Single(after);
+
+			Assert.Equal (2, lineAfter.Start.X);
+			Assert.Equal (2, lineAfter.Start.Y);
+			Assert.Equal (9, lineAfter.Length);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_HorizontalLine_VerticalExclusion_ClipRight ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=10 y=0-10
+						.Exclude (new Point (10, 0), 10, Orientation.Vertical)
+						.ToArray ();
+			// x=1 to x=9,
+			var lineAfter = Assert.Single (after);
+
+			Assert.Equal (1, lineAfter.Start.X);
+			Assert.Equal (2, lineAfter.Start.Y);
+			Assert.Equal (9, lineAfter.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_HorizontalLine_VerticalExclusion_MissLeft ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=0 y=0-10
+						.Exclude (new Point (0, 0), 10, Orientation.Vertical)
+						.ToArray ();
+			// Exclusion line is too far to the left so hits nothing
+			Assert.Same(Assert.Single (after),l1);
+		}
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_HorizontalLine_VerticalExclusion_MissRight ()
+		{
+			// x=1 to x=10
+			var l1 = new StraightLine (new Point (1, 2), 10, Orientation.Horizontal, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude x=11 y=0-10
+						.Exclude (new Point (11, 0), 10, Orientation.Vertical)
+						.ToArray ();
+			// Exclusion line is too far to the right so hits nothing
+			Assert.Same (Assert.Single (after), l1);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_VerticalLine_HorizontalExclusion_ClipTop ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2,1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=1 x=0-10
+						.Exclude (new Point (0,1), 10, Orientation.Horizontal)
+						.ToArray ();
+			// y=2 to y=10,
+			var lineAfter = Assert.Single(after);
+
+			Assert.Equal (2, lineAfter.Start.Y);
+			Assert.Equal (2, lineAfter.Start.X);
+			Assert.Equal (9, lineAfter.Length);
+		}
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_VerticalLine_HorizontalExclusion_ClipBottom ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2,1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=10 x=0-10
+						.Exclude (new Point (0,10), 10, Orientation.Horizontal)
+						.ToArray ();
+			// y=1 to y=9,
+			var lineAfter = Assert.Single (after);
+
+			Assert.Equal (1, lineAfter.Start.Y);
+			Assert.Equal (2, lineAfter.Start.X);
+			Assert.Equal (9, lineAfter.Length);
+		}
+
+
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_VerticalLine_HorizontalExclusion_MissTop ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2,1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=0 x=0-10
+						.Exclude (new Point (0, 0), 10, Orientation.Horizontal)
+						.ToArray ();
+			// Exclusion line is too far above so hits nothing
+			Assert.Same(Assert.Single (after),l1);
+		}
+		[Fact, AutoInitShutdown]
+		public void TestExcludePerpendicular_VerticalLine_HorizontalExclusion_MissBottom ()
+		{
+			// y=1 to y=10
+			var l1 = new StraightLine (new Point (2,1), 10, Orientation.Vertical, LineStyle.Single);
+			var after = new StraightLine [] { l1 }
+						// exclude y=11 x=0-10
+						.Exclude (new Point (0,11), 10, Orientation.Horizontal)
+						.ToArray ();
+			// Exclusion line is too far to the right so hits nothing
+			Assert.Same (Assert.Single (after), l1);
+		}
+
+		#endregion Perpendicular Intersection Tests
+
+		[Fact, AutoInitShutdown]
+		public void LineCanvasIntegrationTest()
+		{
+			var lc = new LineCanvas();
+			lc.AddLine(new Point(0,0),10,Orientation.Horizontal,LineStyle.Single);
+			lc.AddLine(new Point(9,0),5,Orientation.Vertical,LineStyle.Single);
+			lc.AddLine(new Point(9,4),-10,Orientation.Horizontal,LineStyle.Single);
+			lc.AddLine(new Point(0,4),-5,Orientation.Vertical,LineStyle.Single);
+
+
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌────────┐
+│        │
+│        │
+│        │
+└────────┘",$"{Environment.NewLine}{lc}");
+			var origLines = lc.Lines;
+
+			lc = new LineCanvas(origLines.Exclude(new Point(0,0),10,Orientation.Horizontal));
+
+			TestHelpers.AssertEqual (this._output,
+				@"
+│        │
+│        │
+│        │
+└────────┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(0,1),10,Orientation.Horizontal));
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌────────┐
+          
+│        │
+│        │
+└────────┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(0,2),10,Orientation.Horizontal));
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌────────┐
+│        │
+          
+│        │
+└────────┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(0,3),10,Orientation.Horizontal));
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌────────┐
+│        │
+│        │
+          
+└────────┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(0,4),10,Orientation.Horizontal));
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌────────┐
+│        │
+│        │
+│        │",$"{Environment.NewLine}{lc}");
+
+
+			lc = new LineCanvas(origLines.Exclude(new Point(0,0),10,Orientation.Vertical));
+
+			TestHelpers.AssertEqual (this._output,
+				@"
+────────┐
+        │
+        │
+        │
+────────┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(1,0),10,Orientation.Vertical));
+
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌ ───────┐
+│        │
+│        │
+│        │
+└ ───────┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(8,0),10,Orientation.Vertical));
+
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌─────── ┐
+│        │
+│        │
+│        │
+└─────── ┘",$"{Environment.NewLine}{lc}");
+
+			lc = new LineCanvas(origLines.Exclude(new Point(9,0),10,Orientation.Vertical));
+
+			TestHelpers.AssertEqual (this._output,
+				@"
+┌────────
+│        
+│        
+│        
+└────────",$"{Environment.NewLine}{lc}");
+
+		}
+	}
+}