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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user