Files
OpenNest/OpenNest.Core/Converters/ConvertGeometry.cs
T
ajandClaude Opus 5.5 6f555345e3 feat(cad): cut breakoff joining lines as tabbed open break cuts
Breakoffs are now picked by their joining line, default to the smaller side as scrap
(flippable), and cut that line between tabs on a new Break layer instead of slots.
Plan Cutting and Assign Lead-ins enter each cut from the scrap with the nest's internal
lead-in, or pierce on the line without one; checks treat break cuts as intentional open
cuts that are not material.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:46:35 -04:00

187 lines
6.0 KiB
C#

using System.Collections.Generic;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Converters
{
public static class ConvertGeometry
{
public static Program ToProgram(IList<Entity> geometry)
{
// Break cuts keep their order and direction (their scrap side): they are emitted as
// given, one program run per continuous chain, before the contours.
var breakCuts = new List<Shape>();
var contours = new List<Entity>();
foreach (var entity in geometry)
{
if (entity.Layer != SpecialLayers.Break)
{
contours.Add(entity);
continue;
}
var last = breakCuts.Count == 0 ? null : breakCuts[^1].Entities[^1];
if (last == null || EndOf(last).DistanceTo(StartOf(entity)) > Tolerance.Epsilon)
breakCuts.Add(new Shape());
breakCuts[^1].Entities.Add(entity);
}
var shapes = ShapeBuilder.GetShapes(contours);
if (shapes.Count == 0)
return null;
var perimeter = shapes[0];
var area = perimeter.BoundingBox.Area();
var index = 0;
for (int i = 1; i < shapes.Count; ++i)
{
var program = shapes[i];
var area2 = program.BoundingBox.Area();
if (area2 > area)
{
perimeter = program;
area = area2;
index = i;
}
}
shapes.RemoveAt(index);
var pgm = new Program();
foreach (var cut in breakCuts)
pgm.Merge(ToProgram(cut));
foreach (var shape in shapes)
{
var subpgm = ToProgram(shape);
pgm.Merge(subpgm);
}
pgm.Merge(ToProgram(perimeter));
pgm.Mode = Mode.Incremental;
return pgm;
}
private static Vector StartOf(Entity entity) => entity is Arc arc ? arc.StartPoint() : ((Line)entity).StartPoint;
private static Vector EndOf(Entity entity) => entity is Arc arc ? arc.EndPoint() : ((Line)entity).EndPoint;
public static Program ToProgram(Shape shape)
{
var pgm = new Program();
var lastpt = new Vector();
for (int i = 0; i < shape.Entities.Count; i++)
lastpt = AddEntity(pgm, lastpt, shape.Entities[i]);
return pgm;
}
private static Vector AddEntity(Program pgm, Vector lastpt, Entity geo)
{
switch (geo.Type)
{
case EntityType.Arc:
lastpt = AddArc(pgm, lastpt, (Arc)geo);
break;
case EntityType.Circle:
lastpt = AddCircle(pgm, lastpt, (Circle)geo);
break;
case EntityType.Line:
lastpt = AddLine(pgm, lastpt, (Line)geo);
break;
}
return lastpt;
}
private static Vector AddArc(Program pgm, Vector lastpt, Arc arc)
{
var startpt = arc.StartPoint();
var endpt = arc.EndPoint();
if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance)
pgm.MoveTo(startpt);
lastpt = endpt;
var layer = ClassifyLayer(arc);
var sweep = System.Math.Abs(arc.SweepAngle());
if (sweep < Tolerance.Epsilon || sweep.IsEqualTo(Angle.TwoPI))
{
pgm.Codes.Add(new LinearMove(endpt) { Layer = layer });
}
else
{
pgm.Codes.Add(
new ArcMove(
endpt,
arc.Center,
arc.IsReversed ? RotationType.CW : RotationType.CCW
)
{
Layer = layer,
}
);
}
return lastpt;
}
private static Vector AddCircle(Program pgm, Vector lastpt, Circle circle)
{
var startpt = new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
if (startpt.DistanceTo(lastpt) > Tolerance.ChainTolerance)
pgm.MoveTo(startpt);
pgm.Codes.Add(
new ArcMove(startpt, circle.Center, circle.Rotation)
{
Layer = ClassifyLayer(circle),
}
);
lastpt = startpt;
return lastpt;
}
private static Vector AddLine(Program pgm, Vector lastpt, Line line)
{
if (line.StartPoint.DistanceTo(lastpt) > Tolerance.ChainTolerance)
pgm.MoveTo(line.StartPoint);
pgm.Codes.Add(new LinearMove(line.EndPoint) { Layer = ClassifyLayer(line) });
lastpt = line.EndPoint;
return lastpt;
}
// 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;
if (string.Equals(name, SpecialLayers.Break.Name, System.StringComparison.OrdinalIgnoreCase))
return LayerType.Break;
return LayerType.Cut;
}
}
}