mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-06 11:15:44 -05:00
N26-295's PT42 pairs saved spacing in slice 1 but every shared edge was still cut twice, costing a pass per edge and risking a freed part tipping under the head on the second pass. Plan Cutting now cuts a plate's common lines once unless the plate is set to cut them twice. Each group is cut member by member, owner before partner (a captured dependency that search and replay enforce, so a kept order cutting the partner first is a constraint conflict): the member's holes, then the shared cuts it owns as open CommonLine centerline cuts entered through the notch on the span's axis, then its remainder pieces, each ramped in from the kerf the shared cut opened. Capture marks the shared spans as tabs on the owned clean copy, so replay's contour accounting proves each perimeter is cut exactly once; a lead that fits at neither end refuses as UnsupportedGeometry naming the span, never shortened. Material still comes from the drawing, so utilization, quantity and overlap results do not change. Members are never support-tabbed; locked or excluded members and broken groups refuse. The CI Fiber post writes shared cuts in the cut-off form with a lead-in and no G41/G42, and remainders in the contour form on their perimeter's compensation side, named by a remainder comment. Cincinnati CL (whose PreApplied mode offsets nothing) and Gravograph refuse such nests, as do the desktop and console for any post without ICommonLinePostProcessor; nothing falls back to a silent double cut. Plate > Arrange > Cut Common Lines Twice keeps slice 1's behavior, persists with the plate and makes an open plan stale. Pre-post review now checks each span is cut once, on its centerline, by the member cut first, and the rapid check treats a finished member's remainders as an obstacle. Compensation start in void and cancel in kerf are unconfirmed on the CI FIBER 4020; release waits on a 3/16 stainless coupon trial. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
196 lines
6.5 KiB
C#
196 lines
6.5 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;
|
|
|
|
if (string.Equals(name, SpecialLayers.DisplayPerimeter.Name, System.StringComparison.OrdinalIgnoreCase))
|
|
return LayerType.DisplayPerimeter;
|
|
|
|
if (string.Equals(name, SpecialLayers.CommonLine.Name, System.StringComparison.OrdinalIgnoreCase))
|
|
return LayerType.CommonLine;
|
|
|
|
return LayerType.Cut;
|
|
}
|
|
}
|
|
}
|