diff --git a/Examples/UICatalog/Scenarios/Transparent.cs b/Examples/UICatalog/Scenarios/Transparent.cs index 22dc866be..e1c94b325 100644 --- a/Examples/UICatalog/Scenarios/Transparent.cs +++ b/Examples/UICatalog/Scenarios/Transparent.cs @@ -219,11 +219,102 @@ public sealed class Transparent : Scenario return false; } + protected override bool OnRenderingLineCanvas () + { + // Draw "dotnet" using LineCanvas + Point screenPos = ViewportToScreen (new Point (7, 16)); + DrawDotnet (LineCanvas, screenPos.X, screenPos.Y, LineStyle.Single, GetAttributeForRole (VisualRole.Normal)); + + return false; + } + /// protected override bool OnClearingViewport () { return false; } /// protected override bool OnMouseEvent (MouseEventArgs mouseEvent) { return false; } + + + /// + /// Draws "dotnet" text using LineCanvas. The 'd' is 8 cells high. + /// + /// The LineCanvas to draw on + /// Starting X position + /// Starting Y position + /// Line style to use + /// Optional attribute for the lines + private void DrawDotnet (LineCanvas canvas, int x, int y, LineStyle style = LineStyle.Single, Attribute? attribute = null) + { + int currentX = x; + int letterHeight = 8; + int letterSpacing = 2; + + // Letter 'd' - lowercase, height 8 + // Vertical stem on right (goes up full 8 cells) + canvas.AddLine (new (currentX + 3, y), letterHeight, Orientation.Vertical, style, attribute); + // Top horizontal + canvas.AddLine (new (currentX, y + 3), 4, Orientation.Horizontal, style, attribute); + // Left vertical (only bottom 5 cells, leaving top 3 for ascender space) + canvas.AddLine (new (currentX, y + 3), 5, Orientation.Vertical, style, attribute); + // Bottom horizontal + canvas.AddLine (new (currentX, y + 7), 4, Orientation.Horizontal, style, attribute); + currentX += 4 + letterSpacing; + + // Letter 'o' - height 5 (x-height) + int oY = y + 3; // Align with x-height (leaving 3 cells for ascenders) + // Top + canvas.AddLine (new (currentX, oY), 4, Orientation.Horizontal, style, attribute); + // Left + canvas.AddLine (new (currentX, oY), 5, Orientation.Vertical, style, attribute); + // Right + canvas.AddLine (new (currentX + 3, oY), 5, Orientation.Vertical, style, attribute); + // Bottom + canvas.AddLine (new (currentX, oY + 4), 4, Orientation.Horizontal, style, attribute); + currentX += 4 + letterSpacing; + + // Letter 't' - height 7 (has ascender above x-height) + int tY = y + 1; // Starts 1 cell above x-height + // Vertical stem + canvas.AddLine (new (currentX + 1, tY), 7, Orientation.Vertical, style, attribute); + // Top cross bar (at x-height) + canvas.AddLine (new (currentX, tY + 2), 3, Orientation.Horizontal, style, attribute); + // Bottom horizontal (foot) + canvas.AddLine (new (currentX + 1, tY + 6), 2, Orientation.Horizontal, style, attribute); + currentX += 3 + letterSpacing; + + // Letter 'n' - height 5 (x-height) + int nY = y + 3; + // Left vertical + canvas.AddLine (new (currentX, nY), 5, Orientation.Vertical, style, attribute); + // Top horizontal + canvas.AddLine (new (currentX + 1, nY), 3, Orientation.Horizontal, style, attribute); + // Right vertical + canvas.AddLine (new (currentX + 3, nY), 5, Orientation.Vertical, style, attribute); + currentX += 4 + letterSpacing; + + // Letter 'e' - height 5 (x-height) + int eY = y + 3; + // Top + canvas.AddLine (new (currentX, eY), 4, Orientation.Horizontal, style, attribute); + // Left + canvas.AddLine (new (currentX, eY), 5, Orientation.Vertical, style, attribute); + // Right + canvas.AddLine (new (currentX + 3, eY), 3, Orientation.Vertical, style, attribute); + // Middle horizontal bar + canvas.AddLine (new (currentX, eY + 2), 4, Orientation.Horizontal, style, attribute); + // Bottom + canvas.AddLine (new (currentX, eY + 4), 4, Orientation.Horizontal, style, attribute); + currentX += 4 + letterSpacing; + + // Letter 't' - height 7 (has ascender above x-height) - second 't' + int t2Y = y + 1; + // Vertical stem + canvas.AddLine (new (currentX + 1, t2Y), 7, Orientation.Vertical, style, attribute); + // Top cross bar (at x-height) + canvas.AddLine (new (currentX, t2Y + 2), 3, Orientation.Horizontal, style, attribute); + // Bottom horizontal (foot) + canvas.AddLine (new (currentX + 1, t2Y + 6), 2, Orientation.Horizontal, style, attribute); + } } } diff --git a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs index 2be985764..ed3e529ec 100644 --- a/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs +++ b/Terminal.Gui/Drawing/LineCanvas/LineCanvas.cs @@ -179,7 +179,7 @@ public class LineCanvas : IDisposable } } // Safe as long as the list is not modified while the span is in use. - ReadOnlySpan intersects = CollectionsMarshal.AsSpan(intersectionsBufferList); + ReadOnlySpan intersects = CollectionsMarshal.AsSpan (intersectionsBufferList); Cell? cell = GetCellForIntersects (intersects); // TODO: Can we skip the whole nested looping if _exclusionRegion is null? if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false) @@ -192,6 +192,136 @@ public class LineCanvas : IDisposable return map; } + /// + /// Evaluates the lines and returns both the cell map and a Region encompassing the drawn cells. + /// This is more efficient than calling and separately + /// as it builds both in a single pass through the canvas bounds. + /// + /// A tuple containing the cell map and the Region of drawn cells + public (Dictionary CellMap, Region Region) GetCellMapWithRegion () + { + Dictionary map = new (); + Region region = new (); + + List intersectionsBufferList = []; + List rowXValues = []; + + // walk through each pixel of the bitmap, row by row + for (int y = Bounds.Y; y < Bounds.Y + Bounds.Height; y++) + { + rowXValues.Clear (); + + for (int x = Bounds.X; x < Bounds.X + Bounds.Width; x++) + { + intersectionsBufferList.Clear (); + foreach (StraightLine line in _lines) + { + if (line.Intersects (x, y) is { } intersect) + { + intersectionsBufferList.Add (intersect); + } + } + // Safe as long as the list is not modified while the span is in use. + ReadOnlySpan intersects = CollectionsMarshal.AsSpan (intersectionsBufferList); + Cell? cell = GetCellForIntersects (intersects); + + if (cell is { } && _exclusionRegion?.Contains (x, y) is null or false) + { + map.Add (new (x, y), cell); + rowXValues.Add (x); + } + } + + // Build Region spans for this completed row + if (rowXValues.Count <= 0) + { + continue; + } + + // X values are already sorted (inner loop iterates x in order) + int spanStart = rowXValues [0]; + int spanEnd = rowXValues [0]; + + for (int i = 1; i < rowXValues.Count; i++) + { + if (rowXValues [i] == spanEnd + 1) + { + // Continue the span + spanEnd = rowXValues [i]; + } + else + { + // End the current span and add it to the region + region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union); + spanStart = rowXValues [i]; + spanEnd = rowXValues [i]; + } + } + + // Add the final span for this row + region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union); + } + + return (map, region); + } + + /// + /// Efficiently builds a from line cells by grouping contiguous horizontal spans. + /// This avoids the performance overhead of adding each cell individually while accurately + /// representing the non-rectangular shape of the lines. + /// + /// Dictionary of points where line cells are drawn. If empty, returns an empty Region. + /// A Region encompassing all the line cells, or an empty Region if cellMap is empty + public static Region GetRegion (Dictionary cellMap) + { + // Group cells by row for efficient horizontal span detection + // Sort by Y then X so that within each row group, X values are in order + IEnumerable> rowGroups = cellMap.Keys + .OrderBy (p => p.Y) + .ThenBy (p => p.X) + .GroupBy (p => p.Y); + + Region region = new (); + + foreach (IGrouping row in rowGroups) + { + int y = row.Key; + // X values are sorted due to ThenBy above + List xValues = row.Select (p => p.X).ToList (); + + // Note: GroupBy on non-empty Keys guarantees non-empty groups, but check anyway for safety + if (xValues.Count == 0) + { + continue; + } + + // Merge contiguous x values into horizontal spans + int spanStart = xValues [0]; + int spanEnd = xValues [0]; + + for (int i = 1; i < xValues.Count; i++) + { + if (xValues [i] == spanEnd + 1) + { + // Continue the span + spanEnd = xValues [i]; + } + else + { + // End the current span and add it to the region + region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union); + spanStart = xValues [i]; + spanEnd = xValues [i]; + } + } + + // Add the final span for this row + region.Combine (new Rectangle (spanStart, y, spanEnd - spanStart + 1, 1), RegionOp.Union); + } + + return region; + } + // TODO: Unless there's an obvious use case for this API we should delete it in favor of the // simpler version that doesn't take an area. /// @@ -227,7 +357,7 @@ public class LineCanvas : IDisposable } } // Safe as long as the list is not modified while the span is in use. - ReadOnlySpan intersects = CollectionsMarshal.AsSpan(intersectionsBufferList); + ReadOnlySpan intersects = CollectionsMarshal.AsSpan (intersectionsBufferList); Rune? rune = GetRuneForIntersects (intersects); @@ -442,14 +572,14 @@ public class LineCanvas : IDisposable } // TODO: Remove these once we have all of the below ported to IntersectionRuneResolvers - bool useDouble = AnyLineStyles(intersects, [LineStyle.Double]); - bool useDashed = AnyLineStyles(intersects, [LineStyle.Dashed, LineStyle.RoundedDashed]); - bool useDotted = AnyLineStyles(intersects, [LineStyle.Dotted, LineStyle.RoundedDotted]); + bool useDouble = AnyLineStyles (intersects, [LineStyle.Double]); + bool useDashed = AnyLineStyles (intersects, [LineStyle.Dashed, LineStyle.RoundedDashed]); + bool useDotted = AnyLineStyles (intersects, [LineStyle.Dotted, LineStyle.RoundedDotted]); // horiz and vert lines same as Single for Rounded - bool useThick = AnyLineStyles(intersects, [LineStyle.Heavy]); - bool useThickDashed = AnyLineStyles(intersects, [LineStyle.HeavyDashed]); - bool useThickDotted = AnyLineStyles(intersects, [LineStyle.HeavyDotted]); + bool useThick = AnyLineStyles (intersects, [LineStyle.Heavy]); + bool useThickDashed = AnyLineStyles (intersects, [LineStyle.HeavyDashed]); + bool useThickDotted = AnyLineStyles (intersects, [LineStyle.HeavyDotted]); // TODO: Support ruler //var useRuler = intersects.Any (i => i.Line.Style == LineStyle.Ruler && i.Line.Length != 0); @@ -727,10 +857,10 @@ public class LineCanvas : IDisposable private static class CornerIntersections { // Names matching #region "Corner Conditions" IntersectionRuneType - internal static readonly IntersectionType[] UpperLeft = [IntersectionType.StartRight, IntersectionType.StartDown]; - internal static readonly IntersectionType[] UpperRight = [IntersectionType.StartLeft, IntersectionType.StartDown]; - internal static readonly IntersectionType[] LowerRight = [IntersectionType.StartUp, IntersectionType.StartLeft]; - internal static readonly IntersectionType[] LowerLeft = [IntersectionType.StartUp, IntersectionType.StartRight]; + internal static readonly IntersectionType [] UpperLeft = [IntersectionType.StartRight, IntersectionType.StartDown]; + internal static readonly IntersectionType [] UpperRight = [IntersectionType.StartLeft, IntersectionType.StartDown]; + internal static readonly IntersectionType [] LowerRight = [IntersectionType.StartUp, IntersectionType.StartLeft]; + internal static readonly IntersectionType [] LowerLeft = [IntersectionType.StartUp, IntersectionType.StartRight]; } private class BottomTeeIntersectionRuneResolver : IntersectionRuneResolver @@ -773,14 +903,18 @@ public class LineCanvas : IDisposable internal Rune _thickBoth; internal Rune _thickH; internal Rune _thickV; - protected IntersectionRuneResolver () { SetGlyphs (); } + + protected IntersectionRuneResolver () + { + SetGlyphs (); + } public Rune? GetRuneForIntersects (ReadOnlySpan intersects) { // Note that there aren't any glyphs for intersections of double lines with heavy lines - bool doubleHorizontal = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Horizontal, [LineStyle.Double]); - bool doubleVertical = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Vertical, [LineStyle.Double]); + bool doubleHorizontal = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Horizontal, [LineStyle.Double]); + bool doubleVertical = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Vertical, [LineStyle.Double]); if (doubleHorizontal) { @@ -792,9 +926,9 @@ public class LineCanvas : IDisposable return _doubleV; } - bool thickHorizontal = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Horizontal, + bool thickHorizontal = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Horizontal, [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]); - bool thickVertical = AnyWithOrientationAndAnyLineStyle(intersects, Orientation.Vertical, + bool thickVertical = AnyWithOrientationAndAnyLineStyle (intersects, Orientation.Vertical, [LineStyle.Heavy, LineStyle.HeavyDashed, LineStyle.HeavyDotted]); if (thickHorizontal) diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs index 871128e0a..a5e17da18 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.cs @@ -140,25 +140,25 @@ public partial class View // Drawing APIs ClearNeedsDraw (); - if (this is not Adornment && SuperView is not Adornment) - { - // Parent - Debug.Assert (Margin!.Parent == this); - Debug.Assert (Border!.Parent == this); - Debug.Assert (Padding!.Parent == this); + //if (this is not Adornment && SuperView is not Adornment) + //{ + // // Parent + // Debug.Assert (Margin!.Parent == this); + // Debug.Assert (Border!.Parent == this); + // Debug.Assert (Padding!.Parent == this); - // SubViewNeedsDraw is set to false by ClearNeedsDraw. - Debug.Assert (SubViewNeedsDraw == false); - Debug.Assert (Margin!.SubViewNeedsDraw == false); - Debug.Assert (Border!.SubViewNeedsDraw == false); - Debug.Assert (Padding!.SubViewNeedsDraw == false); + // // SubViewNeedsDraw is set to false by ClearNeedsDraw. + // Debug.Assert (SubViewNeedsDraw == false); + // Debug.Assert (Margin!.SubViewNeedsDraw == false); + // Debug.Assert (Border!.SubViewNeedsDraw == false); + // Debug.Assert (Padding!.SubViewNeedsDraw == false); - // NeedsDraw is set to false by ClearNeedsDraw. - Debug.Assert (NeedsDraw == false); - Debug.Assert (Margin!.NeedsDraw == false); - Debug.Assert (Border!.NeedsDraw == false); - Debug.Assert (Padding!.NeedsDraw == false); - } + // // NeedsDraw is set to false by ClearNeedsDraw. + // Debug.Assert (NeedsDraw == false); + // Debug.Assert (Margin!.NeedsDraw == false); + // Debug.Assert (Border!.NeedsDraw == false); + // Debug.Assert (Padding!.NeedsDraw == false); + //} } // ------------------------------------ @@ -226,7 +226,7 @@ public partial class View // Drawing APIs { // Set the clip to be just the thicknesses of the adornments // TODO: Put this union logic in a method on View? - Region? clipAdornments = Margin!.Thickness.AsRegion (Margin!.FrameToScreen ()); + Region clipAdornments = Margin!.Thickness.AsRegion (Margin!.FrameToScreen ()); clipAdornments.Combine (Border!.Thickness.AsRegion (Border!.FrameToScreen ()), RegionOp.Union); clipAdornments.Combine (Padding!.Thickness.AsRegion (Padding!.FrameToScreen ()), RegionOp.Union); clipAdornments.Combine (originalClip, RegionOp.Intersect); @@ -697,8 +697,8 @@ public partial class View // Drawing APIs /// to stop further drawing of . protected virtual bool OnRenderingLineCanvas () { return false; } - /// The canvas that any line drawing that is to be shared by SubViews of this view should add lines to. - /// adds border lines to this LineCanvas. + /// The canvas that any line drawing that is to be shared by subviews of this view should add lines to. + /// adds lines to this LineCanvas. public LineCanvas LineCanvas { get; } = new (); /// @@ -725,7 +725,10 @@ public partial class View // Drawing APIs if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rectangle.Empty) { - foreach (KeyValuePair p in LineCanvas.GetCellMap ()) + // Get both cell map and Region in a single pass through the canvas + (Dictionary cellMap, Region lineRegion) = LineCanvas.GetCellMapWithRegion (); + + foreach (KeyValuePair p in cellMap) { // Get the entire map if (p.Value is { }) @@ -735,12 +738,16 @@ public partial class View // Drawing APIs // TODO: #2616 - Support combining sequences that don't normalize AddStr (p.Value.Value.Grapheme); - - // Add each drawn cell to the context - //context?.AddDrawnRectangle (new Rectangle (p.Key, new (1, 1)) ); } } + // Report the drawn region for transparency support + // Region was built during the GetCellMapWithRegion() call above + if (context is { } && cellMap.Count > 0) + { + context.AddDrawnRegion (lineRegion); + } + LineCanvas.Clear (); } } diff --git a/Tests/UnitTestsParallelizable/Drawing/Lines/LineCanvasTests.cs b/Tests/UnitTestsParallelizable/Drawing/Lines/LineCanvasTests.cs index 1feedd9f2..252a3c080 100644 --- a/Tests/UnitTestsParallelizable/Drawing/Lines/LineCanvasTests.cs +++ b/Tests/UnitTestsParallelizable/Drawing/Lines/LineCanvasTests.cs @@ -17,14 +17,14 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Empty_Canvas_ToString_Returns_EmptyString () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); Assert.Equal (string.Empty, canvas.ToString ()); } [Fact] public void Clear_Removes_All_Lines () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single); canvas.AddLine (new (0, 0), 3, Orientation.Vertical, LineStyle.Single); @@ -38,7 +38,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Lines_Property_Returns_ReadOnly_Collection () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single); Assert.Single (canvas.Lines); @@ -48,7 +48,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void AddLine_Adds_Line_To_Collection () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); Assert.Empty (canvas.Lines); canvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single); @@ -94,7 +94,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase int expectedHeight ) { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (x, y), length, Orientation.Horizontal, LineStyle.Single); canvas.AddLine (new (x, y), length, Orientation.Vertical, LineStyle.Single); @@ -119,7 +119,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase int expectedHeight ) { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (x, y), length, Orientation.Horizontal, LineStyle.Single); Assert.Equal (new (expectedX, expectedY, expectedWidth, expectedHeight), canvas.Bounds); @@ -128,7 +128,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Specific_Coordinates () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (5, 5), 3, Orientation.Horizontal, LineStyle.Single); Assert.Equal (new (5, 5, 3, 1), canvas.Bounds); } @@ -136,14 +136,14 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Empty_Canvas_Returns_Empty_Rectangle () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); Assert.Equal (Rectangle.Empty, canvas.Bounds); } [Fact] public void Bounds_Single_Point_Zero_Length () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (5, 5), 0, Orientation.Horizontal, LineStyle.Single); Assert.Equal (new (5, 5, 1, 1), canvas.Bounds); @@ -152,7 +152,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Horizontal_Line () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (2, 3), 5, Orientation.Horizontal, LineStyle.Single); Assert.Equal (new (2, 3, 5, 1), canvas.Bounds); @@ -161,7 +161,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Vertical_Line () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (2, 3), 5, Orientation.Vertical, LineStyle.Single); Assert.Equal (new (2, 3, 1, 5), canvas.Bounds); @@ -170,7 +170,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Multiple_Lines_Returns_Union () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single); canvas.AddLine (new (0, 0), 3, Orientation.Vertical, LineStyle.Single); @@ -180,7 +180,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Negative_Length_Line () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (5, 5), -3, Orientation.Horizontal, LineStyle.Single); // Line from (5,5) going left 3 positions: includes points 3, 4, 5 (width 3, X starts at 3) @@ -190,7 +190,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Bounds_Complex_Box () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); // top canvas.AddLine (new (0, 0), 3, Orientation.Horizontal, LineStyle.Single); @@ -214,7 +214,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void ClearExclusions_Clears_Exclusion_Region () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single); var region = new Region (new (0, 0, 2, 1)); @@ -229,7 +229,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Exclude_Removes_Points_From_Map () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (0, 0), 5, Orientation.Horizontal, LineStyle.Single); var region = new Region (new (0, 0, 2, 1)); @@ -260,7 +260,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Fill_Property_Defaults_To_Null () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); Assert.Null (canvas.Fill); } @@ -688,7 +688,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Theory] public void Length_0_Is_1_Long (int x, int y, Orientation orientation, string expected) { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); // Add a line at 5, 5 that's has length of 1 canvas.AddLine (new (x, y), 1, orientation, LineStyle.Single); @@ -741,9 +741,10 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [InlineData (-1, 0, -2, Orientation.Vertical, "│\r\n│")] [InlineData (0, -1, -2, Orientation.Vertical, "│\r\n│")] [InlineData (-1, -1, -2, Orientation.Vertical, "│\r\n│")] - [Theory] public void Length_n_Is_n_Long (int x, int y, int length, Orientation orientation, string expected) + [Theory] + public void Length_n_Is_n_Long (int x, int y, int length, Orientation orientation, string expected) { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (new (x, y), length, orientation, LineStyle.Single); var result = canvas.ToString (); @@ -755,7 +756,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase { var offset = new Point (5, 5); - var canvas = new LineCanvas (); + LineCanvas canvas = new (); canvas.AddLine (offset, -3, Orientation.Horizontal, LineStyle.Single); var looksLike = "───"; @@ -820,7 +821,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void TestLineCanvas_LeaveMargin_Top1_Left1 () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); // Upper box canvas.AddLine (Point.Empty, 2, Orientation.Horizontal, LineStyle.Single); @@ -927,7 +928,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Top_Left_From_TopRight_LeftUp () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); // Upper box canvas.AddLine (Point.Empty, 2, Orientation.Horizontal, LineStyle.Single); @@ -943,7 +944,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Top_With_1Down () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); // Top ─ canvas.AddLine (Point.Empty, 1, Orientation.Horizontal, LineStyle.Single); @@ -1328,7 +1329,7 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase [Fact] public void Window () { - var canvas = new LineCanvas (); + LineCanvas canvas = new (); // Frame canvas.AddLine (Point.Empty, 10, Orientation.Horizontal, LineStyle.Single); @@ -1507,4 +1508,360 @@ public class LineCanvasTests (ITestOutputHelper output) : FakeDriverBase return v; } + + #region GetRegion Tests + + [Fact] + public void GetRegion_EmptyCellMap_ReturnsEmptyRegion () + { + Dictionary cellMap = new (); + Region region = LineCanvas.GetRegion (cellMap); + + Assert.NotNull (region); + Assert.True (region.IsEmpty ()); + } + + [Fact] + public void GetRegion_SingleCell_ReturnsSingleRectangle () + { + Dictionary cellMap = new () + { + { new Point (5, 10), new Cell { Grapheme = "X" } } + }; + + Region region = LineCanvas.GetRegion (cellMap); + + Assert.NotNull (region); + Assert.False (region.IsEmpty ()); + Assert.True (region.Contains (5, 10)); + } + + [Fact] + public void GetRegion_HorizontalLine_CreatesHorizontalSpan () + { + Dictionary cellMap = new (); + // Horizontal line from (5, 10) to (9, 10) + for (int x = 5; x <= 9; x++) + { + cellMap.Add (new Point (x, 10), new Cell { Grapheme = "─" }); + } + + Region region = LineCanvas.GetRegion (cellMap); + + Assert.NotNull (region); + // All cells in the horizontal span should be in the region + for (int x = 5; x <= 9; x++) + { + Assert.True (region.Contains (x, 10), $"Expected ({x}, 10) to be in region"); + } + // Cells outside the span should not be in the region + Assert.False (region.Contains (4, 10)); + Assert.False (region.Contains (10, 10)); + Assert.False (region.Contains (7, 9)); + Assert.False (region.Contains (7, 11)); + } + + [Fact] + public void GetRegion_VerticalLine_CreatesMultipleHorizontalSpans () + { + Dictionary cellMap = new (); + // Vertical line from (5, 10) to (5, 14) + for (int y = 10; y <= 14; y++) + { + cellMap.Add (new Point (5, y), new Cell { Grapheme = "│" }); + } + + Region region = LineCanvas.GetRegion (cellMap); + + Assert.NotNull (region); + // All cells in the vertical line should be in the region + for (int y = 10; y <= 14; y++) + { + Assert.True (region.Contains (5, y), $"Expected (5, {y}) to be in region"); + } + // Cells outside should not be in the region + Assert.False (region.Contains (4, 12)); + Assert.False (region.Contains (6, 12)); + } + + [Fact] + public void GetRegion_LShape_CreatesCorrectSpans () + { + Dictionary cellMap = new (); + // L-shape: horizontal line from (0, 0) to (5, 0), then vertical to (5, 3) + for (int x = 0; x <= 5; x++) + { + cellMap.Add (new Point (x, 0), new Cell { Grapheme = "─" }); + } + for (int y = 1; y <= 3; y++) + { + cellMap.Add (new Point (5, y), new Cell { Grapheme = "│" }); + } + + Region region = LineCanvas.GetRegion (cellMap); + + // Horizontal part + for (int x = 0; x <= 5; x++) + { + Assert.True (region.Contains (x, 0), $"Expected ({x}, 0) to be in region"); + } + // Vertical part + for (int y = 1; y <= 3; y++) + { + Assert.True (region.Contains (5, y), $"Expected (5, {y}) to be in region"); + } + // Empty cells should not be in region + Assert.False (region.Contains (1, 1)); + Assert.False (region.Contains (4, 2)); + } + + [Fact] + public void GetRegion_DiscontiguousHorizontalCells_CreatesSeparateSpans () + { + Dictionary cellMap = new () + { + { new Point (0, 5), new Cell { Grapheme = "X" } }, + { new Point (1, 5), new Cell { Grapheme = "X" } }, + // Gap at (2, 5) + { new Point (3, 5), new Cell { Grapheme = "X" } }, + { new Point (4, 5), new Cell { Grapheme = "X" } } + }; + + Region region = LineCanvas.GetRegion (cellMap); + + Assert.True (region.Contains (0, 5)); + Assert.True (region.Contains (1, 5)); + Assert.False (region.Contains (2, 5)); // Gap + Assert.True (region.Contains (3, 5)); + Assert.True (region.Contains (4, 5)); + } + + [Fact] + public void GetRegion_IntersectingLines_CreatesCorrectRegion () + { + Dictionary cellMap = new (); + // Horizontal line + for (int x = 0; x <= 4; x++) + { + cellMap.Add (new Point (x, 2), new Cell { Grapheme = "─" }); + } + // Vertical line intersecting at (2, 2) + for (int y = 0; y <= 4; y++) + { + cellMap [new Point (2, y)] = new Cell { Grapheme = "┼" }; + } + + Region region = LineCanvas.GetRegion (cellMap); + + // Horizontal line + for (int x = 0; x <= 4; x++) + { + Assert.True (region.Contains (x, 2), $"Expected ({x}, 2) to be in region"); + } + // Vertical line + for (int y = 0; y <= 4; y++) + { + Assert.True (region.Contains (2, y), $"Expected (2, {y}) to be in region"); + } + } + + #endregion + + #region GetCellMapWithRegion Tests + + [Fact] + public void GetCellMapWithRegion_EmptyCanvas_ReturnsEmptyMapAndRegion () + { + LineCanvas canvas = new (); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.Empty (cellMap); + Assert.NotNull (region); + Assert.True (region.IsEmpty ()); + } + + [Fact] + public void GetCellMapWithRegion_SingleHorizontalLine_ReturnsCellMapAndRegion () + { + LineCanvas canvas = new (); + canvas.AddLine (new Point (5, 10), 5, Orientation.Horizontal, LineStyle.Single); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.NotEmpty (cellMap); + Assert.NotNull (region); + Assert.False (region.IsEmpty ()); + + // Both cellMap and region should contain the same cells + foreach (Point p in cellMap.Keys) + { + Assert.True (region.Contains (p.X, p.Y), $"Expected ({p.X}, {p.Y}) to be in region"); + } + } + + [Fact] + public void GetCellMapWithRegion_SingleVerticalLine_ReturnsCellMapAndRegion () + { + LineCanvas canvas = new (); + canvas.AddLine (new Point (5, 10), 5, Orientation.Vertical, LineStyle.Single); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.NotEmpty (cellMap); + Assert.NotNull (region); + Assert.False (region.IsEmpty ()); + + // Both cellMap and region should contain the same cells + foreach (Point p in cellMap.Keys) + { + Assert.True (region.Contains (p.X, p.Y), $"Expected ({p.X}, {p.Y}) to be in region"); + } + } + + [Fact] + public void GetCellMapWithRegion_IntersectingLines_CorrectlyHandlesIntersection () + { + LineCanvas canvas = new (); + // Create a cross pattern + canvas.AddLine (new Point (0, 2), 5, Orientation.Horizontal, LineStyle.Single); + canvas.AddLine (new Point (2, 0), 5, Orientation.Vertical, LineStyle.Single); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.NotEmpty (cellMap); + Assert.NotNull (region); + + // Verify intersection point is in both + Assert.True (cellMap.ContainsKey (new Point (2, 2)), "Intersection should be in cellMap"); + Assert.True (region.Contains (2, 2), "Intersection should be in region"); + + // All cells should be in both structures + foreach (Point p in cellMap.Keys) + { + Assert.True (region.Contains (p.X, p.Y), $"Expected ({p.X}, {p.Y}) to be in region"); + } + } + + [Fact] + public void GetCellMapWithRegion_ComplexShape_RegionMatchesCellMap () + { + LineCanvas canvas = new (); + // Create a box + canvas.AddLine (new Point (0, 0), 5, Orientation.Horizontal, LineStyle.Single); + canvas.AddLine (new Point (0, 3), 5, Orientation.Horizontal, LineStyle.Single); + canvas.AddLine (new Point (0, 0), 4, Orientation.Vertical, LineStyle.Single); + canvas.AddLine (new Point (4, 0), 4, Orientation.Vertical, LineStyle.Single); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.NotEmpty (cellMap); + Assert.NotNull (region); + + // Every cell in the map should be in the region + foreach (Point p in cellMap.Keys) + { + Assert.True (region.Contains (p.X, p.Y), $"Expected ({p.X}, {p.Y}) to be in region"); + } + + // Cells not in the map should not be in the region (interior of box) + Assert.False (cellMap.ContainsKey (new Point (2, 1))); + // Note: Region might contain interior if it's filled, so we just verify consistency + } + + [Fact] + public void GetCellMapWithRegion_ResultsMatchSeparateCalls () + { + LineCanvas canvas = new (); + // Create a complex pattern + canvas.AddLine (new Point (0, 0), 10, Orientation.Horizontal, LineStyle.Single); + canvas.AddLine (new Point (5, 0), 10, Orientation.Vertical, LineStyle.Single); + canvas.AddLine (new Point (0, 5), 10, Orientation.Horizontal, LineStyle.Double); + + // Get results from combined method + (Dictionary combinedCellMap, Region combinedRegion) = canvas.GetCellMapWithRegion (); + + // Get results from separate calls + Dictionary separateCellMap = canvas.GetCellMap (); + Region separateRegion = LineCanvas.GetRegion (separateCellMap); + + // Cell maps should be identical + Assert.Equal (separateCellMap.Count, combinedCellMap.Count); + foreach (KeyValuePair kvp in separateCellMap) + { + Assert.True (combinedCellMap.ContainsKey (kvp.Key), $"Combined map missing key {kvp.Key}"); + } + + // Regions should contain the same points + foreach (Point p in combinedCellMap.Keys) + { + Assert.True (combinedRegion.Contains (p.X, p.Y), $"Combined region missing ({p.X}, {p.Y})"); + Assert.True (separateRegion.Contains (p.X, p.Y), $"Separate region missing ({p.X}, {p.Y})"); + } + } + + [Fact] + public void GetCellMapWithRegion_NegativeCoordinates_HandlesCorrectly () + { + LineCanvas canvas = new (); + canvas.AddLine (new Point (-5, -5), 10, Orientation.Horizontal, LineStyle.Single); + canvas.AddLine (new Point (0, -5), 10, Orientation.Vertical, LineStyle.Single); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.NotEmpty (cellMap); + Assert.NotNull (region); + + // Verify negative coordinates are handled + Assert.True (cellMap.Keys.Any (p => p.X < 0 || p.Y < 0), "Should have negative coordinates"); + + // All cells should be in region + foreach (Point p in cellMap.Keys) + { + Assert.True (region.Contains (p.X, p.Y), $"Expected ({p.X}, {p.Y}) to be in region"); + } + } + + [Fact] + public void GetCellMapWithRegion_WithExclusion_RegionExcludesExcludedCells () + { + LineCanvas canvas = new (); + canvas.AddLine (new Point (0, 0), 10, Orientation.Horizontal, LineStyle.Single); + + // Exclude middle section + Region exclusionRegion = new (); + exclusionRegion.Combine (new Rectangle (3, 0, 4, 1), RegionOp.Union); + canvas.Exclude (exclusionRegion); + + (Dictionary cellMap, Region region) = canvas.GetCellMapWithRegion (); + + Assert.NotNull (cellMap); + Assert.NotEmpty (cellMap); + + // Excluded cells should not be in cellMap + for (int x = 3; x < 7; x++) + { + Assert.False (cellMap.ContainsKey (new Point (x, 0)), $"({x}, 0) should be excluded from cellMap"); + } + + // Region should match cellMap + foreach (Point p in cellMap.Keys) + { + Assert.True (region.Contains (p.X, p.Y), $"Expected ({p.X}, {p.Y}) to be in region"); + } + + // Excluded points should not be in region + for (int x = 3; x < 7; x++) + { + Assert.False (region.Contains (x, 0), $"({x}, 0) should be excluded from region"); + } + } + + #endregion } diff --git a/Tests/UnitTestsParallelizable/xunit.runner.json b/Tests/UnitTestsParallelizable/xunit.runner.json index bc6c60196..ba11279f6 100644 --- a/Tests/UnitTestsParallelizable/xunit.runner.json +++ b/Tests/UnitTestsParallelizable/xunit.runner.json @@ -3,5 +3,5 @@ "parallelizeTestCollections": true, "parallelizeAssembly": true, "stopOnFail": false, - "maxParallelThreads": "4x" + "maxParallelThreads": "default" } \ No newline at end of file