Files
OpenNest/OpenNest.Core/Converters/ConvertGeometry.cs
T
ajandClaude Opus 5.5 ba39c3dc55 feat(cad): add tabbed cutouts with draggable tabs
A hole can now be declared a tabbed cutout in the CAD Converter: pick a
circle or any edge of a closed hole, set the tab count and width, and drag
the tabs around the hole. The definition persists on the drawing and in the
nest file, and stays editable from the source entities.

The builder replaces the hole with open cut pieces and uncut spans on a new
Tab layer. Tabs are material, so area, nesting and overlap checks still see
a closed hole, but they are never cut. Plan Cutting and Assign Lead-ins cut
each piece from the slug with the internal lead-in and no lead-out. A lead
that would reach back over a tab is mirrored, and an arc lead onto an arc
pierces straight in. Planning accounts for each span between tabs and runs
the open-cut proof, so its own tabbed cutouts certify; tabs followed by a
lead-out are still refused. Pre-post review flags programs that still
contain tab motions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 00:50:11 -04:00

190 lines
6.2 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;
if (string.Equals(name, SpecialLayers.Tab.Name, System.StringComparison.OrdinalIgnoreCase))
return LayerType.Tab;
return LayerType.Cut;
}
}
}