mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-03 18:22:10 -04:00
fix(irregular): preserve analytic line bounds after polygon cleanup
This commit is contained in:
1 parent
25755a8850
commit
bd5db59e42
3 files changed
+91
-6
No files matched your search
@@ -143,6 +143,72 @@ public class IrregularExactContactTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(0.0, false)]
|
||||||
|
[InlineData(System.Math.PI / 2, false)]
|
||||||
|
[InlineData(System.Math.PI, false)]
|
||||||
|
[InlineData(3 * System.Math.PI / 2, false)]
|
||||||
|
[InlineData(0.0, true)]
|
||||||
|
[InlineData(System.Math.PI / 2, true)]
|
||||||
|
[InlineData(System.Math.PI, true)]
|
||||||
|
[InlineData(3 * System.Math.PI / 2, true)]
|
||||||
|
public void ShortEdgeChainBoundsKeepEveryAnalyticEndpoint(double angle, bool extraOrientation)
|
||||||
|
{
|
||||||
|
var job = Job(new[] { Part("rect", Shapes.Polyline(ShortEdgeChainVertices()), 1,
|
||||||
|
RotationPolicy.Fixed(extraOrientation ? 0 : angle)) }, new[] { Stock("sheet", 20, 20) });
|
||||||
|
var type = PartCatalog.Build(job).Single();
|
||||||
|
var orientation = extraOrientation
|
||||||
|
? PartCatalog.CreateOrientation(type, 100, angle)!
|
||||||
|
: type.Orientations.Single();
|
||||||
|
Assert.NotNull(orientation);
|
||||||
|
|
||||||
|
var (c, s) = (System.Math.Cos(angle), System.Math.Sin(angle));
|
||||||
|
var endpoints = ShortEdgeChainVertices()
|
||||||
|
.Select(p => (X: p.X * c - p.Y * s, Y: p.X * s + p.Y * c)).ToArray();
|
||||||
|
Assert.Equal(endpoints.Min(p => p.X), orientation.MinX, 12);
|
||||||
|
Assert.Equal(endpoints.Min(p => p.Y), orientation.MinY, 12);
|
||||||
|
Assert.Equal(endpoints.Max(p => p.X), orientation.MaxX, 12);
|
||||||
|
Assert.Equal(endpoints.Max(p => p.Y), orientation.MaxY, 12);
|
||||||
|
Assert.Equal(PartCatalog.ChordTolerance, orientation.Tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortEdgeChainCannotEscapeAnExactWorkArea()
|
||||||
|
{
|
||||||
|
var job = ShortEdgeChainJob(8.5);
|
||||||
|
var result = new IrregularNestingEngine().Solve(job);
|
||||||
|
|
||||||
|
Assert.Empty(NestLayoutCheck.Violations(job, result));
|
||||||
|
AssertUnplaced(job, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ShortEdgeChainFitsWhenStockContainsItsTrueExtent()
|
||||||
|
{
|
||||||
|
var job = ShortEdgeChainJob(8.500024);
|
||||||
|
var result = new IrregularNestingEngine().Solve(job);
|
||||||
|
|
||||||
|
Assert.Equal(NestJobStatus.Complete, result.Status);
|
||||||
|
Assert.Equal((1, 1, 0), (result.Fulfillment.Single().Requested,
|
||||||
|
result.Fulfillment.Single().Placed, result.Fulfillment.Single().Unplaced));
|
||||||
|
var sheet = Assert.Single(result.Plates);
|
||||||
|
var pose = Assert.Single(sheet.Placements);
|
||||||
|
Assert.True(OracleGaps(sheet.Stock, pose, ShortEdgeChainVertices()).Min() >= Edge - OracleNoise);
|
||||||
|
LayoutAssert.Valid(job, result);
|
||||||
|
Assert.Empty(NestLayoutCheck.Violations(job, result));
|
||||||
|
Validate(job, pose);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polygon cleanup can collapse these individually short edges, but their accumulated
|
||||||
|
// outward extent is real material and exceeds the unchanged work-area slack.
|
||||||
|
private static (double X, double Y)[] ShortEdgeChainVertices() =>
|
||||||
|
[(0, 0), (0, 3), (8, 3), (8.000008, 2.999992),
|
||||||
|
(8.000016, 2.999984), (8.000024, 2.999976), (8, 0)];
|
||||||
|
|
||||||
|
private static NestJob ShortEdgeChainJob(double length) =>
|
||||||
|
Job(new[] { Part("rect", Shapes.Polyline(ShortEdgeChainVertices()), 1, RotationPolicy.Fixed(0)) },
|
||||||
|
new[] { Stock("sheet", 3.5, length, spacing: Spacing, edge: new Spacing(Edge, Edge, Edge, Edge)) });
|
||||||
|
|
||||||
private static NestJob RectangleJob(double width, double length, int quadrant, RotationPolicy rotation) =>
|
private static NestJob RectangleJob(double width, double length, int quadrant, RotationPolicy rotation) =>
|
||||||
Job(new[] { Rectangle("rect", PartLength, PartWidth, 1, rotation) },
|
Job(new[] { Rectangle("rect", PartLength, PartWidth, 1, rotation) },
|
||||||
new[] { Stock("sheet", width, length, spacing: Spacing, edge: new Spacing(Edge, Edge, Edge, Edge), quadrant: quadrant) });
|
new[] { Stock("sheet", width, length, spacing: Spacing, edge: new Spacing(Edge, Edge, Edge, Edge), quadrant: quadrant) });
|
||||||
@@ -174,11 +240,15 @@ public class IrregularExactContactTests
|
|||||||
|
|
||||||
/// <summary>Clearance from the rotated nominal rectangle to each physical sheet edge
|
/// <summary>Clearance from the rotated nominal rectangle to each physical sheet edge
|
||||||
/// (left, bottom, right, top), computed from corners and the quadrant's sheet origin.</summary>
|
/// (left, bottom, right, top), computed from corners and the quadrant's sheet origin.</summary>
|
||||||
private static double[] OracleGaps(NestPlateStock stock, NestJobPlacement pose)
|
private static double[] OracleGaps(NestPlateStock stock, NestJobPlacement pose) =>
|
||||||
|
OracleGaps(stock, pose, [(0, 0), (PartLength, 0), (PartLength, PartWidth), (0, PartWidth)]);
|
||||||
|
|
||||||
|
private static double[] OracleGaps(NestPlateStock stock, NestJobPlacement pose,
|
||||||
|
(double X, double Y)[] vertices)
|
||||||
{
|
{
|
||||||
var (c, s) = (System.Math.Cos(pose.Rotation), System.Math.Sin(pose.Rotation));
|
var (c, s) = (System.Math.Cos(pose.Rotation), System.Math.Sin(pose.Rotation));
|
||||||
var corners = new[] { (0.0, 0.0), (PartLength, 0.0), (PartLength, PartWidth), (0.0, PartWidth) }
|
var corners = vertices
|
||||||
.Select(p => (X: p.Item1 * c - p.Item2 * s + pose.X, Y: p.Item1 * s + p.Item2 * c + pose.Y))
|
.Select(p => (X: p.X * c - p.Y * s + pose.X, Y: p.X * s + p.Y * c + pose.Y))
|
||||||
.ToArray();
|
.ToArray();
|
||||||
var sheetLeft = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length;
|
var sheetLeft = stock.Quadrant is 1 or 4 ? 0 : -stock.Size.Length;
|
||||||
var sheetBottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width;
|
var sheetBottom = stock.Quadrant is 1 or 2 ? 0 : -stock.Size.Width;
|
||||||
|
|||||||
@@ -171,9 +171,17 @@ internal static class PartCatalog
|
|||||||
double tolerance, Shape perimeter)
|
double tolerance, Shape perimeter)
|
||||||
{
|
{
|
||||||
var bounds = Clipper.GetBounds(outline);
|
var bounds = Clipper.GetBounds(outline);
|
||||||
// Only flattened arcs deviate from the true perimeter; line vertices are exact, so a
|
// Curves retain flattening-error padding. For lines, use analytic endpoint bounds:
|
||||||
// line-only part keeps its nominal material bounds and can fill its work area exactly.
|
// polygon cleanup can discard short-edge chains and shrink the material's true extent.
|
||||||
var padding = perimeter.Entities.All(e => e.Type == EntityType.Line) ? 0 : tolerance;
|
var padding = tolerance;
|
||||||
|
if (perimeter.Entities.All(e => e.Type == EntityType.Line))
|
||||||
|
{
|
||||||
|
var nominal = (Shape)perimeter.Clone();
|
||||||
|
nominal.Rotate(angle);
|
||||||
|
var box = nominal.BoundingBox;
|
||||||
|
bounds = new RectD(box.Left, box.Bottom, box.Right, box.Top);
|
||||||
|
padding = 0;
|
||||||
|
}
|
||||||
return new Orientation
|
return new Orientation
|
||||||
{
|
{
|
||||||
TypeIndex = typeIndex,
|
TypeIndex = typeIndex,
|
||||||
|
|||||||
@@ -25,6 +25,13 @@ A free box may absorb a slightly oversized side only at its right/top edge when
|
|||||||
coincides with the plate work-area boundary. Internal leftover edges keep the strict packing
|
coincides with the plate work-area boundary. Internal leftover edges keep the strict packing
|
||||||
tolerance, and actual part dimensions still determine spacing away from the plate boundary.
|
tolerance, and actual part dimensions still determine spacing away from the plate boundary.
|
||||||
|
|
||||||
|
Irregular keeps nominal orientation bounds for line-only outlines, so an allowed rotation can
|
||||||
|
fit exactly between the configured plate-edge gaps. These bounds use original rotated line
|
||||||
|
endpoints, retaining material extents even when polygon cleanup discards short-edge chains.
|
||||||
|
Curved outlines retain conservative flattening-error padding. This does not relax part-spacing
|
||||||
|
footprints, no-fit polygons or layout validation tolerances; a part extending beyond the accepted
|
||||||
|
work-area bounds remains invalid.
|
||||||
|
|
||||||
Irregular fills gaps and open notches using outer profiles; it does not yet place parts inside
|
Irregular fills gaps and open notches using outer profiles; it does not yet place parts inside
|
||||||
enclosed cutouts. For a part with two or more copies it also offers its best-fit pairs (two copies
|
enclosed cutouts. For a part with two or more copies it also offers its best-fit pairs (two copies
|
||||||
interlocked, as the Best Fit viewer shows them) alongside the single copies, and places a pair
|
interlocked, as the Best Fit viewer shows them) alongside the single copies, and places a pair
|
||||||
|
|||||||
Reference in new issue
Block a user