mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-14 06:55:07 -05:00
Geometry, nesting and import code now call PortableMath instead of System.Math for Sin, Cos, Tan, Atan, Atan2, Asin and Acos (Core, Engine, IO, Data; 269 calls in 64 files). A test fails if those projects call the runtime's trig again. With the runtime's trig, Windows and Linux disagreed in the last bit at non-quarter angles and the same job could nest differently: the learned-angle nests (a cross fixture) placed 23 parts in Strip on Windows and 28 on Linux. Now every Engine.Tests pin gives the same layout on both (3600 passing on Linux and on DesktopPC), Linux layouts are unchanged, and the learned-angle rows are re-pinned to the shared layouts, which pass the overlap and work-area checks, and run on every platform again. Fixes Project Memory 518f3b58.
442 lines
15 KiB
C#
442 lines
15 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using ACadSharp;
|
|
using ACadSharp.IO;
|
|
using CSMath;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
using AcadArc = ACadSharp.Entities.Arc;
|
|
using AcadCircle = ACadSharp.Entities.Circle;
|
|
using AcadLine = ACadSharp.Entities.Line;
|
|
using Layer = ACadSharp.Tables.Layer;
|
|
|
|
namespace OpenNest.IO
|
|
{
|
|
public static class Dxf
|
|
{
|
|
#region Import
|
|
|
|
/// <summary>
|
|
/// Imports a DXF file, returning both converted entities and the raw CadDocument
|
|
/// for bend detection. The CadDocument is NOT disposed — caller can use it for
|
|
/// additional analysis (e.g., MText extraction for bend notes).
|
|
/// </summary>
|
|
public static DxfImportResult Import(string path, bool preserveRepairMarks = false)
|
|
{
|
|
var doc = ReadDocument(path);
|
|
// Isolate marks before optimization, including cross-layer circle/arc deduplication.
|
|
var entities = preserveRepairMarks
|
|
? ConvertEntities(doc, name => IsNonCutLayer(name) || IsRepairMarkLayer(name))
|
|
: ConvertEntities(doc);
|
|
if (preserveRepairMarks)
|
|
{
|
|
// Keep source marks separate: optimization could merge two ticks or unrelated scribing.
|
|
entities.AddRange(
|
|
ConvertEntities(doc, name => !IsRepairMarkLayer(name), optimize: false)
|
|
);
|
|
}
|
|
|
|
return new DxfImportResult
|
|
{
|
|
Entities = entities,
|
|
RawEntities = ConvertEntities(doc, _ => false, optimize: false),
|
|
Document = doc,
|
|
};
|
|
}
|
|
|
|
public static List<Entity> GetGeometry(string path) =>
|
|
ReadGeometry(() => ReadDocument(path), null);
|
|
|
|
public static List<Entity> GetGeometry(Stream stream) =>
|
|
GetGeometry(stream, null);
|
|
|
|
public static List<Entity> GetGeometry(string path, Func<string, bool> layerFilter) =>
|
|
ReadGeometry(
|
|
() =>
|
|
{
|
|
using var stream = SharedFile.OpenRead(path);
|
|
return ReadDxf(new DxfReader(stream));
|
|
},
|
|
layerFilter
|
|
);
|
|
|
|
public static List<Entity> GetGeometry(Stream stream, Func<string, bool> layerFilter) =>
|
|
ReadGeometry(() => ReadDxf(new DxfReader(stream)), layerFilter);
|
|
|
|
#endregion
|
|
|
|
#region Export
|
|
|
|
public static void ExportProgram(Program program, string path)
|
|
{
|
|
using var stream = File.Create(path);
|
|
ExportProgram(program, stream);
|
|
}
|
|
|
|
public static void ExportProgram(Program program, Stream stream)
|
|
{
|
|
var ctx = new ExportContext();
|
|
ctx.AddProgram(program);
|
|
|
|
using var writer = new DxfWriter(stream, ctx.Document, false);
|
|
writer.Write();
|
|
}
|
|
|
|
public static void ExportPlate(Plate plate, string path)
|
|
{
|
|
using var stream = File.Create(path);
|
|
ExportPlate(plate, stream);
|
|
}
|
|
|
|
public static void ExportPlate(Plate plate, Stream stream)
|
|
{
|
|
var ctx = new ExportContext();
|
|
ctx.AddPlateOutline(plate);
|
|
|
|
foreach (var part in plate.Parts)
|
|
{
|
|
var endpt = part.Location.ToAcadXYZ();
|
|
ctx.AddLine(ctx.CurPos, endpt, ctx.RapidLayer);
|
|
ctx.CurPos = part.Location.ToAcadXYZ();
|
|
ctx.AddProgram(part.Program);
|
|
}
|
|
|
|
using var writer = new DxfWriter(stream, ctx.Document, false);
|
|
writer.Write();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Private
|
|
|
|
private static bool IsDwg(string path) =>
|
|
Path.GetExtension(path).Equals(".dwg", StringComparison.OrdinalIgnoreCase);
|
|
|
|
// An unreadable drawing yields no geometry rather than an exception.
|
|
private static List<Entity> ReadGeometry(Func<CadDocument> read, Func<string, bool> layerFilter)
|
|
{
|
|
try
|
|
{
|
|
return ConvertEntities(read(), layerFilter);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.WriteLine(ex.Message);
|
|
return new List<Entity>();
|
|
}
|
|
}
|
|
|
|
private static CadDocument ReadDxf(DxfReader reader)
|
|
{
|
|
using (reader)
|
|
return reader.Read();
|
|
}
|
|
|
|
// Read through a shared stream so a drawing still open in AutoCAD can be imported.
|
|
private static CadDocument ReadDocument(string path)
|
|
{
|
|
using var stream = SharedFile.OpenRead(path);
|
|
if (IsDwg(path))
|
|
{
|
|
using var reader = new DwgReader(stream);
|
|
return reader.Read();
|
|
}
|
|
else
|
|
{
|
|
using var reader = new DxfReader(stream);
|
|
return reader.Read();
|
|
}
|
|
}
|
|
|
|
private static List<Entity> ConvertEntities(
|
|
CadDocument doc,
|
|
Func<string, bool> layerFilter = null,
|
|
bool optimize = true
|
|
)
|
|
{
|
|
var entities = new List<Entity>();
|
|
var lines = new List<Line>();
|
|
var arcs = new List<Arc>();
|
|
var circles = new List<Circle>();
|
|
var filter = layerFilter ?? IsNonCutLayer;
|
|
|
|
foreach (var entity in doc.Entities)
|
|
{
|
|
if (filter(entity.Layer?.Name))
|
|
continue;
|
|
|
|
switch (entity)
|
|
{
|
|
case ACadSharp.Entities.Line line:
|
|
lines.Add(line.ToOpenNest());
|
|
break;
|
|
|
|
case ACadSharp.Entities.Arc arc:
|
|
arcs.Add(arc.ToOpenNest());
|
|
break;
|
|
|
|
case ACadSharp.Entities.Circle circle:
|
|
circles.Add(circle.ToOpenNest());
|
|
break;
|
|
|
|
case ACadSharp.Entities.Spline spline:
|
|
foreach (var e in spline.ToOpenNest())
|
|
{
|
|
if (e is Line l)
|
|
lines.Add(l);
|
|
else if (e is Arc a)
|
|
arcs.Add(a);
|
|
}
|
|
break;
|
|
|
|
case ACadSharp.Entities.LwPolyline lwPolyline:
|
|
lines.AddRange(lwPolyline.ToOpenNest());
|
|
break;
|
|
|
|
case ACadSharp.Entities.Polyline polyline:
|
|
lines.AddRange(polyline.ToOpenNest());
|
|
break;
|
|
|
|
case ACadSharp.Entities.Ellipse ellipse:
|
|
foreach (var e in ellipse.ToOpenNest())
|
|
{
|
|
if (e is Line l)
|
|
lines.Add(l);
|
|
else if (e is Arc a)
|
|
arcs.Add(a);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (optimize)
|
|
{
|
|
GeometryOptimizer.Optimize(lines);
|
|
GeometryOptimizer.Optimize(arcs);
|
|
GeometryOptimizer.Deduplicate(circles);
|
|
GeometryOptimizer.Deduplicate(circles, arcs);
|
|
}
|
|
|
|
entities.AddRange(circles);
|
|
entities.AddRange(lines);
|
|
entities.AddRange(arcs);
|
|
|
|
return entities;
|
|
}
|
|
|
|
private static bool IsRepairMarkLayer(string name) =>
|
|
string.Equals(name, "ETCH", StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(name, "SCRIBE", StringComparison.OrdinalIgnoreCase);
|
|
|
|
internal static bool IsNonCutLayer(string layerName)
|
|
{
|
|
// Etch/scribe marks are never cut geometry — drop them on default import.
|
|
return string.Equals(layerName, "BEND", StringComparison.OrdinalIgnoreCase)
|
|
|| IsRepairMarkLayer(layerName);
|
|
}
|
|
|
|
private class ExportContext
|
|
{
|
|
public CadDocument Document { get; }
|
|
public XYZ CurPos { get; set; }
|
|
public Layer CutLayer { get; }
|
|
public Layer RapidLayer { get; }
|
|
public Layer PlateLayer { get; }
|
|
|
|
private Mode mode;
|
|
|
|
public ExportContext()
|
|
{
|
|
Document = new CadDocument();
|
|
Document.Header.Version = ACadVersion.AC1018;
|
|
|
|
CutLayer = new Layer("Cut") { Color = new Color(1) };
|
|
RapidLayer = new Layer("Rapid") { Color = new Color(5) };
|
|
PlateLayer = new Layer("Plate") { Color = new Color(4) };
|
|
|
|
Document.Layers.Add(CutLayer);
|
|
Document.Layers.Add(RapidLayer);
|
|
Document.Layers.Add(PlateLayer);
|
|
}
|
|
|
|
public void AddLine(XYZ start, XYZ end, Layer layer)
|
|
{
|
|
var ln = new AcadLine
|
|
{
|
|
StartPoint = start,
|
|
EndPoint = end,
|
|
Layer = layer,
|
|
};
|
|
Document.Entities.Add(ln);
|
|
}
|
|
|
|
public void AddPlateOutline(Plate plate)
|
|
{
|
|
XYZ pt1,
|
|
pt2,
|
|
pt3,
|
|
pt4;
|
|
|
|
switch (plate.Quadrant)
|
|
{
|
|
case 1:
|
|
pt1 = new XYZ(0, 0, 0);
|
|
pt2 = new XYZ(0, plate.Size.Width, 0);
|
|
pt3 = new XYZ(plate.Size.Length, plate.Size.Width, 0);
|
|
pt4 = new XYZ(plate.Size.Length, 0, 0);
|
|
break;
|
|
|
|
case 2:
|
|
pt1 = new XYZ(0, 0, 0);
|
|
pt2 = new XYZ(0, plate.Size.Width, 0);
|
|
pt3 = new XYZ(-plate.Size.Length, plate.Size.Width, 0);
|
|
pt4 = new XYZ(-plate.Size.Length, 0, 0);
|
|
break;
|
|
|
|
case 3:
|
|
pt1 = new XYZ(0, 0, 0);
|
|
pt2 = new XYZ(0, -plate.Size.Width, 0);
|
|
pt3 = new XYZ(-plate.Size.Length, -plate.Size.Width, 0);
|
|
pt4 = new XYZ(-plate.Size.Length, 0, 0);
|
|
break;
|
|
|
|
case 4:
|
|
pt1 = new XYZ(0, 0, 0);
|
|
pt2 = new XYZ(0, -plate.Size.Width, 0);
|
|
pt3 = new XYZ(plate.Size.Length, -plate.Size.Width, 0);
|
|
pt4 = new XYZ(plate.Size.Length, 0, 0);
|
|
break;
|
|
|
|
default:
|
|
return;
|
|
}
|
|
|
|
AddLine(pt1, pt2, PlateLayer);
|
|
AddLine(pt2, pt3, PlateLayer);
|
|
AddLine(pt3, pt4, PlateLayer);
|
|
AddLine(pt4, pt1, PlateLayer);
|
|
|
|
var m1 = new XYZ(
|
|
pt1.X + plate.EdgeSpacing.Left,
|
|
pt1.Y + plate.EdgeSpacing.Bottom,
|
|
0
|
|
);
|
|
var m2 = new XYZ(m1.X, pt2.Y - plate.EdgeSpacing.Top, 0);
|
|
var m3 = new XYZ(pt3.X - plate.EdgeSpacing.Right, m2.Y, 0);
|
|
var m4 = new XYZ(m3.X, m1.Y, 0);
|
|
|
|
AddLine(m1, m2, PlateLayer);
|
|
AddLine(m2, m3, PlateLayer);
|
|
AddLine(m3, m4, PlateLayer);
|
|
AddLine(m4, m1, PlateLayer);
|
|
}
|
|
|
|
public void AddProgram(Program program)
|
|
{
|
|
mode = program.Mode;
|
|
|
|
for (var i = 0; i < program.Length; ++i)
|
|
{
|
|
var code = program[i];
|
|
|
|
switch (code.Type)
|
|
{
|
|
case CodeType.ArcMove:
|
|
AddArcMove((ArcMove)code);
|
|
break;
|
|
|
|
case CodeType.LinearMove:
|
|
AddLinearMove((LinearMove)code);
|
|
break;
|
|
|
|
case CodeType.RapidMove:
|
|
AddRapidMove((RapidMove)code);
|
|
break;
|
|
|
|
case CodeType.SubProgramCall:
|
|
var tmpmode = mode;
|
|
AddProgram(((SubProgramCall)code).Program);
|
|
mode = tmpmode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AddLinearMove(LinearMove line)
|
|
{
|
|
var pt = line.EndPoint.ToAcadXYZ();
|
|
|
|
if (mode == Mode.Incremental)
|
|
pt = new XYZ(pt.X + CurPos.X, pt.Y + CurPos.Y, 0);
|
|
|
|
AddLine(CurPos, pt, CutLayer);
|
|
CurPos = pt;
|
|
}
|
|
|
|
private void AddRapidMove(RapidMove rapid)
|
|
{
|
|
var pt = rapid.EndPoint.ToAcadXYZ();
|
|
|
|
if (mode == Mode.Incremental)
|
|
pt = new XYZ(pt.X + CurPos.X, pt.Y + CurPos.Y, 0);
|
|
|
|
AddLine(CurPos, pt, RapidLayer);
|
|
CurPos = pt;
|
|
}
|
|
|
|
private void AddArcMove(ArcMove arc)
|
|
{
|
|
var center = arc.CenterPoint.ToAcadXYZ();
|
|
var endpt = arc.EndPoint.ToAcadXYZ();
|
|
|
|
if (mode == Mode.Incremental)
|
|
{
|
|
endpt = new XYZ(endpt.X + CurPos.X, endpt.Y + CurPos.Y, 0);
|
|
center = new XYZ(center.X + CurPos.X, center.Y + CurPos.Y, 0);
|
|
}
|
|
|
|
var startAngle = PortableMath.Atan2(CurPos.Y - center.Y, CurPos.X - center.X);
|
|
|
|
var endAngle = PortableMath.Atan2(endpt.Y - center.Y, endpt.X - center.X);
|
|
|
|
if (arc.Rotation == RotationType.CW)
|
|
Generic.Swap(ref startAngle, ref endAngle);
|
|
|
|
var dx = endpt.X - center.X;
|
|
var dy = endpt.Y - center.Y;
|
|
var radius = System.Math.Sqrt(dx * dx + dy * dy);
|
|
|
|
if (startAngle.IsEqualTo(endAngle))
|
|
{
|
|
var circle = new AcadCircle
|
|
{
|
|
Center = center,
|
|
Radius = radius,
|
|
Layer = CutLayer,
|
|
};
|
|
Document.Entities.Add(circle);
|
|
}
|
|
else
|
|
{
|
|
var acadArc = new AcadArc
|
|
{
|
|
Center = center,
|
|
Radius = radius,
|
|
StartAngle = startAngle,
|
|
EndAngle = endAngle,
|
|
Layer = CutLayer,
|
|
};
|
|
Document.Entities.Add(acadArc);
|
|
}
|
|
|
|
CurPos = endpt;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
}
|
|
}
|