using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using OpenNest.Geometry; using OpenNest.IO; namespace OpenNest.Benchmark { /// /// Builds a BenchmarkJob from a JSON manifest that lists DXF files and the /// quantity of each to nest. DXF paths resolve relative to the manifest. /// Sheet sizes come from the manifest or the caller's override; unlike a /// .nest file there is no plate to inherit them from, so a job with none is /// an error. Sheet sizes must use the same units as the DXFs. /// public static class DxfManifestLoader { /// Suffix a manifest needs to be picked up when scanning a folder. public const string FolderSuffix = ".manifest.json"; private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, ReadCommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true, }; public static BenchmarkJob Load( string manifestPath, IReadOnlyList sheetSizeOverrides = null, double? partSpacingOverride = null ) { var manifest = ReadManifest(manifestPath); var baseDir = Path.GetDirectoryName(Path.GetFullPath(manifestPath)); if (manifest.Parts == null || manifest.Parts.Count == 0) throw new InvalidOperationException( $"Manifest '{manifestPath}' has no parts. Add entries to \"parts\"." ); var sizes = ResolveSheetSizes(manifest, sheetSizeOverrides, manifestPath); var requests = manifest.Parts.Select(p => BuildRequest(p, baseDir)).ToList(); return new BenchmarkJob { SourceFile = manifestPath, CandidateSizes = sizes, EdgeSpacing = new Spacing(manifest.EdgeSpacing, manifest.EdgeSpacing), PartSpacing = partSpacingOverride ?? manifest.Spacing, Quadrant = manifest.Quadrant, Requests = requests, OffcutCut = OffcutCut(manifest.OffcutCut, manifestPath), }; } /// "horizontal" or "vertical" (any case); absent counts every edge strip. /// private static CutOffAxis? OffcutCut(string value, string manifestPath) => value?.ToLowerInvariant() switch { null => null, "horizontal" => CutOffAxis.Horizontal, "vertical" => CutOffAxis.Vertical, _ => throw new InvalidOperationException( $"Manifest '{manifestPath}' has offcutCut \"{value}\"; " + "use \"horizontal\" or \"vertical\"." ), }; private static Manifest ReadManifest(string manifestPath) { try { return JsonSerializer.Deserialize( File.ReadAllText(manifestPath), JsonOptions ) ?? throw new InvalidOperationException("The manifest is empty."); } catch (JsonException ex) { throw new InvalidOperationException( $"Manifest '{manifestPath}' is not valid JSON: {ex.Message}", ex ); } } private static List ResolveSheetSizes( Manifest manifest, IReadOnlyList overrides, string manifestPath ) { if (overrides != null && overrides.Count > 0) return overrides.ToList(); var sizes = new List(); foreach (var text in manifest.SheetSizes ?? new List()) { if (!JobLoader.TryParseSheetSize(text, out var size)) throw new InvalidOperationException( $"Manifest '{manifestPath}': could not parse sheet size '{text}' (expected e.g. \"48x96\")." ); sizes.Add(size); } if (sizes.Count == 0) throw new InvalidOperationException( $"Manifest '{manifestPath}' has no sheet sizes. Set \"sheetSizes\" or pass --sheet-sizes." ); return sizes.Distinct().ToList(); } private static DrawingRequest BuildRequest(ManifestPart part, string baseDir) { if (string.IsNullOrWhiteSpace(part.Dxf)) throw new InvalidOperationException("A manifest part is missing \"dxf\"."); if (part.Quantity <= 0) throw new InvalidOperationException( $"Manifest part '{part.Dxf}': quantity must be greater than 0 (was {part.Quantity})." ); var dxfPath = Path.GetFullPath(Path.Combine(baseDir, part.Dxf)); if (!File.Exists(dxfPath)) throw new FileNotFoundException($"DXF file not found: {dxfPath}", dxfPath); Drawing drawing; try { drawing = CadImporter.ImportDrawing( dxfPath, new CadImportOptions { Quantity = part.Quantity } ); } catch (Exception ex) { throw new InvalidOperationException($"Failed to import DXF: {dxfPath}", ex); } if (drawing.Program == null || drawing.Program.Codes.Count == 0) throw new InvalidOperationException($"Failed to import DXF: {dxfPath}"); // A zero legacy step means automatic rotation to DrawingJobMapper, so lock it explicitly. // Quarter turns are a 90° sweep from 0 to 270°; automatic rotation allows any angle. var rotations = RotationSet(part); if (rotations != RotationChoice.Any) { var quarter = rotations == RotationChoice.QuarterTurns; drawing.Constraints ??= new NestConstraints(); drawing.Constraints.StepAngle = quarter ? OpenNest.Math.Angle.HalfPI : OpenNest.Math.Angle.TwoPI; drawing.Constraints.StartAngle = 0; drawing.Constraints.EndAngle = quarter ? 3 * OpenNest.Math.Angle.HalfPI : 0; drawing.Constraints.Allow180Equivalent = rotations == RotationChoice.HalfTurns; } var constraints = drawing.Constraints; return new DrawingRequest { Drawing = drawing, Quantity = part.Quantity, Priority = drawing.Priority, StepAngle = constraints?.StepAngle ?? 0, RotationStart = constraints?.StartAngle ?? 0, RotationEnd = constraints?.EndAngle ?? 0, }; } private enum RotationChoice { None, HalfTurns, QuarterTurns, Any, } /// /// A part's "rotations" (degrees) must be {0}, {0, 180} or {0, 90, 180, 270}; without it, /// "allowRotation" picks any angle (true, the default: automatic rotation) or none. /// private static RotationChoice RotationSet(ManifestPart part) { if (part.Rotations == null) return part.AllowRotation ? RotationChoice.Any : RotationChoice.None; if (!part.AllowRotation) throw new InvalidOperationException( $"Manifest part '{part.Dxf}': set \"rotations\" or \"allowRotation\", not both." ); var set = part.Rotations .Select(degrees => ((degrees % 360) + 360) % 360) .Distinct() .OrderBy(degrees => degrees) .ToArray(); if (set.SequenceEqual(new[] { 0.0 })) return RotationChoice.None; if (set.SequenceEqual(new[] { 0.0, 180.0 })) return RotationChoice.HalfTurns; if (set.SequenceEqual(new[] { 0.0, 90.0, 180.0, 270.0 })) return RotationChoice.QuarterTurns; throw new InvalidOperationException( $"Manifest part '{part.Dxf}': \"rotations\" must be [0], [0, 180] or " + $"[0, 90, 180, 270] (was [{string.Join(", ", part.Rotations)}])." ); } private class Manifest { public List SheetSizes { get; set; } public double Spacing { get; set; } public double EdgeSpacing { get; set; } public int Quadrant { get; set; } = 1; public string OffcutCut { get; set; } public List Parts { get; set; } } private class ManifestPart { public string Dxf { get; set; } public int Quantity { get; set; } public bool AllowRotation { get; set; } = true; public List Rotations { get; set; } } } }