mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-15 02:31:54 -04:00
557 lines
21 KiB
C#
557 lines
21 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// PlateView spacing expander: grows part-to-part spacing with the work area
|
|
/// and non-selected parts as hard boundaries.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Independent clearance oracle: naive vertex/segment min distance over raw part lines.</summary>
|
|
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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<Polygon> 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<Polygon>();
|
|
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<Part> { 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<ArgumentException>(() => Expander.Expand(new List<Part>(), plate));
|
|
Assert.Throws<ArgumentException>(() => Expander.Expand(new List<Part> { a }, plate));
|
|
Assert.Throws<ArgumentException>(() => Expander.Expand(new List<Part> { a, stranger }, plate));
|
|
Assert.Throws<ArgumentNullException>(() => Expander.Expand(new List<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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<Part> { 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 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<Part> { 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);
|
|
}
|
|
}
|