using System; using System.Collections.Generic; using System.Linq; using System.Threading; using OpenNest.CNC; using OpenNest.Engine.Fill; using OpenNest.Geometry; using Xunit; namespace OpenNest.Engine.Tests.Fill; /// /// PlateView spacing expander: grows part-to-part spacing with the work area /// and non-selected parts as hard boundaries. /// public class ExpanderTests { private static Program Rectangle(double width = 4, double length = 4) { var program = new Program(); program.MoveTo(0, 0); program.LineTo(width, 0); program.LineTo(width, length); program.LineTo(0, length); program.LineTo(0, 0); return program; } private static Part AddSquare(Plate plate, double x, double y, double size = 4) { var part = new Part(new Drawing($"sq{plate.Parts.Count}", Rectangle(size, size)), new Vector(x, y)); plate.Parts.Add(part); return part; } private static Part AddRectangle(Plate plate, double x, double y, double length, double width) { var part = new Part(new Drawing($"rect{plate.Parts.Count}", Rectangle(length, width)), new Vector(x, y)); plate.Parts.Add(part); return part; } private static Plate MakePlate(double lengthX, double widthY, double edge = 0.5) { var plate = new Plate(new Size(widthY, lengthX)); plate.EdgeSpacing = new Spacing(edge, edge); return plate; } /// Independent clearance oracle: naive vertex/segment min distance over raw part lines. private static double BruteClearance(Part a, Part b) { var linesA = PartGeometry.GetPartLines(a); var linesB = PartGeometry.GetPartLines(b); double min = double.MaxValue; foreach (var la in linesA) foreach (var lb in linesB) { min = System.Math.Min(min, PointSegment(a, la.StartPoint, lb)); min = System.Math.Min(min, PointSegment(a, la.EndPoint, lb)); min = System.Math.Min(min, PointSegment(b, lb.StartPoint, la)); min = System.Math.Min(min, PointSegment(b, lb.EndPoint, la)); } return min; } private static double PointSegment(Part owner, Vector pt, Line seg) { var d = seg.EndPoint - seg.StartPoint; var len2 = d.DotProduct(d); var t = len2 <= 1e-12 ? 0 : System.Math.Clamp((pt - seg.StartPoint).DotProduct(d) / len2, 0, 1); return pt.DistanceTo(seg.StartPoint + d * t); } private static void AssertNoOverlaps(Plate plate) { for (var i = 0; i < plate.Parts.Count; i++) for (var j = i + 1; j < plate.Parts.Count; j++) Assert.False( plate.Parts[i].Intersects(plate.Parts[j], out _), $"{plate.Parts[i].BaseDrawing.Name} overlaps {plate.Parts[j].BaseDrawing.Name}" ); } [Fact] public void Expand_TwoSquares_GrowUntilEdgeFloor_AndAnchorStaysPut() { var plate = MakePlate(24, 24); var a = AddSquare(plate, 6, 10); var b = AddSquare(plate, 14, 10); var result = Expander.Expand(new List { a, b }, plate); // Max gap: B flush against the right edge floor (23.5): 23.5 - 14 - 4 + gap base... // A stays (anchor); B slides to x=19.5 -> gap 9.5. Assert.Equal(6, a.Location.X, 6); Assert.Equal(10, a.Location.Y, 6); Assert.Equal(9.5, b.Location.X - (a.Location.X + 4), 1); Assert.True(result.AchievedSpacing >= 9.4, $"achieved {result.AchievedSpacing}"); Assert.True(result.AchievedSpacing <= 9.6, $"achieved {result.AchievedSpacing}"); AssertNoOverlaps(plate); Assert.True(b.BoundingBox.Right <= 23.5 + 1e-6); } [Fact] public void Expand_SandwichedBetweenWalls_ConvergesOnlyToInitialGaps_AndKeepsWalls() { var plate = MakePlate(26, 10, edge: 0.0); var wallL = AddSquare(plate, 0, 3); var wallR = AddSquare(plate, 18, 3); var a = AddSquare(plate, 6, 3); var b = AddSquare(plate, 12, 3); var result = Expander.Expand(new List { a, b }, plate); // Every gap starts at exactly 2.0; straight separation moves cannot open // the row (opening one gap costs another), so the run stays at ~2.0. Assert.Equal(0, wallL.Location.X, 6); Assert.Equal(18, wallR.Location.X, 6); Assert.True( result.AchievedSpacing >= 1.9 && result.AchievedSpacing <= 2.05, $"achieved {result.AchievedSpacing}" ); AssertNoOverlaps(plate); } [Fact] public void Expand_OverlappingPair_SeparatesAndClearsOverlap() { var plate = MakePlate(30, 12); var a = AddSquare(plate, 5, 4); var b = AddSquare(plate, 7, 4); // 2.0 overlap in X var result = Expander.Expand(new List { a, b }, plate, new Expander.Options { InitialStep = 0.5, MaxSpacing = 3, }); Assert.False(a.Intersects(b, out _)); var gap = b.Location.X - (a.Location.X + 4); Assert.True(gap >= 2.99, $"gap {gap}"); Assert.True(result.AchievedSpacing >= 2.9); AssertNoOverlaps(plate); } [Fact] public void Separate_PinnedPart_ReportsViolationsWithoutOverlap() { var plate = MakePlate(20, 20, edge: 0.0); var pinned = AddSquare(plate, 8, 8); // Walls 0.2 clear on all four sides. var left = AddSquare(plate, 3.8, 8); var right = AddSquare(plate, 12.2, 8); var bottom = AddSquare(plate, 8, 3.8); var top = AddSquare(plate, 8, 12.2); var (converged, positions, violations) = Expander.Separate( new List { pinned }, plate, spacing: 1.0 ); Assert.False(converged); Assert.NotEmpty(violations); // The pinned part may slide into the walls but never through them. AssertNoOverlaps(plate); Assert.Equal(3.8, left.Location.X, 6); Assert.Equal(12.2, right.Location.X, 6); Assert.Equal(3.8, bottom.Location.Y, 6); Assert.Equal(12.2, top.Location.Y, 6); } [Fact] public void Expand_CancelledBeforeRun_LeavesEverythingInPlace() { var plate = MakePlate(24, 24); var a = AddSquare(plate, 6, 10); var b = AddSquare(plate, 14, 10); using var cts = new CancellationTokenSource(); cts.Cancel(); var result = Expander.Expand( new List { a, b }, plate, token: cts.Token ); Assert.True(result.Cancelled); Assert.Equal(6, a.Location.X, 6); Assert.Equal(14, b.Location.X, 6); } [Fact] public void Expand_ThreeInRow_FirstSelectedNeverMoves_AndOracleConfirmsSpacing() { var plate = MakePlate(60, 14); var a = AddSquare(plate, 5, 5); var b = AddSquare(plate, 10, 5); var c = AddSquare(plate, 15, 5); var result = Expander.Expand( new List { a, b, c }, plate, new Expander.Options { MaxSpacing = 8 } ); Assert.Equal(5, a.Location.X, 6); // anchor: never the later index of any pair Assert.True(result.AchievedSpacing >= 7.9); // Independent oracle: every pair clears the reported spacing. var parts = new List { a, b, c }; for (var i = 0; i < parts.Count; i++) for (var j = i + 1; j < parts.Count; j++) { var clearance = BruteClearance(parts[i], parts[j]); Assert.True( clearance >= result.AchievedSpacing - 0.01, $"{parts[i].BaseDrawing.Name}/{parts[j].BaseDrawing.Name}: oracle {clearance} < reported {result.AchievedSpacing}" ); } AssertNoOverlaps(plate); } private static Program RectangleWithHole( double width, double length, double hx, double hy, double hw, double hh ) { var program = Rectangle(width, length); program.MoveTo(hx, hy); program.LineTo(hx + hw, hy); program.LineTo(hx + hw, hy + hh); program.LineTo(hx, hy + hh); program.LineTo(hx, hy); return program; } /// /// Hole-subtracting overlap check matching NestValidator semantics (a part in /// a cutout is legal). Part.Intersects is perimeter-only, so it cannot /// certify part-in-cutout layouts. /// private static bool MateriallyOverlaps(Part a, Part b) { var (outerA, holesA) = Rings(a); var (outerB, holesB) = Rings(b); return Collision.HasOverlap(outerA, outerB, holesA, holesB); } private static (Polygon Outer, List Holes) Rings(Part part) { var entities = OpenNest.Converters.ConvertProgram .ToGeometry(part.Program) .Where(e => SpecialLayers.IsMaterial(e.Layer)) .ToList(); var profile = new ShapeProfile(entities); var outer = profile.Perimeter.ToPolygonWithTolerance(0.001); outer.Offset(part.Location); var holes = new List(); foreach (var cutout in profile.Cutouts) { var hole = cutout.ToPolygonWithTolerance(0.001); hole.Offset(part.Location); holes.Add(hole); } return (outer, holes.Count == 0 ? null : holes); } [Fact] public void Expand_PartInsideCutout_KeepsLegalAndClearsHoleWalls() { var plate = MakePlate(40, 24); // Wall part with a 10x10 cutout; a small selected part sits inside it. var wall = new Part( new Drawing( "wall", RectangleWithHole(20, 20, 5, 5, 10, 10) ), new Vector(0, 0) ); plate.Parts.Add(wall); var inside = new Part(new Drawing("inside", Rectangle(2, 2)), new Vector(9, 9)); var other = new Part(new Drawing("other", Rectangle(2, 2)), new Vector(30, 9)); plate.Parts.Add(inside); plate.Parts.Add(other); var result = Expander.Expand( new List { inside, other }, plate, new Expander.Options { MaxSpacing = 2 } ); // Part-in-cutout is legal, never a material overlap. Assert.False(MateriallyOverlaps(inside, wall)); Assert.False(MateriallyOverlaps(other, wall)); Assert.False(MateriallyOverlaps(inside, other)); Assert.True(result.AchievedSpacing >= 1.9); // The part that started in the cutout must clear the hole walls too. var holeLeft = 5; var holeRight = 15; var gapLeft = inside.Location.X - holeLeft; var gapRight = holeRight - (inside.Location.X + 2); var gapBottom = inside.Location.Y - holeLeft; var gapTop = holeRight - (inside.Location.Y + 2); var minGap = System.Math.Min( System.Math.Min(gapLeft, gapRight), System.Math.Min(gapBottom, gapTop) ); Assert.True(minGap >= 1.9, $"closest hole-wall gap {minGap}"); } [Fact] public void Expand_DegenerateInputs_Throw() { var plate = MakePlate(10, 10); var a = AddSquare(plate, 1, 1); var stranger = new Part(new Drawing("stranger", Rectangle()), new Vector(50, 50)); Assert.Throws(() => Expander.Expand(new List(), plate)); Assert.Throws(() => Expander.Expand(new List { a }, plate)); Assert.Throws(() => Expander.Expand(new List { a, stranger }, plate)); Assert.Throws(() => Expander.Expand(new List { a, a }, null)); } [Fact] public void Separate_PreservesEachEntryGap_EvenWhenTargetIsSmaller() { var plate = MakePlate(30, 12, edge: 0); var anchor = AddSquare(plate, 1, 4, 2); var mover = AddSquare(plate, 4, 4, 2); var wall = AddSquare(plate, 12, 4, 2); var initialWallGap = wall.Location.X - (mover.Location.X + 2); var (converged, positions, _) = Expander.Separate( new List { anchor, mover }, plate, spacing: 3 ); // The wall is six units away at entry. Merely clearing the requested // three-unit spacing is not permission to consume that original gap. Assert.True(positions[1].X + 2 <= wall.Location.X - initialWallGap + 1e-6, $"entry gap {initialWallGap}, proposed gap {wall.Location.X - positions[1].X - 2}"); Assert.Equal(new Vector(4, 4), mover.Location); Assert.Equal(new Vector(12, 4), wall.Location); Assert.False(converged); // This direct translation cannot reach 3 safely. } [Fact] public void Separate_RetriesMiddleAfterLaterBlockerMoves() { var plate = MakePlate(60, 14, edge: 0); var first = AddSquare(plate, 5, 5); var middle = AddSquare(plate, 10, 5); var last = AddSquare(plate, 14, 5); var selected = new List { first, middle, last }; var (converged, positions, violations) = Expander.Separate(selected, plate, spacing: 2); Assert.True(converged, $"remaining: {violations.Count}"); Assert.Empty(violations); Assert.Equal(5, positions[0].X); Assert.True(positions[1].X >= 11 - 1e-4); Assert.True(positions[2].X >= positions[1].X + 6 - 1e-4); Assert.Equal(10, middle.Location.X); // Separate is scratch-only. Assert.Equal(14, last.Location.X); var result = Expander.Expand(selected, plate, new Expander.Options { MaxSpacing = 2 }); Assert.True(result.AchievedSpacing >= 1.99, $"achieved {result.AchievedSpacing}"); Assert.True(middle.Location.X >= 11 - 1e-4); Assert.True(last.Location.X >= middle.Location.X + 6 - 1e-4); Assert.Equal(5, first.Location.X); } [Fact] public void Separate_CannotPassThroughFullHeightWall_ToClearEndpoint() { var plate = MakePlate(30, 12, edge: 0); var anchor = AddSquare(plate, 1, 4, 2); var moving = AddSquare(plate, 4, 4, 2); var wall = AddRectangle(plate, 6.5, 0, 0.2, 12); // Target 5 puts the otherwise clear endpoint beyond the wall AND // preserves the entry wall gap of 0.5. An endpoint-only check passes it. var (converged, poses, violations) = Expander.Separate( new List { anchor, moving }, plate, spacing: 5 ); Assert.False(converged); Assert.NotEmpty(violations); Assert.True(poses[1].X + 2 <= wall.Location.X + 1e-6, $"move crossed wall: right={poses[1].X + 2}, wall={wall.Location.X}"); Assert.Equal(4, moving.Location.X); // public Separate never commits. Assert.Equal(6.5, wall.Location.X); var result = Expander.Expand(new List { anchor, moving }, plate, new Expander.Options { InitialStep = 5, MaxSpacing = 5 }); Assert.True(moving.Location.X + 2 <= wall.Location.X + 1e-6); Assert.True(result.AchievedSpacing < 5); } [Fact] public void Separate_OverlappingPair_CannotExitAcrossThirdWall() { var plate = MakePlate(30, 12, edge: 0); var anchor = AddSquare(plate, 5, 4); var moving = AddSquare(plate, 7, 4); // Overlaps anchor by two. var wall = AddRectangle(plate, 12, 0, 0.2, 12); var (converged, poses, violations) = Expander.Separate( new List { anchor, moving }, plate, spacing: 5 ); Assert.False(converged); Assert.NotEmpty(violations); Assert.True(poses[1].X + 4 <= wall.Location.X + 1e-6, $"overlap exit crossed third wall: {poses[1].X}"); Assert.Equal(7, moving.Location.X); Assert.Equal(12, wall.Location.X); } [Fact] public void Separate_CannotCrossOppositeCutoutWall() { var plate = MakePlate(40, 20, edge: 0); var host = new Part(new Drawing("host", RectangleWithHole(10, 10, 0.2, 0.2, 9.6, 9.6)), new Vector(5, 1)); plate.Parts.Add(host); var anchor = AddSquare(plate, 1, 5, 1); var moving = AddSquare(plate, 5.2, 5, 1); // Touches inner left wall. var (converged, poses, _) = Expander.Separate( new List { anchor, moving }, plate, spacing: 14 ); Assert.False(converged); Assert.True(poses[1].X + 1 <= 14.8 + 1e-6, $"insert crossed opposite hole wall: {poses[1].X}"); Assert.Equal(5.2, moving.Location.X); } [Fact] public void Separate_TinyClampZone_IsNotAnInvisibleWall() { var plate = MakePlate(30, 12, edge: 0); plate.ClampZones.Add(new ClampZone("tiny", new Box(6.5, 0, 1e-5, 1e-5))); var anchor = AddSquare(plate, 1, 0, 2); var moving = AddSquare(plate, 4, 0, 2); var (converged, poses, _) = Expander.Separate( new List { anchor, moving }, plate, spacing: 6 ); Assert.False(converged); Assert.True(poses[1].X + 2 <= 6.5 + 1e-6, $"crossed tiny clamp at x=6.5: right={poses[1].X + 2}"); Assert.Equal(4, moving.Location.X); } [Fact] public void Separate_RemoteTinyClampDoesNotPinUnrelatedGrowth() { var plate = MakePlate(30, 12, edge: 0); plate.ClampZones.Add(new ClampZone("tiny", new Box(0, 0, 1e-5, 1e-5))); var anchor = AddSquare(plate, 1, 4, 2); var moving = AddSquare(plate, 4, 4, 2); var (converged, poses, violations) = Expander.Separate( new List { anchor, moving }, plate, spacing: 3 ); Assert.True(converged, $"remote zone pinned growth: {violations.Count} violations, " + $"poses {poses[0].X:R},{poses[1].X:R}; " + string.Join("; ", violations.Select(v => $"{v.A.BaseDrawing.Name}/{v.B.BaseDrawing.Name}={v.Achieved:R}"))); Assert.True(poses[1].X >= 6 - 1e-4); Assert.Equal(4, moving.Location.X); } [Fact] public void Separate_TouchingHoleWall_UsesOppositeDirectionToOpenGap() { var plate = MakePlate(40, 24, edge: 0); var host = new Part(new Drawing("host", RectangleWithHole(20, 20, 5, 5, 10, 10)), new Vector(0, 0)); plate.Parts.Add(host); var insert = AddSquare(plate, 13, 9, 2); var (converged, poses, violations) = Expander.Separate( new List { insert }, plate, spacing: 1 ); Assert.True(converged, $"contact still blocked: {violations.Count}"); Assert.True(poses[0].X <= 12 + 1e-4, $"insert at x={poses[0].X}"); Assert.True(poses[0].X >= 5 + 1 - 1e-4); Assert.Equal(13, insert.Location.X); Assert.Equal(0, host.Location.X); } [Theory] [InlineData(13, 9)] [InlineData(5, 9)] [InlineData(9, 13)] [InlineData(9, 5)] [InlineData(12.9, 9)] // Positive gap uses its ordinary nearest-boundary direction. public void Separate_FourHoleSides_ReachARealUnitGap(double x, double y) { var plate = MakePlate(40, 24, edge: 0); var host = new Part(new Drawing("host", RectangleWithHole(20, 20, 5, 5, 10, 10)), Vector.Zero); plate.Parts.Add(host); var insert = AddSquare(plate, x, y, 2); var (converged, poses, violations) = Expander.Separate(new List { insert }, plate, 1); Assert.True(converged, $"{x},{y}: {violations.Count} violations"); Assert.InRange(poses[0].X, 6 - 1e-4, 12 + 1e-4); Assert.InRange(poses[0].Y, 6 - 1e-4, 12 + 1e-4); Assert.Equal(x, insert.Location.X); Assert.Equal(y, insert.Location.Y); } [Fact] public void Expand_TouchingHoleInsertWithSecondSelectedPart_StaysInsideCutout() { var plate = MakePlate(40, 24, edge: 0); var host = new Part(new Drawing("host", RectangleWithHole(20, 20, 5, 5, 10, 10)), Vector.Zero); plate.Parts.Add(host); var insert = AddSquare(plate, 13, 9, 2); var other = AddSquare(plate, 30, 9, 2); var result = Expander.Expand(new List { insert, other }, plate, new Expander.Options { MaxSpacing = 1 }); Assert.False(result.Cancelled); Assert.True(result.AchievedSpacing >= 0.99); Assert.InRange(insert.Location.X, 6 - 1e-4, 12 + 1e-4); Assert.False(MateriallyOverlaps(insert, host)); Assert.Equal(0, host.Location.X); } [Fact] public void Separate_ReportsPartBlockerRatherThanPlateEdge() { var plate = MakePlate(20, 20, edge: 0); var pinned = AddSquare(plate, 8, 8); AddSquare(plate, 3.8, 8); AddSquare(plate, 12.2, 8); AddSquare(plate, 8, 3.8); AddSquare(plate, 8, 12.2); var (_, poses, violations) = Expander.Separate(new List { pinned }, plate, 1); Assert.NotEmpty(violations); Assert.All(violations, v => Assert.False(v.BlockedByEdge, $"{v.B.BaseDrawing.Name}: blocker before the work-area edge, pose={poses[0]}")); } [Fact] public void Separate_ReportsActualPlateEdgeWhenItLimitsMovement() { var plate = MakePlate(12, 12, edge: 0); var anchor = AddSquare(plate, 1, 4, 2); var moving = AddSquare(plate, 7, 4, 2); var (_, poses, violations) = Expander.Separate(new List { anchor, moving }, plate, 9); var pair = Assert.Single(violations.Where(v => ReferenceEquals(v.A, anchor) && ReferenceEquals(v.B, moving))); Assert.True(pair.BlockedByEdge); Assert.True(poses[1].X + 2 <= 12 + 1e-6); } [Fact] public void Separate_ReportsPartBeforeFartherEdge_OnNarrowPlate() { var plate = MakePlate(9, 2, edge: 0); var anchor = AddSquare(plate, 0, 0, 2); var moving = AddSquare(plate, 3, 0, 2); var wall = AddRectangle(plate, 5.5, 0, 0.2, 2); var (_, poses, violations) = Expander.Separate(new List { anchor, moving }, plate, 4); var pair = Assert.Single(violations.Where(v => ReferenceEquals(v.A, anchor) && ReferenceEquals(v.B, moving))); Assert.False(pair.BlockedByEdge); Assert.True(poses[1].X + 2 <= wall.Location.X + 1e-6); } [Theory] [InlineData((int)Expander.Checkpoint.AfterScratchMove)] [InlineData((int)Expander.Checkpoint.AfterSuccessfulProbe)] [InlineData((int)Expander.Checkpoint.BeforeApply)] public void Expand_CancellationAtDeterministicCheckpoint_LeavesLiveStateBitIdentical(int stage) { var cancelAt = (Expander.Checkpoint)stage; var plate = MakePlate(30, 12, edge: 0); var anchor = AddSquare(plate, 5, 4); var moving = AddSquare(plate, 10, 4); var wall = AddSquare(plate, 0, 4); var before = plate.Parts.Select(p => ( Part: p, Program: p.Program, X: BitConverter.DoubleToInt64Bits(p.Location.X), Y: BitConverter.DoubleToInt64Bits(p.Location.Y), Left: BitConverter.DoubleToInt64Bits(p.BoundingBox.Left))).ToArray(); var partSpacing = plate.PartSpacing; var edgeSpacing = plate.EdgeSpacing; using var cts = new CancellationTokenSource(); var seen = false; var result = Expander.ExpandObserved(new List { anchor, moving }, plate, new Expander.Options { MaxSpacing = 3 }, checkpoint => { if (checkpoint != cancelAt) return; seen = true; cts.Cancel(); }, cts.Token); Assert.True(seen, $"checkpoint {cancelAt} was not reached"); Assert.True(result.Cancelled); foreach (var state in before) { Assert.Same(state.Program, state.Part.Program); Assert.Equal(state.X, BitConverter.DoubleToInt64Bits(state.Part.Location.X)); Assert.Equal(state.Y, BitConverter.DoubleToInt64Bits(state.Part.Location.Y)); Assert.Equal(state.Left, BitConverter.DoubleToInt64Bits(state.Part.BoundingBox.Left)); } Assert.Equal(partSpacing, plate.PartSpacing); Assert.Equal(edgeSpacing, plate.EdgeSpacing); Assert.Equal(0, wall.Location.X); } [Fact] public void Separate_PinnedBetweenFixedParts_StopsAfterOneNoProgressPass() { var plate = MakePlate(20, 20, edge: 0); var pinned = AddSquare(plate, 8, 8); AddSquare(plate, 3.8, 8); AddSquare(plate, 12.2, 8); AddSquare(plate, 8, 3.8); AddSquare(plate, 8, 12.2); var passes = 0; var initial = pinned.Location; var (converged, poses, violations) = Expander.SeparateObserved( new List { pinned }, plate, spacing: 1, checkpoint => { if (checkpoint == Expander.Checkpoint.AfterPairPass) passes++; }, maxIterations: 10000 ); Assert.False(converged); Assert.NotEmpty(violations); Assert.Equal(1, passes); // Not a MaxIterations spin or wall-clock assertion. Assert.Equal(initial, poses[0]); Assert.Equal(initial, pinned.Location); } [Fact] public void Expand_WallsAndUnselectedPairs_StayExactlyAtClearance() { // Selection must not be pushed to open gaps between parts it excludes. var plate = MakePlate(40, 12); var w1 = AddSquare(plate, 2, 4); var w2 = AddSquare(plate, 6.5, 4); // 0.5 apart from w1, both unselected var a = AddSquare(plate, 14, 4); var b = AddSquare(plate, 20, 4); Expander.Expand(new List { a, b }, plate); Assert.Equal(2, w1.Location.X, 6); Assert.Equal(6.5, w2.Location.X, 6); Assert.True(a.Intersects(w1, out _) == false); Assert.True(b.Intersects(w2, out _) == false); } }