using System.Collections.Generic; using System.IO; using System.Linq; using OpenNest.Bending; using OpenNest.Converters; using OpenNest.Geometry; using OpenNest.IO.Bending; using OpenNest.Math; 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 = 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(); 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); } 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, }; } /// /// 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. /// public static Drawing BuildDrawing( CadImportResult result, IEnumerable entities, IEnumerable bends, int quantity, string customer, OpenNest.CNC.Program editedProgram ) { var visible = entities as IList ?? new List(entities); var bendList = bends as IList ?? new List(bends); var normalized = ShapeProfile.NormalizeEntities(visible); 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.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) ); return drawing; } 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); } } }