mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 16:01:36 -05:00
Select entities in the CAD converter (click, or drag a window/crossing box) and right-click to exclude/include them or set them to cut, etch or display only, regardless of their DXF layer. Delete excludes the selection. Roles are stored per source entity on the drawing and persisted in the entity set as an optional `roles` map. Display-only geometry is kept out of the program, so posts, nesting and verification never see it; placed parts draw it as a dashed overlay. Load-time scribe repair skips etch-layer entities set to cut. Per-entity exclusions now survive filter changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
313 lines
13 KiB
C#
313 lines
13 KiB
C#
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using OpenNest.Bending;
|
|
using OpenNest.Breakoffs;
|
|
using OpenNest.Converters;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.IO.Bending;
|
|
using OpenNest.Math;
|
|
using OpenNest.TabbedCutouts;
|
|
|
|
namespace OpenNest.IO
|
|
{
|
|
/// <summary>
|
|
/// Shared service that converts a CAD source file into a fully-populated
|
|
/// <see cref="Drawing"/>. Used by the UI, console, MCP, API, and training
|
|
/// tools so all code paths produce identical drawings.
|
|
/// </summary>
|
|
public static class CadImporter
|
|
{
|
|
/// <summary>
|
|
/// Load a DXF file, run bend detection, and return a mutable result
|
|
/// ready for interactive editing or direct conversion to a Drawing.
|
|
/// </summary>
|
|
public static CadImportResult Import(string path, CadImportOptions options = null)
|
|
{
|
|
options ??= CadImportOptions.Default;
|
|
|
|
var dxf = Dxf.Import(path, preserveRepairMarks: options.BendRepair != null);
|
|
|
|
var cleanup =
|
|
options.BendRepair == null
|
|
? dxf.Entities
|
|
: dxf.Entities.Where(e => !IsRepairMark(e)).ToList();
|
|
RemoveDuplicateArcs(cleanup);
|
|
RemoveZeroSweepArcs(cleanup);
|
|
if (options.BendRepair != null)
|
|
dxf.Entities.RemoveAll(e => !IsRepairMark(e) && !cleanup.Contains(e));
|
|
|
|
var bends = new List<Bend>();
|
|
if (options.DetectBends && dxf.Document != null)
|
|
{
|
|
bends =
|
|
options.BendDetectorName == null
|
|
? BendDetectorRegistry.AutoDetect(dxf.Document)
|
|
: BendDetectorRegistry
|
|
.GetByName(options.BendDetectorName)
|
|
?.DetectBends(dxf.Document)
|
|
?? new List<Bend>();
|
|
}
|
|
|
|
var repairReports = new List<BendRepairReport>();
|
|
if (options.BendRepair == null)
|
|
Bend.UpdateEtchEntities(dxf.Entities, bends);
|
|
else
|
|
{
|
|
// Unitless DXFs require the explicit caller declaration. Never override a conflicting header.
|
|
var headerUnits = (int)(dxf.Document?.Header.InsUnits ?? 0);
|
|
var requestedUnits =
|
|
options.BendRepair.DrawingUnits == BendRepairUnits.Inches ? 1 : 4;
|
|
if (headerUnits != 0 && headerUnits != requestedUnits)
|
|
repairReports = bends
|
|
.Select(
|
|
(b, i) =>
|
|
new BendRepairReport(
|
|
i,
|
|
"Skipped",
|
|
"DXF insertion units conflict with the declared repair units or are unsupported.",
|
|
b.StartPoint,
|
|
b.EndPoint,
|
|
b.StartPoint,
|
|
b.EndPoint
|
|
)
|
|
)
|
|
.ToList();
|
|
else
|
|
repairReports = BendRepair.Apply(dxf.Entities, bends, options.BendRepair);
|
|
}
|
|
|
|
return new CadImportResult
|
|
{
|
|
Entities = dxf.Entities,
|
|
Bends = bends,
|
|
BendRepairReports = repairReports,
|
|
Bounds = dxf.Entities.GetBoundingBox(),
|
|
SourcePath = path,
|
|
Name = options.Name ?? Path.GetFileNameWithoutExtension(path),
|
|
Document = dxf.Document,
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Convenience for headless callers: Import a file and build a Drawing
|
|
/// in a single call, using all loaded entities and detected bends.
|
|
/// </summary>
|
|
public static Drawing ImportDrawing(string path, CadImportOptions options = null)
|
|
{
|
|
options ??= CadImportOptions.Default;
|
|
var result = Import(path, options);
|
|
return BuildDrawing(
|
|
result,
|
|
result.Entities,
|
|
result.Bends,
|
|
options.Quantity,
|
|
options.Customer,
|
|
editedProgram: null
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build a fully-populated <see cref="Drawing"/> from an import result plus
|
|
/// the caller's current entity and bend state. UI callers pass the currently
|
|
/// visible subset; headless callers pass the full lists.
|
|
///
|
|
/// The produced drawing has:
|
|
/// - Program generated from the visible entities, with its first rapid moved
|
|
/// to the origin and the pierce location stored in Source.Offset
|
|
/// - SourceEntities containing all non-bend-source entities from the result
|
|
/// - SuppressedEntityIds containing entities whose layer or IsVisible is false
|
|
/// - Bends copied from the provided list
|
|
/// - Customer, Quantity, Source.Path from options / result
|
|
/// </summary>
|
|
/// <param name="result">Import result from <see cref="Import"/>.</param>
|
|
/// <param name="entities">
|
|
/// Entities to build the program from. Typically the currently visible subset.
|
|
/// </param>
|
|
/// <param name="bends">Bends to attach to the drawing.</param>
|
|
/// <param name="quantity">Required quantity.</param>
|
|
/// <param name="customer">Customer name, or null.</param>
|
|
/// <param name="editedProgram">
|
|
/// When non-null, replaces the generated program (used by the UI to honor
|
|
/// in-place G-code edits). Source.Offset is still populated from the
|
|
/// generated program so round-trips stay consistent.
|
|
/// </param>
|
|
/// <param name="breakoffs">
|
|
/// Breakoffs to merge into the program and keep on the drawing; ones that cannot be
|
|
/// resolved against <paramref name="entities"/> are kept but not applied.
|
|
/// </param>
|
|
/// <param name="tabbedCutouts">
|
|
/// Tabbed cutouts to merge into the program (after breakoffs) and keep on the drawing;
|
|
/// ones that cannot be resolved are kept but not applied.
|
|
/// </param>
|
|
/// <param name="entityRoles">
|
|
/// Per-entity role overrides keyed by source entity ID, applied before breakoffs and kept
|
|
/// on the drawing; display-only entities stay out of the program.
|
|
/// </param>
|
|
public static Drawing BuildDrawing(
|
|
CadImportResult result,
|
|
IEnumerable<Entity> entities,
|
|
IEnumerable<Bend> bends,
|
|
int quantity,
|
|
string customer,
|
|
OpenNest.CNC.Program editedProgram,
|
|
IReadOnlyList<Breakoff> breakoffs = null,
|
|
IReadOnlyList<TabbedCutout> tabbedCutouts = null,
|
|
IReadOnlyDictionary<System.Guid, EntityRole> entityRoles = null
|
|
)
|
|
{
|
|
var visible = entityRoles is { Count: > 0 }
|
|
? EntityRoles.ForProgram(entities, entityRoles)
|
|
: entities as IList<Entity> ?? new List<Entity>(entities);
|
|
var bendList = bends as IList<Bend> ?? new List<Bend>(bends);
|
|
|
|
var cut = breakoffs is { Count: > 0 }
|
|
? BreakoffBuilder.Apply(visible.ToList(), breakoffs, out _)
|
|
: visible;
|
|
if (tabbedCutouts is { Count: > 0 })
|
|
cut = TabbedCutoutBuilder.Apply(cut.ToList(), tabbedCutouts, out _);
|
|
var normalized = ShapeProfile.NormalizeEntities(cut);
|
|
var pgm = ConvertGeometry.ToProgram(normalized);
|
|
|
|
var offset = Vector.Zero;
|
|
if (
|
|
pgm != null
|
|
&& pgm.Codes.Count > 0
|
|
&& pgm[0].Type == OpenNest.CNC.CodeType.RapidMove
|
|
)
|
|
{
|
|
var rapid = (OpenNest.CNC.RapidMove)pgm[0];
|
|
offset = rapid.EndPoint;
|
|
pgm.Offset(-offset);
|
|
}
|
|
|
|
var drawing = new Drawing(result.Name)
|
|
{
|
|
Color = Drawing.GetNextColor(),
|
|
Customer = customer,
|
|
};
|
|
drawing.Source.Path = result.SourcePath;
|
|
drawing.Source.Offset = offset;
|
|
drawing.Quantity.Required = quantity;
|
|
drawing.Bends.AddRange(bendList);
|
|
drawing.Breakoffs = breakoffs?.Select(b => b.Clone()).ToList() ?? new List<Breakoff>();
|
|
drawing.TabbedCutouts = tabbedCutouts?.Select(c => c.Clone()).ToList() ?? new List<TabbedCutout>();
|
|
drawing.Program = editedProgram ?? pgm;
|
|
|
|
var bendSources = new HashSet<Entity>(
|
|
bendList.Where(b => b.SourceEntity != null).Select(b => b.SourceEntity)
|
|
);
|
|
|
|
drawing.SourceEntities = result.Entities.Where(e => !bendSources.Contains(e)).ToList();
|
|
|
|
drawing.SuppressedEntityIds = new HashSet<System.Guid>(
|
|
drawing
|
|
.SourceEntities.Where(e =>
|
|
!(e.Layer != null && e.Layer.IsVisible && e.IsVisible)
|
|
)
|
|
.Select(e => e.Id)
|
|
);
|
|
|
|
var sourceIds = new HashSet<System.Guid>(drawing.SourceEntities.Select(e => e.Id));
|
|
drawing.EntityRoles = entityRoles?
|
|
.Where(r => sourceIds.Contains(r.Key))
|
|
.ToDictionary(r => r.Key, r => r.Value) ?? new Dictionary<System.Guid, EntityRole>();
|
|
|
|
return drawing;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The entities an editor shows for an existing drawing, in its source frame: the stored
|
|
/// source entities with their suppression applied to <see cref="Entity.IsVisible"/>, or
|
|
/// for older drawings without them, the program geometry moved back by
|
|
/// <c>Source.Offset</c> without rapids.
|
|
/// </summary>
|
|
public static List<Entity> EditableEntities(Drawing drawing)
|
|
{
|
|
if (drawing.SourceEntities != null)
|
|
{
|
|
// Use stored entities with stable GUIDs; apply suppression state
|
|
var entities = new List<Entity>(drawing.SourceEntities);
|
|
|
|
foreach (var entity in entities)
|
|
entity.IsVisible = !drawing.SuppressedEntityIds.Contains(entity.Id);
|
|
|
|
return entities;
|
|
}
|
|
|
|
var derived = ConvertProgram.ToGeometry(drawing.Program);
|
|
|
|
// Re-apply source offset so entities appear in their natural position
|
|
if (drawing.Source?.Offset != null && drawing.Source.Offset != Vector.Zero)
|
|
{
|
|
foreach (var entity in derived)
|
|
entity.Offset(drawing.Source.Offset);
|
|
}
|
|
|
|
// Remove rapid traversals — they aren't part of the cut geometry
|
|
derived.RemoveAll(e => e.Layer == SpecialLayers.Rapid);
|
|
return derived;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Applies an edited rebuild of <paramref name="existing"/> in place, so placed parts keep
|
|
/// their drawing reference. Geometry, source, customer, quantity and bends are taken from
|
|
/// <paramref name="edited"/>; the library link and cut pins stay, since pins live in the
|
|
/// source frame and are resolved against the new program when used.
|
|
/// </summary>
|
|
public static void ApplyEdit(Drawing existing, Drawing edited)
|
|
{
|
|
existing.Program = edited.Program;
|
|
existing.SourceEntities = edited.SourceEntities;
|
|
existing.SuppressedEntityIds = edited.SuppressedEntityIds;
|
|
existing.EntityRoles = edited.EntityRoles;
|
|
existing.Source = edited.Source;
|
|
existing.Customer = edited.Customer;
|
|
existing.Quantity.Required = edited.Quantity.Required;
|
|
existing.Bends.Clear();
|
|
existing.Bends.AddRange(edited.Bends);
|
|
existing.Breakoffs = edited.Breakoffs;
|
|
existing.TabbedCutouts = edited.TabbedCutouts;
|
|
}
|
|
|
|
private static bool IsRepairMark(Entity e) =>
|
|
string.Equals(e.Layer?.Name, "ETCH", System.StringComparison.OrdinalIgnoreCase)
|
|
|| string.Equals(e.Layer?.Name, "SCRIBE", System.StringComparison.OrdinalIgnoreCase);
|
|
|
|
internal static void RemoveZeroSweepArcs(List<Entity> entities)
|
|
{
|
|
entities.RemoveAll(e =>
|
|
e is Arc arc && arc.StartAngle.IsEqualTo(arc.EndAngle, Tolerance.ChainTolerance)
|
|
);
|
|
}
|
|
|
|
internal static void RemoveDuplicateArcs(List<Entity> entities)
|
|
{
|
|
var circles = entities.OfType<Circle>().ToList();
|
|
var arcs = entities.OfType<Arc>().ToList();
|
|
var arcsToRemove = new List<Arc>();
|
|
|
|
foreach (var arc in arcs)
|
|
{
|
|
foreach (var circle in circles)
|
|
{
|
|
if (arc.Layer?.Name != circle.Layer?.Name)
|
|
continue;
|
|
|
|
if (!arc.Center.DistanceTo(circle.Center).IsEqualTo(0))
|
|
continue;
|
|
|
|
if (!arc.Radius.IsEqualTo(circle.Radius))
|
|
continue;
|
|
|
|
arcsToRemove.Add(arc);
|
|
break;
|
|
}
|
|
}
|
|
|
|
foreach (var arc in arcsToRemove)
|
|
entities.Remove(arc);
|
|
}
|
|
}
|
|
}
|