mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
feat(nest): keep parts out of tabbed cutouts
A tabbed cutout's slug stays attached while the sheet is cut, but nesting saw a closed hole and could place parts inside it. Nesting and layout paths now read material through NestingMaterial.ForNesting, which drops closed contours with tab spans so the slug counts as part. That covers part outlines and collision lines, the cutout router, fill expansion, best-fit, NestLayoutCheck and the GPU bitmaps. Area, overlap diagnostics and cutting keep the hole, and programs without tabs read exactly as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ba39c3dc55
commit
0186ccfbff
12 files changed
+139
-21
No files matched your search
@@ -59,7 +59,7 @@ Read the relevant contract before changing its behavior:
|
||||
- [Directional slides](docs/geometry/directional-slides.md) and [pair-spacing limits](docs/geometry/pair-spacing.md)
|
||||
- [Lead-in placement](docs/geometry/lead-in-placement.md)
|
||||
- [Breakoffs](docs/breakoffs.md): tabbed scrap; `Break`-layer open cuts that are cut but are not material
|
||||
- [Tabbed cutouts](docs/tabbed-cutouts.md): holes cut in pieces around `Tab`-layer spans that are material but never cut
|
||||
- [Tabbed cutouts](docs/tabbed-cutouts.md): holes cut in pieces around `Tab`-layer spans that are material but never cut; nesting reads material through `NestingMaterial.ForNesting`, so nothing nests in them
|
||||
- [Material-overlap diagnostics](docs/geometry/visual-overlap-check.md)
|
||||
- [Automatic cutoffs and sequencing](docs/automatic-scrap-cutoffs.md)
|
||||
- [Pre-post verification](docs/post-verification.md)
|
||||
|
||||
@@ -352,7 +352,7 @@ namespace OpenNest
|
||||
PerfCounters.CountOverlapPolygonPreparation();
|
||||
return ConvertProgram
|
||||
.ToGeometry(program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ForNesting()
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace OpenNest
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var shapes = ShapeBuilder.GetShapes(
|
||||
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
entities.ForNesting()
|
||||
);
|
||||
var lines = new List<Line>();
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace OpenNest
|
||||
PerfCounters.CountOffsetPerimeterEntities();
|
||||
var geoEntities = ConvertProgram.ToGeometry(program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
geoEntities.ForNesting().ToList()
|
||||
);
|
||||
|
||||
var offsetShape = profile.Perimeter.OffsetOutward(spacing);
|
||||
@@ -75,7 +75,7 @@ namespace OpenNest
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
geoEntities.ForNesting().ToList()
|
||||
);
|
||||
var entities = new List<Entity>();
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace OpenNest
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
geoEntities.ForNesting().ToList()
|
||||
);
|
||||
|
||||
return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||
@@ -122,7 +122,7 @@ namespace OpenNest
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
geoEntities.ForNesting().ToList()
|
||||
);
|
||||
var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace OpenNest
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var shapes = ShapeBuilder.GetShapes(
|
||||
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
entities.ForNesting()
|
||||
);
|
||||
var lines = new List<Line>();
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest
|
||||
{
|
||||
public static class NestingMaterial
|
||||
{
|
||||
/// <summary>
|
||||
/// The material entities nesting and layout validation place parts against: those
|
||||
/// <see cref="SpecialLayers.IsMaterial"/> keeps, except that a tabbed cutout's slug stays
|
||||
/// attached while the sheet is cut, so a closed hole with <see cref="SpecialLayers.Tab"/>
|
||||
/// spans is dropped and its slug counts as part. Area, overlap diagnostics and cutting
|
||||
/// keep the hole. Entities without tab spans pass through unchanged.
|
||||
/// </summary>
|
||||
public static List<Entity> ForNesting(this IEnumerable<Entity> entities)
|
||||
{
|
||||
var material = entities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList();
|
||||
if (!material.Exists(e => e.Layer == SpecialLayers.Tab))
|
||||
return material;
|
||||
|
||||
var kept = new List<Entity>();
|
||||
foreach (var shape in ShapeBuilder.GetShapes(material))
|
||||
if (!shape.Entities.Exists(e => e.Layer == SpecialLayers.Tab))
|
||||
kept.AddRange(shape.Entities);
|
||||
return kept;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Converters;
|
||||
using OpenNest.Engine.Jobs;
|
||||
using OpenNest.Engine.Jobs.Adapters;
|
||||
using OpenNest.Geometry;
|
||||
using OpenNest.TabbedCutouts;
|
||||
|
||||
namespace OpenNest.Engine.Tests.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// A 20 x 20 frame with a 10 x 10 hole and a 6 x 6 insert on a 22 x 22 sheet: the insert only
|
||||
/// fits inside the hole, which a tabbed cutout's attached slug fills.
|
||||
/// </summary>
|
||||
[Collection("Cutout pipeline registry")]
|
||||
public class TabbedCutoutNestingTests
|
||||
{
|
||||
private static Program Frame(bool tabbed)
|
||||
{
|
||||
var hole = new Entity[] { new Line(5, 5, 15, 5), new Line(15, 5, 15, 15), new Line(15, 15, 5, 15), new Line(5, 15, 5, 5) };
|
||||
var entities = new List<Entity>
|
||||
{
|
||||
new Line(0, 0, 20, 0), new Line(20, 0, 20, 20), new Line(20, 20, 0, 20), new Line(0, 20, 0, 0),
|
||||
};
|
||||
entities.AddRange(hole);
|
||||
if (tabbed)
|
||||
entities = TabbedCutoutBuilder.Apply(entities,
|
||||
[new TabbedCutout { EdgeIds = hole.Select(e => e.Id).ToList() }], out _);
|
||||
return ConvertGeometry.ToProgram(ShapeProfile.NormalizeEntities(entities));
|
||||
}
|
||||
|
||||
private static NestPipelineRequest Request(string engine, bool tabbed) =>
|
||||
new(engine,
|
||||
new[] { new NestItem { Drawing = new Drawing("frame", Frame(tabbed)), Quantity = 1 },
|
||||
new NestItem { Drawing = new Drawing("insert", TestDrawingFactory.Rectangle(6, 6)), Quantity = 1 } },
|
||||
new[] { new NestPlateStock("sheet", new Size(22, 22), 1, 0.25) },
|
||||
new NestJobOptions(maxPlates: 1));
|
||||
|
||||
[Fact]
|
||||
public void TabbedHole_IsNoCutoutForNesting_AndItsSlugCountsAsMaterial()
|
||||
{
|
||||
var geometry = JobPartGeometry.Read(DrawingJobMapper.FromDrawing("frame", new Drawing("frame", Frame(true)), 1).Geometry);
|
||||
|
||||
Assert.Empty(geometry.Cutouts);
|
||||
Assert.Equal(400, geometry.MaterialArea, 6);
|
||||
Assert.Single(JobPartGeometry.Read(
|
||||
DrawingJobMapper.FromDrawing("frame", new Drawing("frame", Frame(false)), 1).Geometry).Cutouts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TabbedHole_KeepsItsHoleForAreaAndCutting()
|
||||
{
|
||||
var drawing = new Drawing("frame", Frame(true));
|
||||
|
||||
Assert.Equal(300, drawing.Area, 6);
|
||||
// Nesting sees only the frame's outline: the hole's pieces and tabs are gone.
|
||||
var nesting = ConvertProgram.ToGeometry(drawing.Program).ForNesting();
|
||||
Assert.Equal(4, nesting.Count);
|
||||
Assert.Equal(400, new ShapeProfile(nesting).Perimeter.Area(), 6);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UntabbedEntities_PassThroughUnchanged()
|
||||
{
|
||||
var material = ConvertProgram.ToGeometry(Frame(false));
|
||||
|
||||
Assert.Equal(material.Where(e => SpecialLayers.IsMaterial(e.Layer)), material.ForNesting());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Irregular")]
|
||||
[InlineData("Default")]
|
||||
public void NoPartIsNestedInsideATabbedHole(string engine)
|
||||
{
|
||||
var plain = NestPipeline.Run(Request(engine, tabbed: false));
|
||||
Assert.Equal(NestJobStatus.Complete, plain.Status);
|
||||
|
||||
var result = NestPipeline.Run(Request(engine, tabbed: true));
|
||||
|
||||
Assert.True(result.IsValid, string.Join("; ", result.Violations));
|
||||
Assert.Equal(NestJobStatus.Incomplete, result.Status);
|
||||
Assert.DoesNotContain(result.Plates.SelectMany(p => p.Parts), p => p.BaseDrawing.Name == "insert");
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
var partEntities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ForNesting()
|
||||
.ToList();
|
||||
|
||||
foreach (var entity in partEntities)
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace OpenNest.Engine.BestFit
|
||||
{
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(drawing.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ForNesting()
|
||||
.ToList();
|
||||
|
||||
if (entities.Count == 0)
|
||||
|
||||
@@ -662,7 +662,7 @@ namespace OpenNest.Engine.Fill
|
||||
var prepared = new RingSet();
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ForNesting()
|
||||
.ToList();
|
||||
|
||||
if (entities.Count > 0)
|
||||
|
||||
@@ -66,10 +66,8 @@ public sealed class JobPartGeometry
|
||||
|| !double.IsFinite(m.CenterX) || !double.IsFinite(m.CenterY)))
|
||||
throw new ArgumentException("Geometry must contain finite motions.", nameof(geometry));
|
||||
var entities = ConvertProgram.ToGeometry(DrawingJobMapper.ToProgram(geometry));
|
||||
var cutEntities = new List<Entity>();
|
||||
foreach (var entity in entities)
|
||||
if (SpecialLayers.IsMaterial(entity.Layer))
|
||||
cutEntities.Add(entity);
|
||||
// A tabbed cutout's slug stays attached, so its hole is no room for other parts.
|
||||
var cutEntities = entities.ForNesting();
|
||||
|
||||
var contours = ShapeBuilder.GetShapes(cutEntities);
|
||||
if (contours.Count == 0)
|
||||
|
||||
@@ -577,7 +577,7 @@ public static class NestLayoutCheck
|
||||
{
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ForNesting()
|
||||
.ToList();
|
||||
|
||||
if (entities.Count == 0)
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace OpenNest.Gpu
|
||||
{
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer));
|
||||
.ForNesting();
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
|
||||
var polygons = new List<Polygon>();
|
||||
@@ -167,7 +167,7 @@ namespace OpenNest.Gpu
|
||||
{
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(drawing.Program)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer));
|
||||
.ForNesting();
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
|
||||
var polygons = new List<Polygon>();
|
||||
|
||||
+11
-3
@@ -29,9 +29,17 @@ the cut pieces and tabs, and marks where each piece is pierced.
|
||||
## Tabs are material, never cut
|
||||
|
||||
Tab spans bound material like `Cut` motions: `SpecialLayers.IsMaterial` keeps them, so
|
||||
area, nesting, material capture (`LeadMaterialSnapshot`) and the material-overlap check
|
||||
all see a closed hole. They are never cut. The plate view fills them but does not draw
|
||||
them as cuts.
|
||||
area, material capture (`LeadMaterialSnapshot`) and the material-overlap check all see a
|
||||
closed hole. They are never cut. The plate view fills them but does not draw them as cuts.
|
||||
|
||||
Nesting is the exception: the slug stays attached while the sheet is cut, so nothing may
|
||||
be placed in the hole. Nesting and layout paths read material through
|
||||
`NestingMaterial.ForNesting`, which drops any closed contour with tab spans so the slug
|
||||
counts as part: part outlines and collision lines (`PartGeometry`, `Part`), the cutout
|
||||
router (`JobPartGeometry`, whose material area then includes the slug), fill expansion,
|
||||
best-fit, `NestLayoutCheck` and the GPU bitmaps. Programs without tab spans read exactly as
|
||||
before. A planned program has open pieces instead of a closed hole, so it bounds no hole
|
||||
either way.
|
||||
|
||||
Both Plan Cutting (`PreparedContours`) and Assign Lead-ins
|
||||
(`ContourCuttingStrategy.Apply`/`ApplySingle`) take a closed contour with tab spans out of
|
||||
|
||||
Reference in new issue
Block a user