mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 14:51:35 -05:00
Entity ids came from a process-random sequence, so importing the same DXF twice produced different source-entity ids and therefore a different drawing library package (different SHA-256), and an importer could not tell an unchanged file from a changed one. CadImporter.Import now derives each entity id from the file's SHA-256 and the entity's position in the import. An unchanged file gives the same ids and the same package bytes; any change to the file gives entirely new ids, so suppression, roles and edge selections keyed by id never move to a different entity.
343 lines
14 KiB
C#
343 lines
14 KiB
C#
using System;
|
|
using System.Buffers.Binary;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Security.Cryptography;
|
|
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);
|
|
}
|
|
|
|
AssignFileIds(path, dxf.Entities);
|
|
|
|
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>
|
|
/// Gives each imported entity an id derived from the file's bytes and the entity's
|
|
/// position in the import, so importing an unchanged file again yields the same ids
|
|
/// (and the same library package), while any change to the file yields entirely new
|
|
/// ids: ids key suppression, roles and edge selections, so one must never move to
|
|
/// another entity.
|
|
/// </summary>
|
|
private static void AssignFileIds(string path, IList<Entity> entities)
|
|
{
|
|
Span<byte> input = stackalloc byte[32 + 4];
|
|
Span<byte> digest = stackalloc byte[32];
|
|
using (var stream = File.OpenRead(path))
|
|
SHA256.HashData(stream, input[..32]);
|
|
|
|
for (var i = 0; i < entities.Count; i++)
|
|
{
|
|
BinaryPrimitives.WriteInt32LittleEndian(input[32..], i);
|
|
SHA256.HashData(input, digest);
|
|
// RFC 9562 version 8 (custom) layout.
|
|
digest[7] = (byte)((digest[7] & 0x0F) | 0x80);
|
|
digest[8] = (byte)((digest[8] & 0x3F) | 0x80);
|
|
entities[i].Id = new Guid(digest[..16]);
|
|
}
|
|
}
|
|
|
|
/// <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);
|
|
}
|
|
}
|
|
}
|