feat: add etch mark entities from bend lines to CNC program pipeline

Etch marks for up bends are now real geometry entities on an ETCH layer
instead of being drawn dynamically. They flow through the full pipeline:
entities → FilterPanel layers → ConvertGeometry (tagged as Scribe) →
post-processor sequencing before cut geometry.

Also includes ShapeProfile normalization (CW perimeter, CCW cutouts)
applied consistently across all import paths, and inward offset support
for cutout shapes in overlap/offset polygon calculations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-28 00:42:49 -04:00
parent 80e8693da3
commit 2db8c49838
15 changed files with 306 additions and 147 deletions
-42
View File
@@ -112,8 +112,6 @@ namespace OpenNest.Controls
DrawEntity(e.Graphics, entity, pen);
}
DrawEtchMarks(e.Graphics);
if (SimplifierPreview != null)
{
// Draw tolerance zone (offset lines each side of original geometry)
@@ -240,46 +238,6 @@ namespace OpenNest.Controls
private static bool IsEtchLayer(Layer layer) =>
string.Equals(layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase);
private void DrawEtchMarks(Graphics g)
{
if (Bends == null || Bends.Count == 0)
return;
using var etchPen = new Pen(Color.Green, 1.5f);
var etchLength = 1.0;
foreach (var bend in Bends)
{
if (bend.Direction != BendDirection.Up)
continue;
var start = bend.StartPoint;
var end = bend.EndPoint;
var length = bend.Length;
if (length < etchLength * 3.0)
{
var pt1 = PointWorldToGraph(start);
var pt2 = PointWorldToGraph(end);
g.DrawLine(etchPen, pt1, pt2);
}
else
{
var angle = start.AngleTo(end);
var dx = System.Math.Cos(angle) * etchLength;
var dy = System.Math.Sin(angle) * etchLength;
var s1 = PointWorldToGraph(start);
var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
g.DrawLine(etchPen, s1, e1);
var s2 = PointWorldToGraph(end);
var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
g.DrawLine(etchPen, s2, e2);
}
}
}
private void DrawBendLines(Graphics g)
{
if (Bends == null || Bends.Count == 0)
+53
View File
@@ -584,6 +584,7 @@ namespace OpenNest.Controls
part.Draw(g, (i + 1).ToString());
DrawBendLines(g, part.BasePart);
DrawEtchMarks(g, part.BasePart);
DrawGrainWarning(g, part.BasePart);
}
@@ -657,6 +658,58 @@ namespace OpenNest.Controls
}
}
private void DrawEtchMarks(Graphics g, Part part)
{
if (!ShowBendLines || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
return;
using var etchPen = new Pen(Color.Green, 1.5f);
var etchLength = 1.0;
foreach (var bend in part.BaseDrawing.Bends)
{
if (bend.Direction != BendDirection.Up)
continue;
var start = bend.StartPoint;
var end = bend.EndPoint;
// Apply part rotation
if (part.Rotation != 0)
{
start = start.Rotate(part.Rotation);
end = end.Rotate(part.Rotation);
}
// Apply part offset
start = start + part.Location;
end = end + part.Location;
var length = bend.Length;
var angle = bend.StartPoint.AngleTo(bend.EndPoint) + part.Rotation;
if (length < etchLength * 3.0)
{
var pt1 = PointWorldToGraph(start);
var pt2 = PointWorldToGraph(end);
g.DrawLine(etchPen, pt1, pt2);
}
else
{
var dx = System.Math.Cos(angle) * etchLength;
var dy = System.Math.Sin(angle) * etchLength;
var s1 = PointWorldToGraph(start);
var e1 = PointWorldToGraph(new Vector(start.X + dx, start.Y + dy));
g.DrawLine(etchPen, s1, e1);
var s2 = PointWorldToGraph(end);
var e2 = PointWorldToGraph(new Vector(end.X - dx, end.Y - dy));
g.DrawLine(etchPen, s2, e2);
}
}
}
private void DrawGrainWarning(Graphics g, Part part)
{
if (!ShowBendLines || Plate == null || part.BaseDrawing.Bends == null || part.BaseDrawing.Bends.Count == 0)
+2 -1
View File
@@ -424,7 +424,8 @@ namespace OpenNest.Forms
drawing.Quantity.Required = part.Qty ?? 1;
drawing.Material = new Material(material);
var pgm = ConvertGeometry.ToProgram(result.Entities);
var normalized = ShapeProfile.NormalizeEntities(result.Entities);
var pgm = ConvertGeometry.ToProgram(normalized);
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
{
+12 -31
View File
@@ -81,6 +81,8 @@ namespace OpenNest.Forms
?? new List<Bend>();
}
Bend.UpdateEtchEntities(result.Entities, bends);
var item = new FileListItem
{
Name = Path.GetFileNameWithoutExtension(file),
@@ -245,6 +247,9 @@ namespace OpenNest.Forms
bend.SourceEntity.IsVisible = true;
item.Bends.RemoveAt(index);
Bend.UpdateEtchEntities(item.Entities, item.Bends);
entityView1.Entities.Clear();
entityView1.Entities.AddRange(item.Entities);
entityView1.Bends = item.Bends;
entityView1.SelectedBendIndex = -1;
filterPanel.LoadItem(item.Entities, item.Bends);
@@ -281,16 +286,8 @@ namespace OpenNest.Forms
var entities = item.Entities.Where(en => en.Layer.IsVisible && en.IsVisible).ToList();
if (entities.Count == 0) return;
var shape = new ShapeProfile(entities);
SetRotation(shape.Perimeter, RotationType.CW);
foreach (var cutout in shape.Cutouts)
SetRotation(cutout, RotationType.CCW);
var drawEntities = new List<Entity>();
drawEntities.AddRange(shape.Perimeter.Entities);
shape.Cutouts.ForEach(c => drawEntities.AddRange(c.Entities));
var pgm = ConvertGeometry.ToProgram(drawEntities);
var normalized = ShapeProfile.NormalizeEntities(entities);
var pgm = ConvertGeometry.ToProgram(normalized);
var originOffset = Vector.Zero;
if (pgm.Codes.Count > 0 && pgm[0].Type == CodeType.RapidMove)
{
@@ -395,6 +392,9 @@ namespace OpenNest.Forms
line.IsVisible = false;
item.Bends.Add(bend);
Bend.UpdateEtchEntities(item.Entities, item.Bends);
entityView1.Entities.Clear();
entityView1.Entities.AddRange(item.Entities);
entityView1.Bends = item.Bends;
filterPanel.LoadItem(item.Entities, item.Bends);
entityView1.Invalidate();
@@ -560,18 +560,8 @@ namespace OpenNest.Forms
if (item.Bends != null)
drawing.Bends.AddRange(item.Bends);
var shape = new ShapeProfile(entities);
SetRotation(shape.Perimeter, RotationType.CW);
foreach (var cutout in shape.Cutouts)
SetRotation(cutout, RotationType.CCW);
entities = new List<Entity>();
entities.AddRange(shape.Perimeter.Entities);
shape.Cutouts.ForEach(cutout => entities.AddRange(cutout.Entities));
var pgm = ConvertGeometry.ToProgram(entities);
var normalized = ShapeProfile.NormalizeEntities(entities);
var pgm = ConvertGeometry.ToProgram(normalized);
var firstCode = pgm[0];
if (firstCode.Type == CodeType.RapidMove)
@@ -605,15 +595,6 @@ namespace OpenNest.Forms
}
}
private static void SetRotation(Shape shape, RotationType rotation)
{
try
{
var dir = shape.ToPolygon(3).RotationDirection();
if (dir != rotation) shape.Reverse();
}
catch { }
}
private static Color GetNextColor()
{
+24 -20
View File
@@ -178,32 +178,36 @@ namespace OpenNest
{
var result = new List<PointF[]>();
var entities = ConvertProgram.ToGeometry(BasePart.Program);
var shapes = ShapeBuilder.GetShapes(entities.Where(e => e.Layer != SpecialLayers.Rapid));
var profile = new ShapeProfile(
entities.Where(e => e.Layer != SpecialLayers.Rapid).ToList());
foreach (var shape in shapes)
{
var offsetEntity = shape.OffsetOutward(spacing);
AddOffsetPolygon(result, profile.Perimeter.OffsetOutward(spacing), tolerance);
if (offsetEntity == null)
continue;
var polygon = offsetEntity.ToPolygonWithTolerance(tolerance);
polygon.RemoveSelfIntersections();
if (polygon.Vertices.Count < 2)
continue;
var pts = new PointF[polygon.Vertices.Count];
for (var j = 0; j < pts.Length; j++)
pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y);
result.Add(pts);
}
foreach (var cutout in profile.Cutouts)
AddOffsetPolygon(result, cutout.OffsetInward(spacing), tolerance);
return result;
}
private static void AddOffsetPolygon(List<PointF[]> result, Shape offsetEntity, double tolerance)
{
if (offsetEntity == null)
return;
var polygon = offsetEntity.ToPolygonWithTolerance(tolerance);
polygon.RemoveSelfIntersections();
if (polygon.Vertices.Count < 2)
return;
var pts = new PointF[polygon.Vertices.Count];
for (var j = 0; j < pts.Length; j++)
pts[j] = new PointF((float)polygon.Vertices[j].X, (float)polygon.Vertices[j].Y);
result.Add(pts);
}
private void RebuildOffsetPath(Matrix matrix)
{
OffsetPath?.Dispose();