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.Dimensions; using OpenNest.Geometry; using OpenNest.IO.Bending; using OpenNest.Math; using OpenNest.TabbedCutouts; namespace OpenNest.IO { /// /// Shared service that converts a CAD source file into a fully-populated /// . Used by the UI, console, MCP, API, and training /// tools so all code paths produce identical drawings. /// public static class CadImporter { /// /// Load a DXF file, run bend detection, and return a mutable result /// ready for interactive editing or direct conversion to a Drawing. /// public static CadImportResult Import(string path, CadImportOptions options = null) { options ??= CadImportOptions.Default; var dxf = Svg.IsSvg(path) ? Svg.Import(path, options) : 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); GeometryOptimizer.JoinFullCircles(cleanup); if (options.BendRepair != null) { dxf.Entities.RemoveAll(e => !IsRepairMark(e) && !cleanup.Contains(e)); dxf.Entities.AddRange(cleanup.Where(e => !dxf.Entities.Contains(e)).ToList()); } var bends = new List(); if (options.DetectBends && dxf.Document != null) { bends = options.BendDetectorName == null ? BendDetectorRegistry.AutoDetect(dxf.Document) : BendDetectorRegistry .GetByName(options.BendDetectorName) ?.DetectBends(dxf.Document) ?? new List(); } var repairReports = new List(); 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, RawEntities = dxf.RawEntities, Bends = bends, BendRepairReports = repairReports, Bounds = dxf.Entities.GetBoundingBox(), SourcePath = path, Name = options.Name ?? Path.GetFileNameWithoutExtension(path), Document = dxf.Document, Warnings = dxf.Warnings, }; } /// /// 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. /// private static void AssignFileIds(string path, IList entities) { Span input = stackalloc byte[32 + 4]; Span digest = stackalloc byte[32]; using (var stream = SharedFile.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]); } } /// /// Convenience for headless callers: Import a file and build a Drawing /// in a single call, using all loaded entities and detected bends. /// 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 ); } /// /// Build a fully-populated 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 /// /// Import result from . /// /// Entities to build the program from. Typically the currently visible subset. /// /// Bends to attach to the drawing. /// Required quantity. /// Customer name, or null. /// /// 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. /// /// /// Breakoffs to merge into the program and keep on the drawing; ones that cannot be /// resolved against are kept but not applied. /// /// /// Tabbed cutouts to merge into the program (after breakoffs) and keep on the drawing; /// ones that cannot be resolved are kept but not applied. /// /// /// 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. /// /// Dimensions to keep on the drawing; they never affect the program. public static Drawing BuildDrawing( CadImportResult result, IEnumerable entities, IEnumerable bends, int quantity, string customer, OpenNest.CNC.Program editedProgram, IReadOnlyList breakoffs = null, IReadOnlyList tabbedCutouts = null, IReadOnlyDictionary entityRoles = null, IReadOnlyList dimensions = null ) { var visible = entityRoles is { Count: > 0 } ? EntityRoles.ForProgram(entities, entityRoles) : entities as IList ?? new List(entities); var bendList = bends as IList ?? new List(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(); drawing.TabbedCutouts = tabbedCutouts?.Select(c => c.Clone()).ToList() ?? new List(); drawing.Dimensions = dimensions?.Select(d => d.Clone()).ToList() ?? new List(); drawing.Program = editedProgram ?? pgm; var bendSources = new HashSet( bendList.Where(b => b.SourceEntity != null).Select(b => b.SourceEntity) ); drawing.SourceEntities = result.Entities.Where(e => !bendSources.Contains(e)).ToList(); drawing.SuppressedEntityIds = new HashSet( drawing .SourceEntities.Where(e => !(e.Layer != null && e.Layer.IsVisible && e.IsVisible) ) .Select(e => e.Id) ); var sourceIds = new HashSet(drawing.SourceEntities.Select(e => e.Id)); drawing.EntityRoles = entityRoles? .Where(r => sourceIds.Contains(r.Key)) .ToDictionary(r => r.Key, r => r.Value) ?? new Dictionary(); return drawing; } /// /// The entities an editor shows for an existing drawing, in its source frame: the stored /// source entities with their suppression applied to , or /// for older drawings without them, the program geometry moved back by /// Source.Offset without rapids. /// public static List EditableEntities(Drawing drawing) { if (drawing.SourceEntities != null) { // Use stored entities with stable GUIDs; apply suppression state var entities = new List(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; } /// /// Applies an edited rebuild of in place, so placed parts keep /// their drawing reference. Geometry, source, customer, quantity and bends are taken from /// ; the library link and cut pins stay, since pins live in the /// source frame and are resolved against the new program when used. /// 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; existing.Dimensions = edited.Dimensions; } 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 entities) { entities.RemoveAll(e => e is Arc arc && arc.StartAngle.IsEqualTo(arc.EndAngle, Tolerance.ChainTolerance) ); } internal static void RemoveDuplicateArcs(List entities) { var circles = entities.OfType().ToList(); var arcs = entities.OfType().ToList(); var arcsToRemove = new List(); 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); } } }