fix: leave etch/scribe marks out of nesting geometry
Every nesting-geometry consumer filtered only rapids, so scribe/etch moves counted as part material. An etch tick that ends a hair outside the outline (PEP bend ticks start on the notch edge) made the part "open geometry leaving the material region": the job validator threw and every built-in engine plus Gpt6Astra crashed on real PEP jobs (PT75, drawing 4980 A01 PT77). Marks are only on the surface, so they should never affect placement, collision, area, or validation. - SpecialLayers.IsMaterial excludes Rapid and Scribe; used by drawing area, canonical angle, part collision, PartGeometry, plate perimeter, best-fit/pair evaluation, rotation analysis, GPU evaluators, and both validators. Timing, display, splitting and posts still see marks. - ConvertGeometry also maps the saved SCRIBE layer name to Scribe, so programs rebuilt from stored entities keep their marks. - NestReader repairs older files (e.g. PepNestExport output) whose programs saved etch as cut moves while source entities kept SCRIBE. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,7 @@ namespace OpenNest
|
||||
|
||||
var entities = ConvertProgram
|
||||
.ToGeometry(drawing.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid);
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer));
|
||||
|
||||
var shapes = ShapeBuilder.GetShapes(entities);
|
||||
if (shapes.Count == 0)
|
||||
|
||||
@@ -139,14 +139,17 @@ namespace OpenNest.Converters
|
||||
return lastpt;
|
||||
}
|
||||
|
||||
// Engrave/etch geometry maps to Scribe so the post processor can treat it as a
|
||||
// separate tool pass; everything else keeps the move's default Cut layer.
|
||||
// Engrave/etch/scribe geometry maps to Scribe so the post processor can treat it as a
|
||||
// separate tool pass; everything else keeps the move's default Cut layer. SCRIBE is the
|
||||
// name marks carry once saved (SpecialLayers.Scribe), so drawings rebuilt from stored
|
||||
// entities must map it too or their marks silently become cut moves.
|
||||
private static LayerType ClassifyLayer(Entity geo)
|
||||
{
|
||||
var name = geo.Layer?.Name;
|
||||
if (
|
||||
string.Equals(name, "ENGRAVE", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, "ETCH", System.StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(name, SpecialLayers.Scribe.Name, System.StringComparison.OrdinalIgnoreCase)
|
||||
)
|
||||
return LayerType.Scribe;
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenNest.CNC;
|
||||
using OpenNest.Geometry;
|
||||
|
||||
namespace OpenNest.Converters
|
||||
{
|
||||
/// <summary>
|
||||
/// Restores the Scribe layer on program moves that were saved as cuts. Programs built from
|
||||
/// stored entities before ConvertGeometry recognized the SCRIBE layer name turned etch marks
|
||||
/// into Cut moves, which nesting then treated as open cut geometry. The drawing's source
|
||||
/// entities still carry the mark layer, so matching moves are reclassified from them.
|
||||
/// </summary>
|
||||
public static class ScribeLayerRepair
|
||||
{
|
||||
private const double MatchTolerance = 0.001;
|
||||
|
||||
/// <summary>
|
||||
/// Reclassifies Cut moves in <paramref name="program"/> that lie on a mark entity in
|
||||
/// <paramref name="sourceEntities"/>. Program coordinates are source coordinates shifted
|
||||
/// by -<paramref name="sourceOffset"/>. Returns the number of moves reclassified.
|
||||
/// </summary>
|
||||
public static int Apply(Program program, IEnumerable<Entity> sourceEntities, Vector sourceOffset)
|
||||
{
|
||||
if (program == null || sourceEntities == null)
|
||||
return 0;
|
||||
|
||||
var marks = sourceEntities.Where(e => IsMarkLayer(e.Layer)).ToList();
|
||||
if (marks.Count == 0 || program.Codes.Any(c => c is SubProgramCall))
|
||||
return 0;
|
||||
|
||||
// ToGeometry emits exactly one entity per rapid/linear/arc move of a flat program.
|
||||
var motions = program.Codes.Where(c => c is RapidMove or LinearMove or ArcMove).ToList();
|
||||
var geometry = ConvertProgram.ToGeometry(program);
|
||||
if (geometry.Count != motions.Count)
|
||||
return 0;
|
||||
|
||||
var repaired = 0;
|
||||
for (var i = 0; i < motions.Count; i++)
|
||||
{
|
||||
var source = Translate(geometry[i], sourceOffset);
|
||||
switch (motions[i])
|
||||
{
|
||||
case LinearMove line when line.Layer == LayerType.Cut && IsOnMark(source, marks):
|
||||
line.Layer = LayerType.Scribe;
|
||||
repaired++;
|
||||
break;
|
||||
case ArcMove arc when arc.Layer == LayerType.Cut && IsOnMark(source, marks):
|
||||
arc.Layer = LayerType.Scribe;
|
||||
repaired++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return repaired;
|
||||
}
|
||||
|
||||
public static bool IsMarkLayer(Layer layer) =>
|
||||
layer != null
|
||||
&& (
|
||||
layer == SpecialLayers.Scribe
|
||||
|| string.Equals(layer.Name, SpecialLayers.Scribe.Name, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(layer.Name, "ETCH", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(layer.Name, "ENGRAVE", StringComparison.OrdinalIgnoreCase)
|
||||
);
|
||||
|
||||
private static List<Vector> Translate(Entity entity, Vector offset)
|
||||
{
|
||||
var points = entity switch
|
||||
{
|
||||
Line l => new List<Vector>
|
||||
{
|
||||
l.StartPoint,
|
||||
l.EndPoint,
|
||||
new Vector((l.StartPoint.X + l.EndPoint.X) / 2, (l.StartPoint.Y + l.EndPoint.Y) / 2),
|
||||
},
|
||||
Arc a => new List<Vector> { a.StartPoint(), a.EndPoint(), a.MidPoint() },
|
||||
Circle c => new List<Vector>
|
||||
{
|
||||
new Vector(c.Center.X + c.Radius, c.Center.Y),
|
||||
new Vector(c.Center.X - c.Radius, c.Center.Y),
|
||||
new Vector(c.Center.X, c.Center.Y + c.Radius),
|
||||
},
|
||||
_ => new List<Vector>(),
|
||||
};
|
||||
return points.ConvertAll(p => new Vector(p.X + offset.X, p.Y + offset.Y));
|
||||
}
|
||||
|
||||
// A move is a mark when every sample point lies on one single mark entity.
|
||||
private static bool IsOnMark(List<Vector> points, List<Entity> marks) =>
|
||||
points.Count > 0
|
||||
&& marks.Any(m => points.All(p => m.ClosestPointTo(p).DistanceTo(p) <= MatchTolerance));
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ namespace OpenNest
|
||||
{
|
||||
var geometry = ConvertProgram
|
||||
.ToGeometry(Program)
|
||||
.Where(entity => entity.Layer != SpecialLayers.Rapid);
|
||||
.Where(entity => SpecialLayers.IsMaterial(entity.Layer));
|
||||
var shapes = ShapeBuilder.GetShapes(geometry);
|
||||
|
||||
if (shapes.Count == 0)
|
||||
|
||||
@@ -239,11 +239,11 @@ namespace OpenNest
|
||||
|
||||
var entities1 = ConvertProgram
|
||||
.ToGeometry(Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
var entities2 = ConvertProgram
|
||||
.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
|
||||
if (entities1.Count == 0 || entities2.Count == 0)
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace OpenNest
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var shapes = ShapeBuilder.GetShapes(
|
||||
entities.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
);
|
||||
var lines = new List<Line>();
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace OpenNest
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var shapes = ShapeBuilder.GetShapes(
|
||||
entities.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
);
|
||||
var lines = new List<Line>();
|
||||
|
||||
@@ -56,7 +56,7 @@ namespace OpenNest
|
||||
PerfCounters.CountOffsetPerimeterEntities();
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
);
|
||||
|
||||
var offsetShape = profile.Perimeter.OffsetOutward(spacing);
|
||||
@@ -79,7 +79,7 @@ namespace OpenNest
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
);
|
||||
var entities = new List<Entity>();
|
||||
|
||||
@@ -112,7 +112,7 @@ namespace OpenNest
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
);
|
||||
|
||||
return CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||
@@ -126,7 +126,7 @@ namespace OpenNest
|
||||
{
|
||||
var geoEntities = ConvertProgram.ToGeometry(part.Program);
|
||||
var profile = new ShapeProfile(
|
||||
geoEntities.Where(e => e.Layer != SpecialLayers.Rapid).ToList()
|
||||
geoEntities.Where(e => SpecialLayers.IsMaterial(e.Layer)).ToList()
|
||||
);
|
||||
var entities = CopyEntitiesAtLocation(profile.Perimeter.Entities, part.Location);
|
||||
|
||||
@@ -158,7 +158,7 @@ namespace OpenNest
|
||||
{
|
||||
var entities = ConvertProgram.ToGeometry(part.Program);
|
||||
var shapes = ShapeBuilder.GetShapes(
|
||||
entities.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
entities.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
);
|
||||
var lines = new List<Line>();
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ namespace OpenNest
|
||||
{
|
||||
var entities = Converters
|
||||
.ConvertProgram.ToGeometry(part.Program)
|
||||
.Where(e => e.Layer != SpecialLayers.Rapid)
|
||||
.Where(e => SpecialLayers.IsMaterial(e.Layer))
|
||||
.ToList();
|
||||
|
||||
if (entities.Count > 0)
|
||||
|
||||
@@ -18,5 +18,12 @@ namespace OpenNest
|
||||
public static readonly Layer Leadout = new Layer("LEADOUT") { Color = Color.Brown };
|
||||
|
||||
public static readonly Layer Scribe = new Layer("SCRIBE") { Color = Color.Magenta };
|
||||
|
||||
/// <summary>
|
||||
/// True when an entity converted from a part program describes part material. Rapids
|
||||
/// and scribe/etch marks are excluded: marks are only on the surface, so they never
|
||||
/// bound material and must not affect nesting, collision, area, or validation.
|
||||
/// </summary>
|
||||
public static bool IsMaterial(Layer layer) => layer != Rapid && layer != Scribe;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user